Skip to content

feat: Propagate page title renames to WikiLinks, tags, and ghost links (issue #726 Phase 2)#736

Merged
otomatty merged 2 commits into
developfrom
claude/fix-issue-726-UDzFs
Apr 24, 2026
Merged

feat: Propagate page title renames to WikiLinks, tags, and ghost links (issue #726 Phase 2)#736
otomatty merged 2 commits into
developfrom
claude/fix-issue-726-UDzFs

Conversation

@otomatty
Copy link
Copy Markdown
Owner

@otomatty otomatty commented Apr 24, 2026

概要

ページのタイトル変更を、参照元ドキュメント内の WikiLink / タグマークおよびゴーストリンクへ自動的に伝播させる機能を実装しました。タイトル変更時に関連するすべての参照を一貫性のある状態に保ちます。

変更点

  • titleRenamePropagationService.ts (新規): タイトル変更の伝播を統合するメインサービス

    • 実体リンク経由で参照しているすべてのソースページの Y.Doc を書き換え
    • ゴーストリンクのテキストが新タイトルと一致する場合、実体リンクへ昇格
    • 各ソースページ書き換え後、Hocuspocus のライブドキュメントを無効化(ベストエフォート)
    • 個別ページの書き換え失敗時も他のページとゴースト昇格処理は継続(ベストエフォート)
  • ydocRenameRewrite.ts (新規): Y.Doc 内の WikiLink / タグマークを書き換えるピュアヘルパー

    • WikiLink マーク(attrs.title)と tag マーク(attrs.name)を対象
    • マッチは大文字小文字・前後スペース除去で正規化
    • セグメントテキストが旧タイトルと一致する場合のみテキストを書き換え(手動編集は保持)
    • タグ書き換えは新タイトルが有効なタグ文字集合のときのみ実行
  • hocuspocusInvalidation.ts (新規): Hocuspocus のライブドキュメント無効化ヘルパー

    • スナップショット復元とタイトルリネーム伝播の共通呼び出し元
    • ネットワーク失敗・タイムアウトはログのみで呼び出し側に伝播させない
  • routes/pages.ts (修正): PUT /content エンドポイントでタイトル変更を検出

    • タイトル更新時に旧タイトルを SELECT してから UPDATE を実行
    • 正規化(小文字化)して比較し、表記揺れだけの変更は伝播をスキップ
    • 変更確定後、fire-and-forget で伝播処理を起動
  • routes/pageSnapshots.ts (修正): Hocuspocus 無効化ロジックを共通ライブラリへ抽出

  • テスト (新規): 3 つの包括的なテストスイート

    • ydocRenameRewrite.test.ts: Y.Doc 書き換えロジックの単体テスト(294 行)
    • titleRenamePropagationService.test.ts: サービス統合テスト(282 行)
    • pages.test.ts: エンドポイント統合テスト(50 行追加)

変更の種類

  • ✨ 新機能 (New feature)
  • 🧪 テスト (Tests)
  • 🎨 スタイル/リファクタリング (Style/Refactor)

テスト方法

  1. ユニットテスト実行: npm test で以下が実

https://claude.ai/code/session_01BMij57Zvw2nvhf7tdKwQ3f


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Page title renames now propagate in the background to update wiki-links and tags across documents without blocking the save response.
    • Ghost links matching a new title are promoted to real links; failures are logged but do not abort the overall propagation.
    • Collaborative-document invalidation is performed best-effort after updates.
  • Utilities

    • Content-extraction and preview helpers for rich document content.
  • Tests

    • Added thorough tests covering rename rewriting, propagation, ghost promotion, and edge cases.

…#726 Phase 2)

When a page title changes via PUT /api/pages/:id/content, traverse every
source document referencing it and rewrite the matching `wikiLink` marks
(`attrs.title`) and `tag` marks (`attrs.name`) inside the Y.Doc stored in
`page_contents.ydoc_state`. Segment text is only replaced when it still
matches the old title (preserving manually-edited link text), while the
mark attribute is always refreshed. Ghost links whose text now matches
the new title are promoted to real `links` rows.

Design choices (see issue #726 open questions):
- Background (fire-and-forget) propagation; the main PUT response is not
  blocked on the rewrite.
- Row-locked per-source transactions serialize with Hocuspocus's
  `onStoreDocument` writes; the live Y.Doc cache is invalidated after each
  successful source-page rewrite so collaborators reload from DB.
- Best-effort: per-source failures are logged and counted, never aborting
  the batch or the originating request.

Also extracts `invalidateHocuspocusDocument` into
`server/api/src/lib/hocuspocusInvalidation.ts` so both the rename flow
and the existing snapshot-restore flow share the same helper.

Closes #726.

https://claude.ai/code/session_01BMij57Zvw2nvhf7tdKwQ3f
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Detects meaningful page title renames on PUT, returns response immediately, and fires a background propagation task that rewrites Yjs wiki-link/tag marks, promotes matching ghost links to real links, persists updated Y.Doc state and previews, and best-effort invalidates Hocuspocus documents.

Changes

Cohort / File(s) Summary
Tests
server/api/src/__tests__/routes/pages.test.ts, server/api/src/__tests__/services/titleRenamePropagationService.test.ts, server/api/src/__tests__/services/ydocRenameRewrite.test.ts
Added tests for PUT title-rename detection, full propagation service behavior (early-exits, rewrites, ghost promotion, partial failures), and Yjs rewrite correctness (marks/text, normalization, nesting).
Hocuspocus Invalidation
server/api/src/lib/hocuspocusInvalidation.ts
New shared helper invalidateHocuspocusDocument(pageId, opts?) performing best-effort POST to Hocuspocus internal invalidate endpoint with timeout, logging, and env-var gating.
Routes
server/api/src/routes/pages.ts, server/api/src/routes/pageSnapshots.ts
Pages PUT updated to detect/return optional {oldTitle,newTitle} for meaningful renames and to spawn background propagation; snapshot restore switched to use the shared invalidation helper.
Rename Propagation Service
server/api/src/services/titleRenamePropagationService.ts
New propagateTitleRename implementing per-source Y.Doc rewriting, atomic persistence (page_contents + pages preview/version), Hocuspocus invalidation hook, ghost-link deletion→real-link insertion, error-tolerant counters and return shape.
Yjs Rewrite Helper
server/api/src/services/ydocRenameRewrite.ts
New rewriteTitleRefsInDoc that updates wikiLink/tag marks and optionally visible text under normalized matching, with counters and transaction-scoped mutations.
Text Extraction Utility
server/api/src/lib/extractPlainTextFromYXml.ts
New extractor to produce plain-text previews from Yjs XML, plus CONTENT_PREVIEW_MAX_LENGTH and buildContentPreview helper used when persisting previews.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as API Server (pages.ts)
    participant DB as Database
    participant YRewrite as Yjs Rewriter
    participant Hocus as Hocuspocus

    Client->>API: PUT /pages/:id/content (with body.title)
    activate API
    API->>DB: SELECT current title
    API->>DB: UPDATE/UPSERT page_contents & pages
    API-->>Client: 200 OK
    deactivate API

    Note over API: Background task — propagateTitleRename

    API->>DB: SELECT source pages linking → renamedPageId
    loop per source page
        DB->>API: return sourceId, page_contents.ydocState
        API->>YRewrite: rewriteTitleRefsInDoc(old→new)
        YRewrite-->>API: RewriteResult
        alt changes present
            API->>DB: UPDATE page_contents (ydocState, version, contentText, updatedAt)
            API->>DB: UPDATE pages (contentPreview, updatedAt)
            API->>Hocus: POST /internal/documents/:id/invalidate
        end
    end

    par ghost promotion
        API->>DB: SELECT ghostLinks matching newTitle
        API->>DB: DELETE ghostLinks
        API->>DB: INSERT links (onConflictDoNothing)
    end

    API-->>API: Log summary counters
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I nudged a title, soft and sly,
Links hopped along as I hopped by.
Ghosts turned real with gentle taps,
Yjs pages shimmered, fixed the gaps.
Hocuspocus blinked — caches fly!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: propagating page title renames to WikiLinks, tags, and ghost links (issue #726 Phase 2), which aligns with the comprehensive changes across routes, services, and tests in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-issue-726-UDzFs

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request implements Phase 2 of the title rename propagation feature (Issue #726). It introduces a service to update WikiLink and tag marks across all referencing documents when a page is renamed, and promotes matching ghost links to real links. Key additions include the titleRenamePropagationService, a ydocRenameRewrite helper for Y.Doc mutations, and a centralized hocuspocusInvalidation utility to ensure clients reload updated content. Feedback was provided regarding a performance optimization in the Y.Doc node iteration logic to avoid O(N^2) complexity by using an iterator or converting the node to an array.

Comment on lines +216 to +217
for (let i = 0; i < total; i++) {
const child = node.get(i) as XmlNode;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Yjs の Y.XmlFragmentY.XmlElement において、node.get(i) はインデックス i に比例した計算量(O(i))がかかります。そのため、このループ全体では要素数 N に対して O(N^2) の計算量となり、ドキュメントが大きくなった場合にパフォーマンスが著しく低下する恐れがあります。

node.toArray() を一度呼び出して配列に変換してからループを回すか、イテレータを使用することで O(N) に改善できます。

  for (const child of node.toArray()) {
    if (child instanceof Y.XmlText) {

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11cc096bc7

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +191 to +192
sql`LOWER(TRIM(${ghostLinks.linkText})) = LOWER(TRIM(${newTitle}))`,
ne(ghostLinks.sourcePageId, renamedPageId),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict ghost-link promotion to renamed page scope

promoteGhostLinks currently deletes/promotes rows by link_text match only, so a rename in one workspace/user scope will also consume unrelated ghost_links from other owners/notes that happen to use the same title text. In a multi-user database this can silently create cross-tenant links edges (and remove the original ghosts), corrupting reference graphs outside the renamed page’s scope. The promotion query should join/filter by the renamed page’s ownership/note scope (or equivalent scope key) before deleting and inserting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@server/api/src/lib/hocuspocusInvalidation.ts`:
- Around line 46-53: The code currently returns silently when baseUrl or
internalSecret is missing; update the check around baseUrl/internalSecret to
emit a clear error in non-development environments (use processLogger.error or
console.error) including which envs are missing (HOCUSPOCUS_INTERNAL_URL /
BETTER_AUTH_SECRET) and contextual identifiers (prefix and pageId) so operators
see misconfiguration, then still return (or optionally throw) to prevent silent
invalidation disabling; ensure you reference the same variables baseUrl,
internalSecret, prefix, and pageId in the log.

In `@server/api/src/services/titleRenamePropagationService.ts`:
- Around line 163-169: The transaction updates only ydocState
(tx.update(pageContents).set) so the visible plain-text and preview stay stale;
after reconstructing or applying edits to the Y.Doc used to produce
encodedState, compute the new plain text/preview (e.g., derive contentText and
preview from the rewritten Y.Doc) and include those fields in the same
tx.update(...) .set call alongside ydocState, version, and updatedAt so
page_contents.contentText and any preview metadata are persisted atomically with
ydocState.
- Around line 261-271: The rewrite currently only matches by title and can
retarget links when duplicate titles exist; update the flow to carry the renamed
page's ID through the rewrite so rewrites match both title and identity. Change
the call site in titleRenamePropagationService where you call
rewriteSourcePage(db, sourceId, trimmedOld, trimmedNew) to pass the
renamedPageId as an additional argument, update rewriteSourcePage(...) signature
to accept targetId and forward it into rewriteTitleRefsInDoc(...), and modify
rewriteTitleRefsInDoc(oldTitle, newTitle, ...) to accept the targetId and only
replace WikiLink/tag marks when the normalized title matches AND the mark’s
resolved target id (or explicit id in the mark) equals the renamedPageId (or
skip marks without identity); alternatively, if you prefer enforcing uniqueness,
add a schema unique constraint on pages.title and migrate data -- pick one
approach and implement the corresponding signature and matching changes for
rewriteSourcePage and rewriteTitleRefsInDoc.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59c2e86f-afcc-4053-9462-b836d7929ac6

📥 Commits

Reviewing files that changed from the base of the PR and between 4a54b19 and 11cc096.

📒 Files selected for processing (8)
  • server/api/src/__tests__/routes/pages.test.ts
  • server/api/src/__tests__/services/titleRenamePropagationService.test.ts
  • server/api/src/__tests__/services/ydocRenameRewrite.test.ts
  • server/api/src/lib/hocuspocusInvalidation.ts
  • server/api/src/routes/pageSnapshots.ts
  • server/api/src/routes/pages.ts
  • server/api/src/services/titleRenamePropagationService.ts
  • server/api/src/services/ydocRenameRewrite.ts

Comment thread server/api/src/lib/hocuspocusInvalidation.ts
Comment thread server/api/src/services/titleRenamePropagationService.ts
Comment on lines +261 to +271
const sourceRows = await db
.select({ sourceId: links.sourceId })
.from(links)
.where(eq(links.targetId, renamedPageId));

const uniqueSourceIds = Array.from(new Set(sourceRows.map((r) => r.sourceId)));

for (const sourceId of uniqueSourceIds) {
result.sourcePagesAttempted += 1;
try {
const { changed, rewrite } = await rewriteSourcePage(db, sourceId, trimmedOld, trimmedNew);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== pages schema / uniqueness ==\n'
rg -n -C4 'pgTable\("pages"|title:|unique|uniqueIndex|UNIQUE' server/api/src/schema/index.ts

printf '\n== rename propagation / title-only matching ==\n'
rg -n -C3 'rewriteTitleRefsInDoc|targetId|sourceId|oldTitle|newTitle' \
  server/api/src/services/titleRenamePropagationService.ts \
  server/api/src/services/ydocRenameRewrite.ts

printf '\n== page creation/update paths that may allow duplicate titles ==\n'
rg -n -C3 'insert\(pages\)|update\(pages\)|title:' server/api/src/routes/pages.ts

Repository: otomatty/zedi

Length of output: 90


🏁 Script executed:

#!/bin/bash
# First, check if the files exist
echo "== Checking file existence =="
find server -name "index.ts" -path "*/schema/*" 2>/dev/null | head -20
find server -name "titleRenamePropagationService.ts" 2>/dev/null
find server -name "pages.ts" -path "*/routes/*" 2>/dev/null

Repository: otomatty/zedi

Length of output: 243


🏁 Script executed:

#!/bin/bash
# Check schema file size and examine pages table definition
echo "== Examining schema/index.ts =="
wc -l server/api/src/schema/index.ts

# Look for pages table definition and uniqueness constraints
rg -n "pages\s*=" server/api/src/schema/index.ts | head -20

# Get context around pages table
rg -n -A 30 "pgTable\(" server/api/src/schema/index.ts | grep -A 30 "pages"

Repository: otomatty/zedi

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Read the entire schema file
echo "== Full schema/index.ts =="
cat -n server/api/src/schema/index.ts

Repository: otomatty/zedi

Length of output: 3797


🏁 Script executed:

#!/bin/bash
# Find and examine pages.js in the schema directory
echo "== Locating pages.js =="
find server -name "pages.js" -path "*/schema/*"

# Check size
wc -l server/api/src/schema/pages.js

# Show the pages table definition
echo "== Full pages.js =="
cat -n server/api/src/schema/pages.js

Repository: otomatty/zedi

Length of output: 244


🏁 Script executed:

#!/bin/bash
# List all files in schema directory
echo "== Files in schema directory =="
ls -la server/api/src/schema/

# Look for pages definitions
echo "== Searching for pages table definition =="
rg -l "pgTable\|export.*pages" server/api/src/schema/

Repository: otomatty/zedi

Length of output: 1404


🏁 Script executed:

#!/bin/bash
# Read pages.ts schema file
echo "== pages.ts schema =="
cat -n server/api/src/schema/pages.ts

Repository: otomatty/zedi

Length of output: 4992


🏁 Script executed:

#!/bin/bash
# Read the titleRenamePropagationService.ts file
echo "== titleRenamePropagationService.ts =="
wc -l server/api/src/services/titleRenamePropagationService.ts

# Read the relevant sections - around lines 261-271 and the rewriteSourcePage/rewriteTitleRefsInDoc functions
head -280 server/api/src/services/titleRenamePropagationService.ts | tail -50

Repository: otomatty/zedi

Length of output: 2049


🏁 Script executed:

#!/bin/bash
# Read the full titleRenamePropagationService.ts
cat -n server/api/src/services/titleRenamePropagationService.ts

Repository: otomatty/zedi

Length of output: 12714


🏁 Script executed:

#!/bin/bash
# Read ydocRenameRewrite.ts to understand rewriteTitleRefsInDoc
echo "== ydocRenameRewrite.ts =="
wc -l server/api/src/services/ydocRenameRewrite.ts

cat -n server/api/src/services/ydocRenameRewrite.ts

Repository: otomatty/zedi

Length of output: 10640


This title-only rewrite can silently retarget unrelated references when duplicate page titles exist.

The bug is real: pages.ts schema has no unique constraint on title, so duplicate titles are allowed. When propagateTitleRename() selects source pages via links.targetId (correct UUID-based identity), it then calls rewriteSourcePage()rewriteTitleRefsInDoc(), which receives only oldTitle/newTitle strings. rewriteTitleRefsInDoc() then rewrites every WikiLink and tag mark in that document whose normalized title matches the old value, with no identity check against the actual renamed page's ID.

Example: If a source page contains links to both "Page A (id: 123, title: 'Foo')" and "Page B (id: 456, title: 'Foo')", renaming Page A from 'Foo' to 'Bar' will rewrite all 'Foo' marks to 'Bar', including those that actually pointed to Page B.

Fix: Either enforce title uniqueness in the schema, or carry the target page ID through the rewrite to match both title and identity before updating marks.

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

In `@server/api/src/services/titleRenamePropagationService.ts` around lines 261 -
271, The rewrite currently only matches by title and can retarget links when
duplicate titles exist; update the flow to carry the renamed page's ID through
the rewrite so rewrites match both title and identity. Change the call site in
titleRenamePropagationService where you call rewriteSourcePage(db, sourceId,
trimmedOld, trimmedNew) to pass the renamedPageId as an additional argument,
update rewriteSourcePage(...) signature to accept targetId and forward it into
rewriteTitleRefsInDoc(...), and modify rewriteTitleRefsInDoc(oldTitle, newTitle,
...) to accept the targetId and only replace WikiLink/tag marks when the
normalized title matches AND the mark’s resolved target id (or explicit id in
the mark) equals the renamedPageId (or skip marks without identity);
alternatively, if you prefer enforcing uniqueness, add a schema unique
constraint on pages.title and migrate data -- pick one approach and implement
the corresponding signature and matching changes for rewriteSourcePage and
rewriteTitleRefsInDoc.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 6 additional findings.

Open in Devin Review

- gemini: use `node.toArray()` in the Y.Doc walker to replace the O(N^2)
  `node.get(i)` index loop with a single O(N) pass.
- coderabbit: `hocuspocusInvalidation` now logs a warn in every env when
  `HOCUSPOCUS_INTERNAL_URL` / `BETTER_AUTH_SECRET` is missing (previously
  silent in production), naming the missing variable(s) in the message.
- chatgpt-codex P1: `promoteGhostLinks` now scopes promotion by the
  renamed page's `pages.note_id` / `pages.owner_id`. Without this,
  renaming a page in one tenant could consume ghost rows on unrelated
  users' / notes' pages and create cross-scope `links` edges.
  Note-native targets match by `note_id`; personal targets match by
  `note_id IS NULL AND owner_id = ...`. A new test covers the guard.
- coderabbit: rewriting a source page's Y.Doc now re-derives the plain
  text via `extractTextFromYXml`, writes it into `page_contents.content_text`
  in the same transaction, and refreshes `pages.content_preview`. Search
  and listing metadata stay consistent with the rewritten document.
- Adds `server/api/src/lib/extractPlainTextFromYXml.ts` duplicating the
  Hocuspocus-side helper (server/api and server/hocuspocus are separate
  Bun projects per CLAUDE.md; snapshot utilities already follow this
  mirror-with-note pattern).

Tests updated to reflect the new query plans. Tracks #726 / PR #736.

https://claude.ai/code/session_01BMij57Zvw2nvhf7tdKwQ3f
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
server/api/src/services/titleRenamePropagationService.ts (1)

321-358: ⚠️ Potential issue | 🟠 Major

Title-only rewrite can still retarget unrelated references when duplicate titles exist.

The concern from the past review remains: the service queries links by targetId (UUID-based identity, line 326), but rewriteTitleRefsInDoc matches marks purely by normalized title string. If a source page contains WikiLinks to two different pages that happen to share the same title, renaming one will incorrectly rewrite all matching marks.

While duplicate titles within the same owner are uncommon, the schema allows them (pages.title has no unique constraint).

Mitigation options:

  1. Carry renamedPageId into the rewrite and match both title and mark identity (if marks store target page IDs).
  2. Add a unique constraint on (ownerId, title) or (noteId, title).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/src/services/titleRenamePropagationService.ts` around lines 321 -
358, The rewrite currently finds source pages by links.targetId but
rewriteSourcePage and its helper rewriteTitleRefsInDoc only match marks by
normalized title, causing unrelated same-title marks to be retargeted; fix by
passing the renamedPageId into rewriteSourcePage and down into
rewriteTitleRefsInDoc so the mark matching checks both normalized title and the
mark's stored target id (or mark identity field) before rewriting; update
rewriteSourcePage(signature) and rewriteTitleRefsInDoc(signature) to accept
renamedPageId and adjust their matching logic to require title match AND
mark.targetId === renamedPageId (or equivalent mark id field), leaving the
existing link-based source selection intact.
🧹 Nitpick comments (1)
server/api/src/services/ydocRenameRewrite.ts (1)

47-56: The character sets are already in sync, but consider extracting the shared character class to a dedicated module.

The character set [A-Za-z0-9_\-぀-ヿ㐀-鿿] is identical in both VALID_TAG_NAME_REGEX (server) and TAG_PASTE_REGEX (client), confirming the comment's sync intention is currently maintained. However, the two regexes serve different purposes: the client regex detects tags with # prefix during paste, while the server regex validates tag name strings after rename.

Rather than extracting the full regexes (which would be architecturally difficult), consider creating a shared constant for the character class alone in a location both server and client can access, reducing duplication and making the sync dependency explicit.

Note: The original comment references TAG_PASTE_REGEX in src/lib/tagUtils.ts, but it is actually defined in src/components/editor/extensions/TagExtension.ts.

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

In `@server/api/src/services/ydocRenameRewrite.ts` around lines 47 - 56, Extract
the shared character class string (currently "[A-Za-z0-9_\\-぀-ヿ㐀-鿿]") into a
small shared constant module that both server and client can import, update
VALID_TAG_NAME_REGEX in ydocRenameRewrite.ts to build its regex from that shared
character-class constant (e.g., new RegExp(`^${CHAR_CLASS}+$/`)), update
TAG_PASTE_REGEX in TagExtension.ts to use the same shared constant instead of
its own literal, and fix the comment in ydocRenameRewrite.ts to point to the
actual TagExtension.ts location rather than src/lib/tagUtils.ts so the sync
dependency is explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/api/src/services/titleRenamePropagationService.ts`:
- Around line 321-358: The rewrite currently finds source pages by
links.targetId but rewriteSourcePage and its helper rewriteTitleRefsInDoc only
match marks by normalized title, causing unrelated same-title marks to be
retargeted; fix by passing the renamedPageId into rewriteSourcePage and down
into rewriteTitleRefsInDoc so the mark matching checks both normalized title and
the mark's stored target id (or mark identity field) before rewriting; update
rewriteSourcePage(signature) and rewriteTitleRefsInDoc(signature) to accept
renamedPageId and adjust their matching logic to require title match AND
mark.targetId === renamedPageId (or equivalent mark id field), leaving the
existing link-based source selection intact.

---

Nitpick comments:
In `@server/api/src/services/ydocRenameRewrite.ts`:
- Around line 47-56: Extract the shared character class string (currently
"[A-Za-z0-9_\\-぀-ヿ㐀-鿿]") into a small shared constant module that both server
and client can import, update VALID_TAG_NAME_REGEX in ydocRenameRewrite.ts to
build its regex from that shared character-class constant (e.g., new
RegExp(`^${CHAR_CLASS}+$/`)), update TAG_PASTE_REGEX in TagExtension.ts to use
the same shared constant instead of its own literal, and fix the comment in
ydocRenameRewrite.ts to point to the actual TagExtension.ts location rather than
src/lib/tagUtils.ts so the sync dependency is explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6db650af-6678-4a1b-87f2-e52d2f44b3a7

📥 Commits

Reviewing files that changed from the base of the PR and between 11cc096 and d0a8610.

📒 Files selected for processing (5)
  • server/api/src/__tests__/services/titleRenamePropagationService.test.ts
  • server/api/src/lib/extractPlainTextFromYXml.ts
  • server/api/src/lib/hocuspocusInvalidation.ts
  • server/api/src/services/titleRenamePropagationService.ts
  • server/api/src/services/ydocRenameRewrite.ts
✅ Files skipped from review due to trivial changes (1)
  • server/api/src/tests/services/titleRenamePropagationService.test.ts

@otomatty otomatty self-assigned this Apr 24, 2026
@otomatty otomatty merged commit 9e1d66f into develop Apr 24, 2026
12 checks passed
@otomatty otomatty deleted the claude/fix-issue-726-UDzFs branch April 24, 2026 23:55
@coderabbitai coderabbitai Bot mentioned this pull request Apr 26, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants