feat(wiki): index ページ・活動ログ・stale 検出を追加 (P4, #598)#604
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis 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 Changes
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
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__
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
| 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 }, | ||
| }); |
There was a problem hiding this comment.
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;
});There was a problem hiding this comment.
| 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 }, | ||
| }); |
There was a problem hiding this comment.
対応不要(意図された現状仕様)。
コメント 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.
There was a problem hiding this comment.
💡 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".
| const apiKeyName = getProviderApiKeyName(provider); | ||
| const apiKey = process.env[apiKeyName]; |
There was a problem hiding this comment.
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 👍 / 👎.
|
|
||
| // --- Model access & usage enforcement (mirrors /api/ai/chat) --- | ||
| const tier = await getUserTier(userId, db); | ||
| const modelInfo = await validateModelAccess(model, tier, db); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 | 🟠 MajorReplace 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 interfacesandInclude 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 | 🟡 MinorExpose selected state to assistive tech.
The row is a toggle-like control, but Line 24 button does not expose pressed/selected state. Add
aria-pressedso 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 | 🟡 MinorDrop blank entities after trimming.
Whitespace-only
title/summaryvalues 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 | 🟡 MinorReplace 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
LintRuletype missingstalerule.The backend schema includes
"stale"as a valid lint rule (used inrunStaleRule), 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 | 🟡 MinorFail fast on malformed
conflictsto keep parser truly strict.Line 235–248 currently drops invalid
conflictsvalues 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}:anytype 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 upstreamvalidateModelAccess()inusageService.tsthrows plainErrorinstances 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 refactoringusageService.tsto 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 thesignalproperty.The new
signalproperty 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: Makeconsole.errorspy cleanup failure-safe.If an assertion fails before Line 51, the spy can leak into subsequent tests. Prefer
try/finallyaround 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: Constrainkind/actorto known values at DB level.Using plain
texthere allows silent drift/typos in activity taxonomy. ConsiderCHECKconstraints (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
videoIdis 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: Missingwiki_generatecase in formatDetail.The
wiki_generatekind is defined inKIND_LABELSbut has no dedicated formatting informatDetail. It falls back toJSON.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.
fetchPageFindingsreturns 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
stalelint rule, butruleIconandruleLabeldon't have explicit cases for it. It will fall through to the default (Infoicon, 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: Clamplimitbefore querying to avoid fetching unbounded rows.Currently,
limitis only clamped in the response (Line 132), butlistActivityForOwnerreceives the raw value. If a caller passeslimit=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 redundantgetUserTierandvalidateModelAccesscalls.Lines 187-188 re-fetch
tierandmodelInfothat were already obtained at Lines 148-149 (whenhasProvider && hasModel). Since the usage-recording block is only entered whenaiProvider && aiModel && body.modelare 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 thepageContentsupsert inside the transaction.The
pageContentsupsert (Lines 127-138) happens outside the transaction that creates/updates thepagesrow. 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 NULLand sort bycreated_at, but the current indexes only coverowner_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;anytype 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
buildSchemaBlockis internal, adding@internalor 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 addingmax_tokensfor 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
languagesfalls back todetectOcrLanguages, but the implementation defaults to["eng"]directly. Consider usingdetectOcrLanguages()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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockserver/api/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (111)
.env.exampleadmin/src/App.tsxadmin/src/api/activity.tsadmin/src/api/lint.tsadmin/src/pages/ActivityLog.tsxadmin/src/pages/Layout.tsxadmin/src/pages/wiki-health/WikiHealthContent.tsxadmin/src/pages/wiki-health/index.tsxpackage.jsonserver/api/drizzle/0007_add_sources_and_page_sources.sqlserver/api/drizzle/0008_add_lint_findings.sqlserver/api/drizzle/0009_add_page_is_schema.sqlserver/api/drizzle/0010_add_activity_log.sqlserver/api/drizzle/0011_add_page_special_kind.sqlserver/api/drizzle/meta/_journal.jsonserver/api/package.jsonserver/api/src/__tests__/routes/clip.test.tsserver/api/src/__tests__/routes/ext.test.tsserver/api/src/__tests__/routes/ingest.test.tsserver/api/src/__tests__/services/activityLogService.test.tsserver/api/src/__tests__/services/indexBuilder.test.tsserver/api/src/__tests__/services/ingestPlanner.test.tsserver/api/src/app.tsserver/api/src/lib/aiAccessHelpers.tsserver/api/src/lib/articleExtractor.tsserver/api/src/lib/clipAndCreate.tsserver/api/src/lib/youtubeEmbedServerExtension.tsserver/api/src/lib/youtubeExtractor.test.tsserver/api/src/lib/youtubeExtractor.tsserver/api/src/routes/activity.tsserver/api/src/routes/clip.tsserver/api/src/routes/ext.tsserver/api/src/routes/ingest.tsserver/api/src/routes/lint.tsserver/api/src/routes/wikiSchema.tsserver/api/src/schema/activityLog.tsserver/api/src/schema/index.tsserver/api/src/schema/lintFindings.tsserver/api/src/schema/pageSources.tsserver/api/src/schema/pages.tsserver/api/src/schema/relations.tsserver/api/src/schema/sources.tsserver/api/src/services/activityLogService.tsserver/api/src/services/indexBuilder.tsserver/api/src/services/ingestPlanner.tsserver/api/src/services/lintEngine/index.tsserver/api/src/services/lintEngine/rules/brokenLink.tsserver/api/src/services/lintEngine/rules/conflict.test.tsserver/api/src/services/lintEngine/rules/conflict.tsserver/api/src/services/lintEngine/rules/ghostMany.tsserver/api/src/services/lintEngine/rules/orphan.tsserver/api/src/services/lintEngine/rules/stale.test.tsserver/api/src/services/lintEngine/rules/stale.tsserver/api/src/services/lintEngine/rules/titleSimilar.test.tsserver/api/src/services/lintEngine/rules/titleSimilar.tsserver/api/src/services/lintEngine/types.tsserver/api/src/services/youtubeService.test.tsserver/api/src/services/youtubeService.tssrc/App.tsxsrc/components/ai-chat/AIChatMessage.tsxsrc/components/ai-chat/AIChatMessages.tsxsrc/components/ai-chat/AIChatPanelContent.tsxsrc/components/ai-chat/EntityRow.tsxsrc/components/ai-chat/PromoteToWikiDialog.tsxsrc/components/editor/PageEditor/PageEditorContent.tsxsrc/components/editor/PageEditor/usePendingChatPageGeneration.tssrc/components/editor/extensions/MarkdownPasteExtension.test.tssrc/components/editor/extensions/WikiLinkExtension.test.tssrc/components/editor/extensions/WikiLinkExtension.tssrc/components/editor/extensions/transformWikiLinksInContent.test.tssrc/components/editor/extensions/transformWikiLinksInContent.tssrc/components/layout/ImageCreateDialog.test.tsxsrc/components/layout/ImageCreateDialog.tsxsrc/components/page/LintSuggestions.tsxsrc/hooks/useImageUpload.test.tssrc/hooks/useImageUpload.tssrc/hooks/usePromoteToWiki.tssrc/hooks/useWebClipper.test.tssrc/hooks/useWebClipper.tssrc/hooks/useWikiSchema.tssrc/i18n/index.tssrc/i18n/locales/en/aiChat.jsonsrc/i18n/locales/en/wikiSchema.jsonsrc/i18n/locales/ja/aiChat.jsonsrc/i18n/locales/ja/wikiSchema.jsonsrc/lib/ai/describeImage.test.tssrc/lib/ai/describeImage.tssrc/lib/aiChat/extractEntitiesPrompt.test.tssrc/lib/aiChat/extractEntitiesPrompt.tssrc/lib/api/apiClient.test.tssrc/lib/api/apiClient.tssrc/lib/contentUtils.tssrc/lib/markdownExport.tssrc/lib/ocr/tesseractOcr.test.tssrc/lib/ocr/tesseractOcr.tssrc/lib/storage/providers/GitHubProvider.tssrc/lib/storage/providers/GoogleDriveProvider.tssrc/lib/storage/providers/GyazoProvider.tssrc/lib/storage/providers/S3Provider.test.tssrc/lib/storage/providers/S3Provider.tssrc/lib/storage/types.tssrc/lib/wikiGenerator.test.tssrc/lib/wikiGenerator.tssrc/lib/wikiGenerator/wikiGeneratorFromChatPrompt.test.tssrc/lib/wikiGenerator/wikiGeneratorFromChatPrompt.tssrc/lib/wikiGenerator/wikiGeneratorPrompt.tssrc/lib/wikiGenerator/wikiGeneratorProviders.tssrc/pages/AIChatDetail.tsxsrc/pages/IndexPage.tsxsrc/pages/WikiSchemaPage.tsxsrc/types/chatPageGeneration.ts
| const RULE_LABELS: Record<LintRule, string> = { | ||
| orphan: "孤立ページ / Orphan", | ||
| ghost_many: "Ghost Link 過多 / Ghost Excess", | ||
| title_similar: "タイトル類似 / Title Similar", | ||
| conflict: "矛盾 / Conflict", | ||
| broken_link: "リンク切れ / Broken Link", | ||
| }; |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsxRepository: 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.
There was a problem hiding this comment.
現コードを確認したところ、admin/src/api/lint.ts の LintRule 型・admin/src/pages/wiki-health/WikiHealthContent.tsx の RULE_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.
There was a problem hiding this comment.
@otomatty, ご確認ありがとうございます。ファイル全体を見ると、現在のコードでは stale: "古い情報 / Stale" がすでに RULE_LABELS に含まれており、admin/src/api/lint.ts の LintRule 型にも "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.
| 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]); |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -100Repository: otomatty/zedi
Length of output: 2856
🏁 Script executed:
# Look for any react-hooks related rules
grep -i "react-hooks\|set-state" eslint.config.jsRepository: otomatty/zedi
Length of output: 141
🏁 Script executed:
# Check if there are any lint errors by examining the rules more
cat eslint.config.jsRepository: 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.jsRepository: 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 -20Repository: otomatty/zedi
Length of output: 146
🏁 Script executed:
# Check if there's a specific rule override for exhaustive-deps
grep -i "exhaustive" eslint.config.jsRepository: 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 -20Repository: 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.tsxRepository: 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 -5Repository: 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 -1Repository: 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 -5Repository: 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 -10Repository: 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.tsRepository: 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 -50Repository: 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 -40Repository: 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 -30Repository: 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 -5Repository: 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 -10Repository: 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.tsxRepository: 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 -30Repository: 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 -5Repository: 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 -60Repository: 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.tsxRepository: 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 -30Repository: 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.tsxRepository: 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.
There was a problem hiding this comment.
対応不要と判断しました。
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.
There was a problem hiding this comment.
🧩 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.
| 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]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd WikiSchemaPage.tsxRepository: otomatty/zedi
Length of output: 85
🏁 Script executed:
# Check the file size and read the relevant sections
wc -l src/pages/WikiSchemaPage.tsxRepository: otomatty/zedi
Length of output: 89
🏁 Script executed:
# Read the file to see the exact code
cat -n src/pages/WikiSchemaPage.tsxRepository: 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 || trueRepository: 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 -20Repository: 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.
There was a problem hiding this comment.
対応不要と判断しました(同上)。
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.
There was a problem hiding this comment.
@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.
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>
|
レビューコメントを精査し、本 PR 由来の指摘のみを修正しました。対応区分を以下にまとめます。 修正済み(commit 37948e4)
対応不要(本 PR の変更対象外 / 別 Issue 推奨)
いずれも妥当な指摘なので、後続 PR / 別 Issue として切り出すのが適切だと思います。 誤検知(対応不要)
|
- 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
レビュー対応サマリ / Review response summaryCodeRabbit / Devin / Bito 等からの全 28 件のレビューコメントを精査し、対応/対応不要を判断しました。 対応済み (15 件) — commits
|
|
再レビューを開始します。 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
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 UPDATEdoes 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 102locks 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 | 🟠 MajorAdd 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.errormay stay mocked. Usingtry/finally(orafterEach(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 forkindandactor.Using plain
texthere 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 emptyydocStateintent.The empty buffer for
ydocStateis 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
📒 Files selected for processing (37)
admin/src/App.tsxadmin/src/api/activity.tsadmin/src/api/lint.tsadmin/src/pages/ActivityLog.tsxadmin/src/pages/Layout.tsxadmin/src/pages/wiki-health/WikiHealthContent.tsxserver/api/drizzle/0010_add_activity_log.sqlserver/api/drizzle/0011_add_page_special_kind.sqlserver/api/drizzle/meta/_journal.jsonserver/api/src/__tests__/services/activityLogService.test.tsserver/api/src/__tests__/services/indexBuilder.test.tsserver/api/src/app.tsserver/api/src/lib/articleExtractor.tsserver/api/src/routes/activity.tsserver/api/src/routes/ingest.tsserver/api/src/routes/wikiSchema.tsserver/api/src/schema/activityLog.tsserver/api/src/schema/index.tsserver/api/src/schema/lintFindings.tsserver/api/src/schema/pages.tsserver/api/src/schema/relations.tsserver/api/src/services/activityLogService.tsserver/api/src/services/indexBuilder.tsserver/api/src/services/lintEngine/index.tsserver/api/src/services/lintEngine/rules/brokenLink.tsserver/api/src/services/lintEngine/rules/conflict.tsserver/api/src/services/lintEngine/rules/orphan.tsserver/api/src/services/lintEngine/rules/stale.test.tsserver/api/src/services/lintEngine/rules/stale.tssrc/App.tsxsrc/components/layout/ImageCreateDialog.tsxsrc/hooks/useImageUpload.tssrc/hooks/useWikiSchema.tssrc/lib/markdownExport.tssrc/lib/wikiGenerator/wikiGeneratorFromChatPrompt.tssrc/pages/IndexPage.tsxsrc/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>
|
Devin のレビュー 2 件への対応状況です。 修正済み
対応済み(ただし他コミット由来)
|
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>
Summary
Karpathy "LLM Wiki" パターンの P4 フェーズを実装する(#598, 親エピック #594)。
__index__(pages.special_kind = '__index__')として Markdown 本文を保存する。POST /api/activity/index/rebuildで再構築。初期実装はルールベースで LLM 不要 — 将来 LLM/embedding クラスタに差し替え可能。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—sources.extracted_at > pages.updated_atのページを warn レベルで検出。lint_findingsに既存スキーマのまま追加できるようLintRule型を拡張。recordActivityは DB 失敗時に throw せずconsole.errorするのみ(ログが本処理を巻き込まないように)。Test plan
bun run test:run— 新規 3 テストファイル / 25 テスト すべて passbun x tsc --noEmit(server/api, admin)— 既存の無関係なエラー以外クリーンbun x eslint— 新規ファイルはエラー 0(@Property TSDoc 警告は既存 schema 同様)bun x prettier --check— 新規 / 変更ファイルすべてクリーンdrizzle:migrateの適用確認はレビュー時に)__index__ページ生成 →/page/:idで開いて Markdown 表示を確認lint run→staleが finding に現れることを page_sources 付きページで確認Notes for reviewer
activity_logはai_usage_logs(課金)・admin_audit_logs(管理監査)とは用途が別で、ユーザー自身の Wiki 成長ログ。管理画面から見えるのは今のところ「自分の」データのみ。special_kindの部分ユニーク制約で、オーナーごとに各 kind は最大 1 ページ。🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Improvements
Bug Fixes