Skip to content

2025.11.1#1441

Open
mattyatea wants to merge 1136 commits intomainfrom
next-2025.11.1
Open

2025.11.1#1441
mattyatea wants to merge 1136 commits intomainfrom
next-2025.11.1

Conversation

@mattyatea
Copy link
Member

@mattyatea mattyatea commented Mar 3, 2026

What

Why

Additional info (optional)

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

Summary by CodeRabbit

  • 新機能

    • ドラフト(作成/編集/削除)と予定投稿、チャンネルのミュート、ページ参照カウント、ダイレクトメッセージ改善、画像透かし・編集・エフェクトツール、QRコード対応、背景アニメーションコンポーネント、CLIコマンド群を追加。
  • 設定変更

    • Node.js 要件更新(22.21+/24+)、PostgreSQL 18 への移行、プロキシ/プレビュー関連の設定整理と新しい信頼プロキシオプション。
  • 多言語対応

    • 多数のローカライズ拡張と翻訳追加。
  • CI/運用

    • ワークフローと開発コンテナの更新、テストランナー・スクリプト改良。

syuilo and others added 30 commits November 9, 2025 19:43
* enhance(frontend): アンテナ・リストの設定画面からタイムラインへの動線を追加

* Update Changelog

* fix
* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Kabyle)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)
…16784)

* fix: 並列deleteによるpostgresqlのdeadlock

* chore: update comment
* chore: channelのdescriptionを空欄にできるようにする

* update: CHANGELOG.md

* update: CHANGELOG.md

* fix: CHANGELOG.md

* fix: CHANGELOG.md
* fix(frontend): 投稿フォームのアバター画像が縮むのを修正

* fix CHANGELOG.md

* fix
* fix(frontend): カスタム絵文字(β)画面で変更行が正しくハイライトされない問題を修正

* rollback

* 詳細度->!important
* fix(ci): DockleのCIが落ちるのを修正

* fix

* fix

* fix

* fix

* fix

* downgrade dockle

* fix
* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Italian)
…sskey-dev#16793)

* fix(devcontainer): devcontainerのバージョンアップ+Renovateでbump出来るように設定を追加

* Update renovate.json5

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update renovate.json5

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix matchFileNames

* using trixie

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* enhance(frontend): リアクションの受け入れ設定にキャプションを追加

* Update Changelog

* CHANGELOG.mdを修正

* CHANGELOG.mdのコンフリクトを解消し、再度変更内容を記載

* 条件分岐をswitch文に変更

* chore: trigger CI re-run
* fix(frontend): ページのコンテンツがはみ出る問題を修正

* Update Changelog
* fix: ヘッダーメニューのチャンネルからチャンネルを新規作成の遷移先修正

* add changelog

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
@mattyatea
Copy link
Member Author

@coderabbitai fullreview

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

@mattyatea Sure! I'll perform a full review of the PR now.


🧠 Learnings used
Learnt from: u1-liquid
Repo: MisskeyIO/misskey PR: 1081
File: package.json:50-71
Timestamp: 2025-10-31T07:51:27.692Z
Learning: In MisskeyIO/misskey's package.json resolutions section, the GitHub references for storybook-addon-misskey-theme and webgl-audiovisualizer intentionally do not include version tags, unlike other resolution entries.
✅ Actions performed

Full review triggered.

Copy link

@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: 6

Note

Due to the large number of review comments, Critical 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 (4)
packages/backend/src/core/MfmService.ts (1)

105-112: ⚠️ Potential issue | 🟠 Major

URLパースで例外が発生する可能性があります

new URL(href)は相対URLや不正なURLの場合にTypeErrorをスローします。toHtmlメソッド内では同様のURL処理がtry/catchで囲まれていますが(lines 375-380, 389-394, 417-422)、ここでは例外処理がありません。

相対パス(例: /user/foo)を含むHTMLを処理する際に、fromHtml全体が失敗する可能性があります。

🔧 例外処理を追加する修正案
 						if (part.length === 2 && href) {
 							//#region ホスト名部分が省略されているので復元する
-							const acct = `${txt}@${(new URL(href)).hostname}`;
-							text += acct;
+							try {
+								const acct = `${txt}@${(new URL(href)).hostname}`;
+								text += acct;
+							} catch {
+								text += txt;
+							}
 							//#endregion
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/MfmService.ts` around lines 105 - 112, In toHtml
(packages/backend/src/core/MfmService.ts) the expression new URL(href) can throw
for relative or invalid URLs; wrap the hostname-reconstruction branch (the if
block where part.length === 2 and href is used to build acct and append to text)
in a try/catch and fall back to a safe behavior (e.g., use a provided base URL
from the parser/context if available, or skip adding hostname and just append
txt) so that malformed/relative href values do not bubble up and break fromHtml;
ensure the catch logs or silently handles the error and preserves existing flow
for part.length === 3.
packages/backend/src/core/FetchInstanceMetadataService.ts (1)

249-312: ⚠️ Potential issue | 🟡 Minor

getDescriptionメソッドにコピペエラーがあります。

getThemeColorgetSiteNamegetDescriptionメソッドで使用されているquerySelector()getAttribute()はnode-html-parserで正式にサポートされており、型シグネチャも正しいです。

ただし、getDescriptionメソッドのmanifestフォールバック部分に問題があります。現在のコードは以下のようになっています:

if (manifest) {
	return manifest.name ?? manifest.short_name;
}

これはgetSiteNameからのコピペエラーです。getDescriptionではmanifest.descriptionを返すべきです:

if (manifest) {
	return manifest.description ?? manifest.short_name;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/FetchInstanceMetadataService.ts` around lines 249 -
312, The getDescription method has a copy-paste bug: its manifest fallback
returns manifest.name (from getSiteName) instead of the description; in the
getDescription function update the manifest branch to return
manifest.description ?? manifest.short_name (replace the existing manifest.name
?? manifest.short_name) so the description fallback uses the correct manifest
field.
.github/workflows/lint.yml (1)

80-91: ⚠️ Potential issue | 🟠 Major

lintジョブのアクションバージョンが他のジョブと不整合です

pnpm_installおよびtypecheckジョブではactions/checkout@v4.3.0pnpm/action-setup@v4.2.0actions/setup-node@v4.4.0を使用していますが、lintジョブでは@v6を指定しています。actions/checkoutactions/setup-nodeの現行メジャーバージョンはv4であり、v6は存在しない可能性があります。

,

修正案
     steps:
-    - uses: actions/checkout@v6
+    - uses: actions/checkout@v4.3.0
       with:
         fetch-depth: 0
         submodules: true
     - name: Setup pnpm
-      uses: pnpm/action-setup@v4
+      uses: pnpm/action-setup@v4.2.0
       with:
         run_install: false
-    - uses: actions/setup-node@v6
+    - uses: actions/setup-node@v4.4.0
       with:
         node-version-file: '.node-version'
         cache: 'pnpm'
What is the latest version of actions/checkout GitHub Action?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint.yml around lines 80 - 91, The lint job uses
inconsistent and likely invalid action versions (`@v6`) for actions/checkout,
pnpm/action-setup, and actions/setup-node; update the lint job to use the same
stable versions as other jobs (actions/checkout@v4.3.0,
pnpm/action-setup@v4.2.0, actions/setup-node@v4.4.0) and confirm the
actions/checkout version is the repository's desired latest release before
committing; adjust the three action references in the lint job block
accordingly.
packages/backend/src/core/FanoutTimelineEndpointService.ts (1)

120-145: ⚠️ Potential issue | 🟠 Major

新しいフィルタが DB fallback 経路に適用されていません。

ミュート済みチャンネルと停止済みユーザーの除外は Redis 側の filter にしか入っていませんが、足りない分は Line 210 の dbFallback() 結果をそのまま返しています。Redis ミスや部分読み込み時に、せっかく追加した除外条件をすり抜けたノートが混ざるので、この経路にも同じ条件を適用するか、dbFallback 契約側に取り込む必要があります。

Also applies to: 199-211

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

In `@packages/backend/src/core/FanoutTimelineEndpointService.ts` around lines 120
- 145, The new mute/block/channel filters applied to the in-memory Redis path
(built into the local variable filter in FanoutTimelineEndpointService) are not
being applied to the dbFallback() path; update the code so that when ps.me is
present you either (A) pass the same filter into the dbFallback call (extend
dbFallback's signature to accept a predicate) or (B) post-filter the
dbFallback() results with the same predicate before returning them; use the same
checks (isUserRelated with userIdsWhoBlockingMe/userIdsWhoMeMuting and
ps.ignoreAuthorFromBlock/ps.ignoreAuthorFromMute, renote checks including
userIdsWhoMeMutingRenotes, isInstanceMuted with userMutedInstances, and
isChannelRelated with userMutedChannels/ps.ignoreAuthorChannelFromMute) so both
paths enforce identical exclusions.
♻️ Duplicate comments (6)
idea/MkAnimatedBg.dotted-ripples.vue (1)

192-195: ⚠️ Potential issue | 🟠 Major

既出ですが、uniform location の探索がまだレンダーループ内に残っています。

getUniformLocation() を毎フレーム 32 回呼ぶホットパスのままです。初期化時に配列へキャッシュして、ここでは uniform2f / uniform1f だけを呼ぶ形にしてください。

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

In `@idea/MkAnimatedBg.dotted-ripples.vue` around lines 192 - 195, The render loop
is still calling gl.getUniformLocation() for each ripple every frame (hot path);
move the location lookups into initialization and cache them (an array of
WebGLUniformLocation for u_ripplePositions and u_rippleRadiuses keyed by index)
so that inside the loop you only call gl.uniform2f/uniform1f with the cached
locations and ripple values (use the existing shaderProgram, ripples[], and the
names u_ripplePositions/u_rippleRadiuses to locate where to change).
packages/backend/migration/1736686850345-createNoteDraft.js (2)

57-75: ⚠️ Potential issue | 🔴 Critical

ON DELETE CASCADEがドラフトの意図しない削除を引き起こします(既出)

replyIdrenoteIdchannelIdの外部キーにON DELETE CASCADEが設定されていますが、NoteDraft.tsエンティティではcreateForeignKeyConstraints: falseと「参照先が削除されてもnullになる可能性がある」というコメントがあり、orphaned referenceを許容する設計意図が示されています。ON DELETE SET NULLに変更すべきです。

,

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

In `@packages/backend/migration/1736686850345-createNoteDraft.js` around lines 57
- 75, Change the three foreign-key constraints for replyId, renoteId, and
channelId to use ON DELETE SET NULL instead of ON DELETE CASCADE to match the
NoteDraft entity's createForeignKeyConstraints: false/orphanable design;
specifically update the SQL in the migration queries that add constraints
"FK_NOTE_DRAFT_REPLY_ID", "FK_NOTE_DRAFT_RENOTE_ID", and
"FK_NOTE_DRAFT_CHANNEL_ID" to reference "note"("id") or "channel"("id") with ON
DELETE SET NULL so deleted parents become NULL rather than removing drafts.

18-27: ⚠️ Potential issue | 🔴 Critical

NOT NULL制約が不足しています(既出)

localOnlyfileIdsvisibleUserIdshasPollpollChoicesカラムにNOT NULL制約がありません。TypeORMエンティティ定義では非nullが前提となっており、整合性を保つために制約を追加すべきです。

,

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

In `@packages/backend/migration/1736686850345-createNoteDraft.js` around lines 18
- 27, In the CreateNoteDraft migration add NOT NULL constraints for the columns
listed: change "localOnly" to boolean NOT NULL DEFAULT false, "fileIds" and
"visibleUserIds" to varchar[] NOT NULL DEFAULT '{}', "hasPoll" to boolean NOT
NULL DEFAULT false, and "pollChoices" to varchar(256)[] NOT NULL DEFAULT '{}';
update the SQL column definitions in the migration (the CREATE TABLE / column
definitions containing "localOnly", "fileIds", "visibleUserIds", "hasPoll",
"pollChoices") so they match the non-null expectations from the TypeORM entity.
packages/backend/jest.js (1)

25-30: ⚠️ Potential issue | 🔴 Critical

シグナル終了時の終了コード計算が不正です(既出)

Node.jsの'exit'イベントで渡されるsignalは文字列(例:'SIGTERM')であり、128 + signal"128SIGTERM"という不正な値になります。

,

修正案
+import os from 'node:os';
+
 child.on('exit', (code, signal) => {
 	if (code === null) {
-		process.exit(128 + signal);
+		const signalCode = signal ? os.constants.signals[signal] : undefined;
+		process.exit(signalCode != null ? 128 + signalCode : 1);
 	} else {
 		process.exit(code);
 	}
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/jest.js` around lines 25 - 30, The exit handler for
child.on('exit', (code, signal) => ...) treats signal as a number and does
process.exit(128 + signal), but signal is a string (e.g. "SIGTERM"); update the
handler to detect when code is null and signal is non-null and compute a numeric
exit code by resolving the signal name to its numeric value (e.g. use
os.constants.signals[signal] or similar lookup) then call process.exit(128 +
numericSignal); keep the existing behavior of process.exit(code) when code is a
number and handle missing/unknown signal names with a sensible fallback (e.g.
just exit(128) or exit(1)).
.github/workflows/test-backend.yml (1)

26-32: ⚠️ Potential issue | 🟡 Minor

force_ffmpeg_cache_update がまだワークフロー挙動に反映されていません。

input を追加していますが、FFmpeg の restore/save ステップの条件にも key にも参照がないので、手動実行で true にしてもキャッシュ更新を強制できません。restore をスキップするだけでは既存 key を上書きできないため、save 側の key も input で分岐させる必要があります。

Also applies to: 73-90

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

In @.github/workflows/test-backend.yml around lines 26 - 32, The workflow input
force_ffmpeg_cache_update is not used in the FFmpeg cache restore/save steps;
update the FFmpeg restore and save steps so their if conditions reference
github.event.inputs.force_ffmpeg_cache_update (skip restore when true) and
modify the cache key generation (e.g., append a conditional suffix or use a
different key when force_ffmpeg_cache_update is true) so a manual true run
produces a different key and forces cache save; apply the same changes to both
the restore step and the save step referenced in the FFmpeg-related steps.
packages/backend/migration/1752502434151-no-action-on-draft-relation.js (1)

9-15: ⚠️ Potential issue | 🔴 Critical

replyId / renoteId / channelId の FK が up() で復元されていません。

up() は 4 本の制約を落として userId だけ張り直しているので、適用後は replyId / renoteId / channelId が参照整合性なしになります。down() で 4 本とも復元していることから見ても、ここは意図した最終状態とずれています。

🛠️ 修正例
 	async up(queryRunner) {
 			await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID"`);
 			await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_USER_ID"`);
 			await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID"`);
 			await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID"`);
 			await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_e4983f28b4b18b03491536052f5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+			await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+			await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+			await queryRunner.query(`ALTER TABLE "note_draft" ADD CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 	}

Also applies to: 17-22

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

In `@packages/backend/migration/1752502434151-no-action-on-draft-relation.js`
around lines 9 - 15, The up() migration only re-adds the userId FK but fails to
restore the three other foreign keys dropped earlier (FK_NOTE_DRAFT_CHANNEL_ID,
FK_NOTE_DRAFT_RENOTE_ID, FK_NOTE_DRAFT_REPLY_ID), leaving
replyId/renoteId/channelId without referential integrity; update the up()
implementation to add back all four constraints (same constraint identifiers:
FK_NOTE_DRAFT_CHANNEL_ID, FK_NOTE_DRAFT_RENOTE_ID, FK_NOTE_DRAFT_REPLY_ID, and
the existing FK_e4983f28b4b18b03491536052f5 for userId) using
queryRunner.query(...) so each FOREIGN KEY ("channelId"), FOREIGN KEY
("renoteId"), and FOREIGN KEY ("replyId") reference their correct target tables
with the appropriate ON DELETE/ON UPDATE actions matching the down()
definitions.
🟠 Major comments (30)
packages/backend/migration/1753868431598-remove_note_constraints.js-15-16 (1)

15-16: ⚠️ Potential issue | 🟠 Major

ロールバック時に孤立参照があると down() が失敗します。

up() 実行後は replyId / renoteId の孤立データを作れてしまうので、Line 15 と Line 16 の ADD CONSTRAINT はロールバック時にその場で失敗します。down() 側で先に不正参照を NULL に戻すか削除してから制約を張り直したほうが安全です。

修正案
 async down(queryRunner) {
+        await queryRunner.query(`
+            UPDATE "note" AS n
+            SET "replyId" = NULL
+            WHERE "replyId" IS NOT NULL
+              AND NOT EXISTS (
+                  SELECT 1 FROM "note" AS parent WHERE parent."id" = n."replyId"
+              )
+        `);
+        await queryRunner.query(`
+            UPDATE "note" AS n
+            SET "renoteId" = NULL
+            WHERE "renoteId" IS NOT NULL
+              AND NOT EXISTS (
+                  SELECT 1 FROM "note" AS parent WHERE parent."id" = n."renoteId"
+              )
+        `);
         await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
         await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1753868431598-remove_note_constraints.js` around
lines 15 - 16, down() currently re-adds the foreign keys
FK_17cb3553c700a4985dff5a30ff5 and FK_52ccc804d7c69037d558bac4c96 for columns
replyId and renoteId without first handling orphaned rows, which causes failures
on rollback; update the migration's down() to first null out or delete rows
where replyId or renoteId reference missing note ids (e.g., run a DELETE or
UPDATE setting replyId/renoteId = NULL for any rows whose referenced note id
does not exist), then re-add the ALTER TABLE ... ADD CONSTRAINT for those same
constraint names so the constraint creation cannot fail due to existing orphaned
references.
locales/ko-GS.yml-654-656 (1)

654-656: ⚠️ Potential issue | 🟠 Major

新規セクションの翻訳が不完全です。基準ロケールとの差分を埋めてください。

以下のキーが ko-GS.yml から漏れており、ユーザーには基準言語がフォールバック表示されます。

  • _imageEditing._vars: 17 個のキー不足(camera_model、camera_f、camera_iso、year、month、day、hour、minute、second、caption、filename_without_ext、gps_lat、gps_long、camera_mm、camera_mm_35、camera_lens_model、camera_s)
  • _watermarkEditor: 29 個のキー不足(title、text、angle、scale、opacity、position、type、stripe、stripeWidth、stripeFrequency、polkadot、polkadotMainDotRadius、polkadotMainDotOpacity、polkadotSubDotRadius、polkadotSubDotOpacity、polkadotSubDotDivisions、repeat、margin、qr、checker、cover、preserveBoundingRect、leaveBlankToAccountUrl、advanced、driveFileTypeWarn、driveFileTypeWarnDescription、failedToLoadImage、quitWithoutSaveConfirm、tip)
  • _qr: 13 個のキー不足(startQr、stopQr、readTabTitle、chooseCamera、mfm、raw、scanFile、noQrCodeFound、cannotToggleFlash、turnOnFlash、turnOffFlash、shareTitle、shareText)

すべてのキーを翻訳して、各セクションが完全になるように修正してください。

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

In `@locales/ko-GS.yml` around lines 654 - 656, The ko-GS.yml translations are
incomplete for the new sections; add the missing keys under _imageEditing._vars
(camera_model, camera_f, camera_iso, year, month, day, hour, minute, second,
caption, filename_without_ext, gps_lat, gps_long, camera_mm, camera_mm_35,
camera_lens_model, camera_s), the entire _watermarkEditor block (title, text,
angle, scale, opacity, position, type, stripe, stripeWidth, stripeFrequency,
polkadot, polkadotMainDotRadius, polkadotMainDotOpacity, polkadotSubDotRadius,
polkadotSubDotOpacity, polkadotSubDotDivisions, repeat, margin, qr, checker,
cover, preserveBoundingRect, leaveBlankToAccountUrl, advanced,
driveFileTypeWarn, driveFileTypeWarnDescription, failedToLoadImage,
quitWithoutSaveConfirm, tip) and the _qr block (startQr, stopQr, readTabTitle,
chooseCamera, mfm, raw, scanFile, noQrCodeFound, cannotToggleFlash, turnOnFlash,
turnOffFlash, shareTitle, shareText) with Korean translations matching the base
locale keys and structure so no fallback occurs; locate and update the
_imageEditing._vars, _watermarkEditor, and _qr sections in ko-GS.yml to mirror
the base locale keys and provide appropriate Korean strings for each missing
key.
packages/backend/src/core/DriveService.ts-548-565 (1)

548-565: ⚠️ Potential issue | 🟠 Major

リモート経由だと容量制限をすり抜けます。

ここは maxFileSize 超過をローカルユーザーにしか拒否していないので、リモートユーザーは上限超過ファイルをそのまま保存できます。さらに単体で driveCapacity を超えるファイルでも expireOldFile(driveCapacity - info.size) に進めてしまうため、既存ファイルを消しても収まらないケースを防げません。加えて expireOldFile() 側は各 deleteFile() を await していないので、この await でも空き容量確保は保証されません。少なくとも単体サイズ超過は即 reject し、容量超過時は削除完了を待てる形にしないと policy enforcement が崩れます。

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

In `@packages/backend/src/core/DriveService.ts` around lines 548 - 565, The code
currently only rejects oversized uploads for local users and calls expireOldFile
incorrectly and without awaiting deletes; fix by (1) checking if info.size >
maxFileSize OR info.size > driveCapacity and throwing the same IdentifiableError
for all users (use the same or an appropriate error id) so remote users cannot
bypass maxFileSize or single-file capacity; (2) compute requiredFree = (usage +
info.size) - driveCapacity and, if requiredFree > 0, call await
this.expireOldFile(userRecord, requiredFree) where userRecord comes from await
this.usersRepository.findOneByOrFail({ id: user.id }); and (3) update
expireOldFile/deleteFile so it awaits each delete (ensure expireOldFile returns
a Promise that resolves after all deleteFile awaits complete) so the await here
guarantees space was freed before proceeding. Include references to maxFileSize,
isLocalUser, driveCapacity, driveFileEntityService.calcDriveUsageOf,
expireOldFile, usersRepository.findOneByOrFail, and deleteFile when making
changes.
locales/id-ID.yml-403-403 (1)

403-403: ⚠️ Potential issue | 🟠 Major

enableMcaptcha が空文字です

Line 403 が空だと設定トグルのラベルが空表示になり、何を有効化する項目なのか分からなくなります。設定画面の操作性を直接壊すので、ここは埋めるべきです。

修正案
-enableMcaptcha: ""
+enableMcaptcha: "Nyalakan mCaptcha"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/id-ID.yml` at line 403, The locale key enableMcaptcha currently has
an empty value; replace it with a meaningful Indonesian label (for example
"Aktifkan mCaptcha" or similar) so the settings toggle displays correctly;
update the string for the enableMcaptcha key in locales/id-ID.yml to a concise,
user-facing phrase in Indonesian.
packages/backend/src/core/FileInfoService.ts-281-288 (1)

281-288: ⚠️ Potential issue | 🟠 Major

sharp.resize(299, 299) のデフォルト動作で画像が切り抜かれます。

Line 282 は fit オプションが未指定のため、Sharp のデフォルト値 cover が適用されます。fit: "cover" では指定された両方の寸法に対して、アスペクト比を保ちながら画像をクロップします。加えて withoutEnlargement: false により小さい画像も拡大されるため、元画像から大きく乖離した入力が AI 判定に渡される可能性があります。アスペクト比を保ちながら全体を収めるリサイズに変更することを推奨します。

修正案
 			const png = await (await sharpBmp(source, mime))
 				.resize(299, 299, {
-					withoutEnlargement: false,
+					fit: 'inside',
+					withoutEnlargement: true,
 				})
 				.rotate()
 				.flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
 				.png()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/FileInfoService.ts` around lines 281 - 288, The
resize call in FileInfoService.ts uses sharp (via sharpBmp) without a fit option
so Sharp defaults to fit: "cover" and crops the image; also withoutEnlargement:
false allows upscaling. Update the pipeline that produces the png buffer (the
chain starting at sharpBmp(source, mime) and assigned to png) to specify fit:
"contain" (so the whole image is preserved with aspect ratio) and set
withoutEnlargement: true (to avoid upscaling small images); keep the existing
rotate() and flatten() behavior so output remains the same otherwise.
packages/backend/migration/1752509043847-migration-cleanup.js-24-25 (1)

24-25: ⚠️ Potential issue | 🟠 Major

SET NOT NULL の前に既存 NULL を埋めてください。

既存行に NULL が 1 件でもあると、この migration はここで停止します。Line 23 のコメントどおり flash.visibility は過去 migration で制約漏れがあり、note_draft 側も hand-written SQL の補正なので、SET NOT NULL の前に UPDATE ... WHERE ... IS NULL で安全な値へ backfill して self-contained にしておいた方が安全です。

Also applies to: 40-45

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

In `@packages/backend/migration/1752509043847-migration-cleanup.js` around lines
24 - 25, Before the ALTER TABLE that sets flash.visibility NOT NULL, add a
backfill step that updates any existing NULLs to a safe default so the migration
won't fail; specifically, before queryRunner.query(`ALTER TABLE "flash" ALTER
COLUMN "visibility" SET NOT NULL`), run an UPDATE on the "flash" table (e.g.,
via queryRunner.query with an UPDATE ... WHERE "visibility" IS NULL) to set a
valid non-null value, and do the same for the analogous hand-written SQL block
that touches note_draft (lines around the second ALTER in the file) so both
flash.visibility and the note_draft column are backfilled prior to their
respective ALTER COLUMN ... SET NOT NULL statements.
packages/backend/migration/1752509043847-migration-cleanup.js-55-56 (1)

55-56: ⚠️ Potential issue | 🟠 Major

Line 56: userId コメントの rollback 値が誤っています。

元の migration では userId にコメント定義がないため、down() では IS NULL に戻す必要があります。他の 3 列(replyIdrenoteIdchannelId)は正しく IS NULL に設定されていますが、Line 56 の userId だけが 'The ID of author.' のままで完全な rollback になっていません。

Line 56 を以下に修正してください:

await queryRunner.query(`COMMENT ON COLUMN "note_draft"."userId" IS NULL`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1752509043847-migration-cleanup.js` around lines
55 - 56, The rollback for the "userId" column is incorrect: update the down()
migration so the COMMENT ON COLUMN "note_draft"."userId" is reverted to IS NULL
instead of keeping 'The ID of author.'; locate the queryRunner.query call that
handles "note_draft"."userId" in the migration (the line that currently sets the
comment to 'The ID of author.') and change it to set the comment to NULL to
match the original state and the other columns (replyId, renoteId, channelId).
locales/kn-IN.yml-47-48 (1)

47-48: ⚠️ Potential issue | 🟠 Major

Kannadaの「Mention」は名詞形の訳語が必要

現在の ಹೆಸರಿಸಿದ は過去分詞形で、UI ラベルとして使用する名詞としては不自然です。メンション一覧・通知種別・テーマ設定の複数箇所で影響するため、より自然な名詞形の ಉಲ್ಲೇಖ (単数) / ಉಲ್ಲೇಖಗಳು (複数) への変更を推奨します。

適用行: 47-48, 69-71, 87

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

In `@locales/kn-IN.yml` around lines 47 - 48, The current Kannada translations for
the UI keys mention and mentions use the past-participial form "ಹೆಸರಿಸಿದ";
replace the values for the keys mention -> "ಉಲ್ಲೇಖ" and mentions -> "ಉಲ್ಲೇಖಗಳು"
to use the correct noun forms, and update any other occurrences of these keys in
the same locale file (the other mention/mentions entries present) so all UI
labels (mention list, notification types, theme settings) consistently use
ಉಲ್ಲೇಖ / ಉಲ್ಲೇಖಗಳು.
packages/backend/scripts/dev.mjs-52-61 (1)

52-61: ⚠️ Potential issue | 🟠 Major

非同期ハンドラを直列化しないと再起動フローが競合します。

on('message', async ...) は前のハンドラの完了を待たないため、ファイル保存連打で複数の killProc() -> execBuildAssets() -> execStart() シーケンスが同時実行されます。これはプロセス状態を破壊する可能性があります。また、execBuildAssets() の失敗は未処理のままになります。

修正例
+let restartChain = Promise.resolve();
+
 (async () => {
 	execaNode(
 		'./node_modules/nodemon/bin/nodemon.js',
 		[
 			'-w', 'src',
 			'-e', 'ts,js,mjs,cjs,json,pug',
 			'--exec', 'pnpm', 'run', 'build',
 		],
 		{
 			stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
 			serialization: "json",
 		})
-		.on('message', async (message) => {
-			if (message.type === 'exit') {
-				// かならずbuild->build-assetsの順番で呼び出したいので、
-				// 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
-				// pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
-
-				await killProc();
-				await execBuildAssets();
-				execStart();
-			}
-		});
+		.on('message', (message) => {
+			if (message.type !== 'exit') return;
+			restartChain = restartChain
+				.catch(() => {})
+				.then(async () => {
+					// かならずbuild->build-assetsの順番で呼び出したいので、
+					// 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
+					// pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
+					await killProc();
+					await execBuildAssets();
+					execStart();
+				})
+				.catch((error) => {
+					console.error(error);
+				});
+		});
 })();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/scripts/dev.mjs` around lines 52 - 61, The message handler
currently spawns concurrent restart sequences because on('message', async
(message) => { ... }) doesn't serialize concurrent invocations; to fix,
serialize the restart flow by adding a simple in-process lock/queue around the
on('message') handler: when message.type === 'exit' check/acquire a boolean or
Promise-based lock before calling killProc(), await killProc(), await
execBuildAssets() inside a try/catch that logs and handles errors, then call
execStart() only after successful build-assets and finally release the lock;
reference symbols: the on('message'...) handler, killProc, execBuildAssets,
execStart.
packages/backend/scripts/dev.mjs-31-36 (1)

31-36: ⚠️ Potential issue | 🟠 Major

バックエンドプロセスの既終了時に killProc() が永遠にハングします。

Line 35 で kill() の後に exit リスナーを追加していますが、プロセスが既に終了済みの場合、exit イベントは再発火しません。Node.js の EventEmitter では、イベント発火後にリスナーを追加しても、そのイベントは再生されないためです。バックエンドが一度クラッシュすると、次の再起動シーケンスがここで永遠に停止します。

修正例
 async function killProc() {
 	if (backendProcess) {
-		backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
-		backendProcess.kill();
-		await new Promise(resolve => backendProcess.on('exit', resolve));
-		backendProcess = undefined;
+		const proc = backendProcess;
+		backendProcess = undefined;
+		proc.catch(() => {}); // kill に伴う reject を握りつぶす
+		if (proc.exitCode === null && proc.signalCode === null) {
+			const exited = new Promise(resolve => proc.once('exit', resolve));
+			proc.kill();
+			await exited;
+		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/scripts/dev.mjs` around lines 31 - 36, killProc currently
calls backendProcess.kill() then awaits backendProcess.on('exit', ...) which
hangs if the process already exited; fix by ensuring you either attach the exit
listener before calling kill or short-circuit when the child has already exited
(check backendProcess.exitCode !== null or backendProcess.killed) so the Promise
resolves immediately; update the killProc implementation (references: function
killProc, backendProcess.kill(), backendProcess.on('exit', ...)) to attach the
listener with once('exit') before calling kill or return immediately when
exitCode is non-null.
packages/backend/scripts/dev.mjs-19-29 (1)

19-29: ⚠️ Potential issue | 🟠 Major

バックエンド起動失敗時の promise rejection がハンドリングされていません。

execaNode() が返す Promise は起動失敗時(ファイルが見つからない、権限エラーなど)に reject されます。しかし execStart() ではこの reject を処理していないため、起動失敗時に unhandled rejection が発生し、Node.js プロセスがクラッシュします。Line 33 の .catch()kill() 時の例外処理のみで、startup 失敗には対応していません。

修正例
 function execStart() {
 	// pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
 	// 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
-	backendProcess = execaNode('./built/boot/entry.js', [], {
+	const proc = execaNode('./built/boot/entry.js', [], {
 		stdout: process.stdout,
 		stderr: process.stderr,
 		env: {
 			'NODE_ENV': 'development',
 		},
 	});
+	backendProcess = proc;
+	void proc.catch((error) => {
+		if (proc.killed) return;
+		console.error(error);
+	});
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/scripts/dev.mjs` around lines 19 - 29, The execStart
function currently assigns backendProcess = execaNode(...) but does not handle
rejections from execaNode; update execStart to handle startup failures by
attaching a rejection handler (or awaiting in an async function) to the
execaNode() Promise returned by backendProcess, log the error (including
error.message) and perform graceful shutdown/exit (e.g. call kill() or
process.exit(1)) so unhandled promise rejections don't crash the process;
reference execStart, execaNode, backendProcess and existing kill() logic when
adding the .catch or try/catch around the spawn.
packages/backend/src/core/AiService.ts-74-75 (1)

74-75: ⚠️ Potential issue | 🟠 Major

decodeImage() の直接呼び出しで GIF などの形式が壊れる可能性があります。

Line 75 の tf.node.decodeImage(buffer, 3)expandAnimations パラメータを省略しており、これはデフォルト値の true に設定されます。その結果、GIF ファイルは 4D テンソル [frames, height, width, 3] を返しますが、model.classify() が期待する入力は 3D テンソル [height, width, 3] です。この不一致により、GIF ファイルをこの経路で処理する場合、実行時エラーが発生します。

一方、mime が指定される経路(line 64-72)では sharp.js により適切に前処理され、常に 3D テンソルが返されます。as any による型警告の抑制がこの潜在的な失敗を隠しています。

GIF を含む全形式で一貫性を持たせるため、expandAnimations を明示的に false に設定し、アニメーション GIF を最初のフレームに固定する必要があります。

修正案
-					return await tf.node.decodeImage(buffer, 3) as any;
+					return await tf.node.decodeImage(buffer, 3, 'int32', false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/AiService.ts` around lines 74 - 75, The
tf.node.decodeImage call currently omits the expandAnimations flag which leaves
GIFs expanded into 4D tensors and breaks model.classify; update the call to
tf.node.decodeImage(buffer, 3, /*expandAnimations=*/false) so animated images
are fixed to the first frame and produce a 3D tensor, remove or tighten the "as
any" suppression if possible, and ensure the same preprocessing behavior as the
mime-handled path so model.classify receives a consistent [height,width,3]
tensor.
packages/backend/migration/1755168347001-PageCountInNote.js-10-10 (1)

10-10: ⚠️ Potential issue | 🟠 Major

pageCountsmallint ではなく integer が安全です。

smallint-32768~+32767 の範囲に制限され、この範囲外の値を代入すると PostgreSQL はエラーを発生させます。pageCount に厳密な上限がないなら、参照元ページ数が多いノートで将来の更新や再集計が失敗し得ます。

修正例
-        await queryRunner.query(`ALTER TABLE "note" ADD "pageCount" smallint NOT NULL DEFAULT '0'`);
+        await queryRunner.query(`ALTER TABLE "note" ADD "pageCount" integer NOT NULL DEFAULT 0`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1755168347001-PageCountInNote.js` at line 10, The
migration uses smallint for the new column in the queryRunner.query call (ALTER
TABLE "note" ADD "pageCount" smallint NOT NULL DEFAULT '0'), which risks
overflow; change the column type to integer and set the default to 0 (e.g.,
ALTER TABLE "note" ADD "pageCount" integer NOT NULL DEFAULT 0) so future large
page counts won't error — update the migration SQL inside the
1755168347001-PageCountInNote.js file where queryRunner.query is invoked to use
integer instead of smallint and adjust the default literal accordingly.
idea/MkAnimatedBg.dotted.vue-142-150 (1)

142-150: ⚠️ Potential issue | 🟠 Major

Chromatic 用の 1 フレーム描画が非決定になっています。

u_seed を分岐前で乱数初期化しているので、isChromatic() 側でも毎回違うパターンになります。これだとスナップショットが安定しません。

修正案
-	gl.uniform1f(u_seed, Math.random() * 1000);
+	const chromatic = isChromatic();
+	gl.uniform1f(u_seed, chromatic ? 0.0 : Math.random() * 1000);
 	gl.uniform1f(u_scale, props.scale);
 	gl.uniform1f(u_angle, 0.0);
 	gl.uniform1f(u_radius, 0.15);
 	gl.uniform3fv(u_color, [0.5, 1.0, 0]);
 
-	if (isChromatic()) {
+	if (chromatic) {
 		gl.uniform1f(u_time, 0);
 		gl.drawArrays(gl.TRIANGLES, 0, 6);
 	} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idea/MkAnimatedBg.dotted.vue` around lines 142 - 150, The chromatic branch is
non-deterministic because u_seed is set to Math.random() before the
isChromatic() check; change the code so that when isChromatic() is true you pass
a deterministic seed (e.g., 0 or a fixed value) to gl.uniform1f(u_seed, ...)
right before gl.drawArrays, and only use Math.random() for u_seed in the
non-chromatic path; keep u_time, gl.drawArrays, and the isChromatic() check
intact but ensure the seed assignment is moved or conditional so chromatic
snapshots are stable.
idea/MkAnimatedBg.dotted-ripples.vue-162-164 (1)

162-164: ⚠️ Potential issue | 🟠 Major

Chromatic では透明な 1 フレームしか出ません。

この分岐では u_rippleRadiuses を一度も正の値にしていないので、uniform 初期値のまま getRipple() が常に 0 になります。今のままだとスナップショットが空です。

修正案
 	if (isChromatic()) {
+		const chromaticRipplePosition = gl.getUniformLocation(shaderProgram, 'u_ripplePositions[0]');
+		const chromaticRippleRadius = gl.getUniformLocation(shaderProgram, 'u_rippleRadiuses[0]');
+		gl.uniform2f(chromaticRipplePosition, 0.0, 0.0);
+		gl.uniform1f(chromaticRippleRadius, 0.35);
 		gl.uniform1f(u_time, 0);
 		gl.drawArrays(gl.TRIANGLES, 0, 6);
 	} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idea/MkAnimatedBg.dotted-ripples.vue` around lines 162 - 164, Branch for
isChromatic() currently draws with u_rippleRadiuses left at their default
(zero), so getRipple() returns 0 and the snapshot is empty; update the
isChromatic() path to initialize the ripple uniforms the same way as the
non-chromatic path before drawing: set u_rippleRadiuses (and any related
uniforms like u_time if needed) to positive/expected initial values used by
getRipple(), or call the existing ripple-initialization routine (the code that
prepares u_rippleRadiuses/getRipple) so gl.drawArrays renders visible ripples.
.devcontainer/devcontainer.json-7-8 (1)

7-8: ⚠️ Potential issue | 🟠 Major

devcontainer の Node.js バージョンと最小バージョン要件の不整合を修正してください。

devcontainer で Node.js 24.10.0 を指定していますが、.github/min.node-version では 22.21.0 が最小バージョンとして定義されています。開発環境と最小要件間のバージョン差異を統一してください。

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

In @.devcontainer/devcontainer.json around lines 7 - 8, The devcontainer Node
version under the JSON key "ghcr.io/devcontainers/features/node:1" is set to
"24.10.0" but the project minimum in .github/min.node-version is "22.21.0"; pick
one source of truth and make them consistent by either (A) lowering the
"version" value for "ghcr.io/devcontainers/features/node:1" to "22.21.0" to
match the min.node-version, or (B) bumping the string inside
.github/min.node-version to "24.10.0" (and update any CI/tooling that enforces
the min) so both entries match. Ensure the chosen numeric version string is
valid and consistent across both symbols.
packages/backend/assets/LICENSE_MisskeyHQ-13-21 (1)

13-21: ⚠️ Potential issue | 🟠 Major

利用許諾の条件が前後で矛盾しています。

Line 13 は「再配布・再利用不可」と読めますが、Line 21 は「use, copy, and publish ... for any purpose」を許可しています。このままだと利用者が何を許されているのか判断できず、配布時のコンプライアンス判断もぶれます。許可したい範囲を 1 つに絞って文言を統一してください。

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

In `@packages/backend/assets/LICENSE_MisskeyHQ` around lines 13 - 21, The license
text contains contradictory clauses: the sentence "These files may not be
redistributed, modified, or reused in any way without explicit permission from
MisskeyHQ." conflicts with "Permission is hereby granted to the user of this
digital asset and related documentation to use, copy, and publish the asset for
any purpose, provided that this notice and disclaimer of warranty appears in all
copies."; pick one definitive policy (either restrictive: disallow
redistribution/modification, or permissive: allow use/copy/publish under
conditions) and rewrite both the "These files may not be redistributed..." and
"Permission is hereby granted..." passages to express that single policy
consistently, updating related sentences such as "The user is expressly
prohibited..." to match the chosen grant or prohibition so the license no longer
contradicts itself.
packages/backend/src/core/ChatService.ts-334-341 (1)

334-341: ⚠️ Potential issue | 🟠 Major

readAllChatMessages() が未読状態を完全には消していません。

ここで消しているのは newChatMessagesExists:${readerId} だけなので、hasUnreadMessages() は false になりますが、getUserReadStateMap() / getRoomReadStateMap() が見ている newUserChatMessageExists:*newRoomChatMessageExists:* は残ります。結果として「全件既読」にした直後でも一覧や詳細側の未読表示が食い違います。

.github/workflows/test-production.yml-32-35 (1)

32-35: ⚠️ Potential issue | 🟠 Major

最小サポート Node.js の検証が外れています。

.node-version ファイルが主要バージョン 22 のみを指定しているのに対し、実際の最小要件は package.json^22.21.0 || ^24.0.0.github/min.node-version22.21.0 です。このため、本ワークフローは Node.js の最新 22.x で動作確認されますが、最小サポート版 22.21.0 での動作は保証されません。

.node-version を最小サポート版で固定するか、すべてのバージョン定義(.node-version.github/min.node-versionpackage.jsonengines.node)の整合性を常に保つ仕組みが必要です。

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

In @.github/workflows/test-production.yml around lines 32 - 35, The workflow is
using .node-version which only pins major 22, so it may not run on the minimum
supported Node.js (22.21.0); update the workflow in test-production.yml to
reference the canonical minimum Node version source (either set
node-version-file to '.github/min.node-version' or hard-pin node-version to
'22.21.0'), and ensure this aligns with the .node-version file,
.github/min.node-version, and package.json engines.node so all three definitions
remain consistent.
.config/example.yml-108-116 (1)

108-116: ⚠️ Potential issue | 🟠 Major

trustProxy の既定値が全プロキシ信頼になっており、IP ベースのセキュリティ制御が脆弱化しています。

packages/backend/src/server/ServerService.ts 行 82 で trustProxy: this.config.trustProxy ?? true と設定されており、設定がない場合は無条件に全プロキシを信頼します。コード全体で request.ip がレート制限(SigninApiService 行 93、SigninWithPasskeyApiService 行 89 など)とユーザーアカウントのアクセス履歴ロギング(ApiCallService 行 402-404)に使用されているため、バックエンドが逆プロキシを経由せずに直接到達可能な場合、X-Forwarded-For ヘッダーでクライアントが IP スプーフィングを行えます。.config/example.yml 行 113 の文書でこの危険な既定を「Default: trust all proxies」と記載すると、安全でない設定が正常のように見えてしまいます。

ドキュメントの推奨値を trustProxy: 1(ホップ数指定)に変更し、直接アクセス不可の構成を強く明記してください。

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

In @.config/example.yml around lines 108 - 116, Update the `.config/example.yml`
documentation to stop suggesting "Default: trust all proxies" and instead set
the recommended example value to "trustProxy: 1" with a clear, strong warning
that trusting all proxies is unsafe because code paths use request.ip (see
ServerService.ts where trustProxy is set via this.config.trustProxy ?? true, and
usages in SigninApiService, SigninWithPasskeyApiService, and ApiCallService),
and explicitly call out that production deployments should only trust known
proxy hops or configure the server to be unreachable directly from the internet
to prevent X-Forwarded-For IP spoofing.
packages/backend/eslint.config.js-27-28 (1)

27-28: ⚠️ Potential issue | 🟠 Major

no-unused-vars の全域無効化により未使用変数の検知が完全に消えています。

Line 28 で backend 全体の TS/TSX ファイルに対して @typescript-eslint/no-unused-vars を無効化していますが、TypeScript コンパイラオプション(tsconfig.json)では noUnusedLocalsnoUnusedParameters が両方とも false に設定されています。そのため、ESLint ルールが無効な状態では未使用変数・引数の検知が完全に失われます。問題のあるファイルに限定して override を設定するか、TypeScript コンパイラオプションで有効化してください。

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

In `@packages/backend/eslint.config.js` around lines 27 - 28, 現在 backend の
eslint.config.js で '@typescript-eslint/no-unused-vars'
を全域オフにしているため未使用変数検知が無効化されています。修正案としては(1)eslint.config.js 内の rules
から全域オフを取り除くか、該当ルールを特定のファイル群だけに限定する override を追加して全体の抑制をやめる、または(2)TypeScript 側で
tsconfig.json の noUnusedLocals と noUnusedParameters を true
に変更してコンパイラレベルで未使用検知を有効にする、どちらかを選んで適用してください。対象となる識別子は
'@typescript-eslint/no-unused-vars'(eslint.config.js)と
'noUnusedLocals'/'noUnusedParameters'(tsconfig.json)です。
packages/backend/migration/1745378064470-composite-note-index.js-15-25 (1)

15-25: ⚠️ Potential issue | 🟠 Major

concurrent モードでも DROP INDEX が通常ロックのままです。

CREATE INDEX CONCURRENTLY に合わせて transaction = false にしている一方、18行目の DROP INDEX IF EXISTS(concurrent ブロック内)、25行目の DROP INDEX IF EXISTS(常に実行)、および 33行目の down() メソッド内の DROP INDEX IF EXISTS はいずれも通常の DROP のままです。これだと大きい "note" テーブルで書き込みをブロックするため、フラグを有効にしても非ブロッキング化の目的を満たせません。concurrent モードでは DROP INDEX CONCURRENTLY IF EXISTS ... を使い分けた方が安全です。

修正例
 	async up(queryRunner) {
 		const concurrently = isConcurrentIndexMigrationEnabled();
+		const dropIndex = concurrently
+			? 'DROP INDEX CONCURRENTLY IF EXISTS'
+			: 'DROP INDEX IF EXISTS';
 
 		if (concurrently) {
 			const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
 			if (hasValidIndex.length === 0 || hasValidIndex[0].indisvalid !== true) {
-				await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
+				await queryRunner.query(`${dropIndex} "IDX_724b311e6f883751f261ebe378"`);
 				await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
 			}
 		} else {
 			await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
 		}
 
-		await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`);
+		await queryRunner.query(`${dropIndex} "IDX_5b87d9d19127bd5d92026017a7"`);
 		await queryRunner.query(`ANALYZE "user", "note"`);
 	}
 
 	async down(queryRunner) {
-		const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
-		await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
-		await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
+		const concurrently = isConcurrentIndexMigrationEnabled();
+		const createConcurrently = concurrently ? 'CONCURRENTLY ' : '';
+		const dropIndex = concurrently
+			? 'DROP INDEX CONCURRENTLY IF EXISTS'
+			: 'DROP INDEX IF EXISTS';
+
+		await queryRunner.query(`${dropIndex} "IDX_724b311e6f883751f261ebe378"`);
+		await queryRunner.query(`CREATE INDEX ${createConcurrently}"IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1745378064470-composite-note-index.js` around
lines 15 - 25, The DROP INDEX statements currently use the standard blocking
form; update them to use the concurrent-safe form when running in concurrent
mode: inside the concurrently branch change the DROP INDEX IF EXISTS at the
queryRunner.query call (the one checked by hasValidIndex) to use DROP INDEX
CONCURRENTLY IF EXISTS, change the unconditional DROP INDEX IF EXISTS after the
branch to use DROP INDEX CONCURRENTLY IF EXISTS when the concurrently flag is
true, and make the corresponding DROP INDEX IF EXISTS in the down() method use
DROP INDEX CONCURRENTLY IF EXISTS when transaction=false/concurrently is
enabled; locate these calls by searching for queryRunner.query invocations that
drop indices including the index names "IDX_724b311e6f883751f261ebe378" and
"IDX_5b87d9d19127bd5d92026017a7".
packages/backend/src/config.ts-311-316 (1)

311-316: ⚠️ Potential issue | 🟠 Major

manifest の存在だけでなくエントリキー欠落も吸収してください。

manifest.json 自体が存在していても src/_boot_.ts / src/boot.ts のキーが無い場合、ここは undefined を返します。今回 frontendEntry / frontendEmbedEntry{ file: string | null } に変更しているので、部分ビルドやエントリ名変更時に型と実値がずれて .file アクセスで落ちます。

💡 修正案
 const frontendManifest = frontendManifestExists ?
 	JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
-	: { 'src/_boot_.ts': { file: null } };
+	: {};
 const frontendEmbedManifest = frontendEmbedManifestExists ?
 	JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
-	: { 'src/boot.ts': { file: null } };
+	: {};
@@
-		frontendEntry: frontendManifest['src/_boot_.ts'],
+		frontendEntry: frontendManifest['src/_boot_.ts'] ?? { file: null },
 		frontendManifestExists: frontendManifestExists,
-		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
+		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'] ?? { file: null },
 		frontendEmbedManifestExists: frontendEmbedManifestExists,

Also applies to: 406-409

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

In `@packages/backend/src/config.ts` around lines 311 - 316, The manifest parsing
currently assumes keys 'src/_boot_.ts' and 'src/boot.ts' exist; update the logic
that builds frontendManifest/frontendEmbedManifest (and the similar code around
the frontendEntry/frontendEmbedEntry usage) to defensively check for the entry
key and fall back to { file: null } if the key is missing or its value is falsy:
after JSON.parse(...) verify parsedManifest['src/_boot_.ts'] (and
parsedEmbedManifest['src/boot.ts']) and assign frontendEntry/frontendEmbedEntry
to that object or to { file: null } so consumers that access .file never see
undefined.
packages/backend/src/core/NoteDraftService.ts-58-76 (1)

58-76: ⚠️ Potential issue | 🟠 Major

下書き上限チェックが競合で破れます。

countBy()insertOne() が分離されているので、同一ユーザーの同時 create が並ぶと全リクエストが上限未満と判定して超過保存できます。上限を仕様として守るなら、ここはユーザー単位で排他するか、DB/トランザクション側で原子的に制約してください。

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

In `@packages/backend/src/core/NoteDraftService.ts` around lines 58 - 76, The
create method in NoteDraftService (create, noteDraftsRepository.countBy,
noteDraftsRepository.insertOne, roleService.getUserPolicies, idService.gen) is
racy because countBy and insertOne are separate; fix it by enforcing the
draft-limit atomically: either perform the check+insert inside a single DB
transaction that uses a row- or table-level lock for the user's drafts (or
serializable isolation), or add a DB-enforced constraint (e.g., per-user draft
counter or unique indexed slot) and handle violation errors to return the
IdentifiableError; update create to attempt the atomic operation and fall back
to translating DB constraint/transaction failures into the same "Too many
drafts" IdentifiableError.
packages/backend/src/core/NoteDraftService.ts-143-149 (1)

143-149: ⚠️ Potential issue | 🟠 Major

visibleUserIds の存在確認が未完了です。

ここは findBy() の結果を捨てているので、存在しない ID が混ざってもそのまま保存されます。宛先付き下書きとしては壊れた状態を持ててしまうので、少なくとも取得件数の一致は確認したいです。

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

In `@packages/backend/src/core/NoteDraftService.ts` around lines 143 - 149, The
code fetches visibleUsers via this.usersRepository.findBy(...) but doesn't
verify all requested visibleUserIds were found, allowing drafts to reference
non-existent users; update NoteDraftService to validate that when
data.visibleUserIds is non-empty the returned visibleUsers length matches
data.visibleUserIds.length (or compare sets of ids), and if there are missing
ids either throw a validation error (e.g., BadRequest/ValidationError) or return
a clear failure so the draft is not saved with missing recipients; reference the
variables visibleUserIds, visibleUsers and the call this.usersRepository.findBy
to locate where to add this check.
packages/backend/src/core/NoteDraftService.ts-82-99 (1)

82-99: 🛠️ Refactor suggestion | 🟠 Major

部分更新の検証が既存値を見ていません。

validate(me, data) は patch だけを見るので、既存の replyId / renoteId / channelId と組み合わせた不正状態を作れます。たとえば direct への返信を保持したまま visibility だけ広げてもここを通ります。更新時は既存 draft と patch をマージした状態を検証した方がよいです。

💡 修正イメージ
 	public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
 		const draft = await this.noteDraftsRepository.findOneBy({
 			id: draftId,
 			userId: me.id,
 		});

 		if (draft == null) {
 			throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
 		}

-		await this.validate(me, data);
+		const nextDraft = {
+			...draft,
+			...data,
+		};
+		await this.validate(me, nextDraft);

 		const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update()
 			.set(data)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/NoteDraftService.ts` around lines 82 - 99, The
update method calls validate(me, data) which only checks the patch and can allow
invalid combinations when combined with existing draft fields; instead, merge
the loaded draft (variable draft) with the incoming partial data to form the
full new state and call validate(me, mergedData) before persisting; update the
code path around update(...) / validate(...) (use draft and data to produce
mergedData) so validation sees replyId/renoteId/channelId plus any changed
visibility, then proceed with the repository update.
packages/backend/src/core/ChannelMutingService.ts-8-10 (1)

8-10: ⚠️ Potential issue | 🟠 Major

期限切れミュートがキャッシュ上では有効なまま残ります。

list() は期限切れを除外していますが、mutingChannelsCachefetcher は全件取得なので、expiresAt を過ぎたミュートも isMuted() やタイムライン除外で効き続けます。クリーンアップが走るまで設定より長く効いてしまうので、キャッシュ生成時点で期限切れを落とす必要があります。

💡 修正案
-import { Brackets, In } from 'typeorm';
+import { Brackets, In, IsNull, MoreThan } from 'typeorm';
@@
 			fetcher: (userId) => this.channelMutingRepository.find({
-				where: { userId: userId },
+				where: [
+					{ userId, expiresAt: IsNull() },
+					{ userId, expiresAt: MoreThan(new Date()) },
+				],
 				select: ['channelId'],
 			}).then(xs => new Set(xs.map(x => x.channelId))),

Also applies to: 32-40

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

In `@packages/backend/src/core/ChannelMutingService.ts` around lines 8 - 10, The
cache fetcher for mutingChannelsCache must exclude expired mutes so they don't
remain effective; update the fetcher (the function that currently calls the
repository method that returns all MiChannelMuting records) to filter out
records where expiresAt is set and expiresAt <= now (use the same expiry logic
as ChannelMutingService.list()), so mutingChannelsCache, isMuted(), and any
timeline exclusion use only non-expired MiChannelMuting entries when populating
the cache and its keys.
packages/backend/src/core/NoteCreateService.ts-273-320 (1)

273-320: ⚠️ Potential issue | 🟠 Major

renote 対象の可視性チェックが reply 側より弱いです。

fetchAndCreate()reply では Line 334 の isVisibleForMe() を通していますが、renote 側には同等の検証がありません。今のままだと、ID さえ分かれば非表示のノートやアクセス不可なチャンネル内ノートを quote/renote できる経路になります。renote 取得後にも reply と同じ可視性チェックを入れるべきです。

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

In `@packages/backend/src/core/NoteCreateService.ts` around lines 273 - 320, The
renote visibility checks in NoteCreateService (inside the renote handling block)
are weaker than the reply path; after fetching renote (the variable renote in
fetchAndCreate()), call the same visibility validation used for replies (the
isVisibleForMe() flow used around Line 334 in fetchAndCreate()) so hidden notes,
notes in access-restricted channels, or otherwise non-visible renotes cannot be
quoted/renoted; if the helper function isVisibleForMe or its equivalent exists,
invoke it with renote and the current user and throw the same IdentifiableError
used for reply visibility failures when it returns false, otherwise inline the
same checks (channel access, specified/direct visibility, blocking) used by the
reply branch to reject non-visible renotes.
locales/en-US.yml-3275-3275 (1)

3275-3275: ⚠️ Potential issue | 🟠 Major

空文字のままだとカスタム絵文字管理 UI の文言が消えます。

searchLimit と確認ダイアログ文言が '' のままなので、英語ロケールではラベルや説明が空表示になります。zh-TW 側には値が入っているので、未確定ではなく入れ忘れに見えます。

💡 修正案
   _gridCommon:
-    searchLimit: ''
+    searchLimit: 'Number of items to display'
@@
-      confirmChangeView: ''
+      confirmChangeView: 'Change the display mode?'
       confirmMovePage: 'Would you like to move pages?'
-      confirmResetDescription: ''
+      confirmResetDescription: 'All current changes will be reset.'

Also applies to: 3300-3302

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

In `@locales/en-US.yml` at line 3275, The English locale has empty strings for the
custom emoji UI keys (notably searchLimit and the confirmation dialog keys near
the other empty entries) causing labels/descriptions to disappear; update the
en-US translations for searchLimit and the empty confirmation dialog keys to
non-empty English strings (use the corresponding zh-TW values as reference) so
the UI shows proper labels and dialog text, ensuring keys like searchLimit and
the adjacent confirmation dialog keys are populated consistently.
locales/ko-KR.yml-859-859 (1)

859-859: ⚠️ Potential issue | 🟠 Major

このキーは「サーバー再起動必須」なのに、文言が「リロード」になっています。

管理者がブラウザ更新だけで反映されると誤解します。運用手順を誤らせるので、再起動必須であることを明示した方がよいです。

修正案
-needToRestartServerToApply: "변경 사항은 새로고침이 필요합니다."
+needToRestartServerToApply: "변경 사항을 적용하려면 서버를 재시작해야 합니다."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ko-KR.yml` at line 859, The translation for the key
needToRestartServerToApply currently says "변경 사항은 새로고침이 필요합니다." which implies a
browser reload; update the value to explicitly state a server restart is
required (e.g., "변경 사항을 적용하려면 서버를 재시작해야 합니다.") so the meaning matches the key
needToRestartServerToApply and avoids operator confusion.

@mattyatea
Copy link
Member Author

@coderabbitai fullreview

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

@mattyatea Sure! I'll perform a full review of the PR now.


🧠 Learnings used
Learnt from: u1-liquid
Repo: MisskeyIO/misskey PR: 1081
File: package.json:50-71
Timestamp: 2025-10-31T07:51:27.692Z
Learning: In MisskeyIO/misskey's package.json resolutions section, the GitHub references for storybook-addon-misskey-theme and webgl-audiovisualizer intentionally do not include version tags, unlike other resolution entries.
✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical 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 (5)
packages/backend/src/core/MfmService.ts (1)

105-112: ⚠️ Potential issue | 🔴 Critical

new URL(href) に try-catch がなく、不正なURLで例外が発生します。

href が不正なURL(相対パス、javascript: スキーム、壊れた文字列など)の場合、new URL(href)TypeError をスローし、HTML解析が中断されます。

toHtml メソッド(lines 375-380, 389-394, 417-422)では try-catch で適切に処理されていますが、ここでは処理されていません。

🐛 例外処理を追加する修正案
 						if (part.length === 2 && href) {
 							//#region ホスト名部分が省略されているので復元する
-							const acct = `${txt}@${(new URL(href)).hostname}`;
-							text += acct;
+							try {
+								const acct = `${txt}@${(new URL(href)).hostname}`;
+								text += acct;
+							} catch {
+								text += txt;
+							}
 							//#endregion
 						} else if (part.length === 3) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/MfmService.ts` around lines 105 - 112, In
MfmService.ts inside the logic that builds acct with `const acct =
`${txt}@${(new URL(href)).hostname}`;` wrap the `new URL(href)` call in a
try-catch (or validate `href` first) so invalid/relative/javascript: URLs don't
throw; on error fallback to not appending the hostname (i.e., treat as plain
`txt`) or skip this branch, and log/debug as appropriate; mirror the
error-handling approach used by the `toHtml` method to ensure parsing continues
safely when `href` is malformed.
.github/workflows/test-production.yml (2)

51-56: ⚠️ Potential issue | 🟠 Major

SARIF アップロード権限が足りません。

このジョブは Code Scanning へ SARIF を送っていますが、permissionssecurity-events: write が必要です。現状だとアップロードが失敗しても continue-on-error: true で埋もれてしまいます。

修正案
 permissions:
   contents: read
+  security-events: write
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-production.yml around lines 51 - 56, The CodeQL SARIF
upload step is missing the required permissions so the upload will fail
silently; update the workflow so the job (or workflow) includes permissions:
security-events: write and remove or reconsider continue-on-error: true on the
github/codeql-action/upload-sarif@v4 step (or make failures visible) so SARIF
upload of snyk.sarif can succeed and surface errors instead of being swallowed.

44-45: ⚠️ Potential issue | 🟠 Major

snyk/actions/node@master ブランチ参照ではなく、固定リファレンスで指定してください。

@master のようなブランチ参照は浮動参照(floating reference)であり、GitHub のセキュリティガイダンスでは非推奨です。upstreamが更新されると予期しない挙動変更が発生し、スキャン結果の再現性が低下します。また、アクションのリポジトリが侵害された場合、悪意のあるコードが実行される供給チェーン攻撃のリスクがあります。GitHub の公式推奨に従い、完全なコミット SHA(40文字)または リリースタグに固定してください。特に SNYK_TOKEN シークレットを使用するため、セキュリティ上重要です。

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

In @.github/workflows/test-production.yml around lines 44 - 45, The workflow
step "Run Snyk to check for vulnerabilities" currently references
snyk/actions/node@master (a floating branch); replace that floating reference
with a fixed release tag or a full commit SHA (40-character) for the uses field
(i.e., use snyk/actions/node@vX.Y.Z or snyk/actions/node@<commit-sha>) so the
action is pinned and reproducible, and ensure the SNYK_TOKEN secret usage
remains unchanged; update the single step that uses "snyk/actions/node"
accordingly.
locales/ja-KS.yml (1)

2597-2614: ⚠️ Potential issue | 🟡 Minor

通知タイプの列挙が新しい通知をカバーできていません。

直前で scheduledNotePosted / scheduledNotePostFailed / chatRoomInvitationReceived / createToken の文言を追加しているのに、_notification._types 側に対応ラベルがありません。通知設定やフィルタ画面だけ関西弁訳が抜けます。

修正例
   _types:
     all: "すべて"
     note: "あんたらの新規投稿"
     follow: "フォロー"
     mention: "あんた宛て"
     reply: "リプライ"
     renote: "リノート"
     quote: "引用"
     reaction: "ツッコミ"
     pollEnded: "アンケートが終了したで"
+    scheduledNotePosted: "予約ノート投稿"
+    scheduledNotePostFailed: "予約ノート投稿失敗"
     receiveFollowRequest: "フォロー許可してほしいみたいやで"
     followRequestAccepted: "フォローが受理されたで"
     roleAssigned: "ロールが付与された"
+    chatRoomInvitationReceived: "チャットルーム招待"
     achievementEarned: "実績の獲得"
     exportCompleted: "エクスポート終わった"
     login: "ログイン"
+    createToken: "アクセストークン作成"
     test: "通知テスト"
     app: "連携アプリからの通知や"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ja-KS.yml` around lines 2597 - 2614, The _types map in
locales/ja-KS.yml is missing labels for recently added notification keys; add
Kansai-dialect labels for scheduledNotePosted, scheduledNotePostFailed,
chatRoomInvitationReceived, and createToken inside the _notification._types
(refer to the existing keys like note, follow, mention) so the notification
settings and filter UI show localized text—ensure each new key uses the same
style/terminology as nearby entries (e.g., casual Kansai phrasing) and matches
the key names exactly.
locales/de-DE.yml (1)

1635-1669: ⚠️ Potential issue | 🟡 Minor

de-DE の管理/セットアップ系キーがまだ揃っていません。

同PR内の locales/zh-CN.yml にはある scheduledNoteLimit, remoteNotesCleaning*, restartServerSetupWizardConfirm_*, entrancePageStyle*, remoteContentsCleaning* などがこのファイルには見当たりません。このままだとロール設定、サーバー設定、セットアップウィザードの新項目だけ de-DE で未翻訳表示になります。

Also applies to: 2010-2014, 3084-3125

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

In `@locales/de-DE.yml` around lines 1635 - 1669, The German locale is missing
several server/setup keys present in zh-CN; add the missing keys (e.g.,
scheduledNoteLimit, remoteNotesCleaning*, remoteContentsCleaning*,
restartServerSetupWizardConfirm_*, entrancePageStyle*, and any other keys
present in locales/zh-CN.yml but absent here) into the _serverSettings block
(and corresponding nested keys like _userGeneratedContentsVisibilityForVisitor
if applicable), using the same key names and providing German translations for
each new entry so the new role/server/setup and setup-wizard items display in
de-DE. Ensure keys with wildcard groups (remoteNotesCleaning*,
remoteContentsCleaning*) are fully expanded and translated to mirror the zh-CN
structure and keep YAML indentation and quoting consistent with surrounding
entries.
♻️ Duplicate comments (1)
packages/backend/package.json (1)

24-29: ⚠️ Potential issue | 🟡 Minor

jest:fed スクリプトに NODE_ENV=test が設定されていません

Line 26 の jest:fed スクリプトのみ cross-env NODE_ENV=test が設定されていません。他の Jest スクリプト(jestjest:e2ejest-and-coverage など)はすべて NODE_ENV=test を指定しています。コード内で process.env.NODE_ENV === 'test' をチェックしている箇所があるため、テスト環境の不整合が発生する可能性があります。

🔧 修正案
-    "jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
+    "jest:fed": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.fed.cjs",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/package.json` around lines 24 - 29, The "jest:fed" npm
script is missing NODE_ENV=test which can cause test-environment mismatches;
update the "jest:fed" entry in package.json (the script named jest:fed) to
prepend "cross-env NODE_ENV=test" so it matches the other Jest scripts (jest,
jest:e2e, jest-and-coverage) and ensures process.env.NODE_ENV === 'test' during
federated Jest runs.
🟠 Major comments (29)
packages/backend/assets/LICENSE_MisskeyHQ-13-21 (1)

13-21: ⚠️ Potential issue | 🟠 Major

許諾条件が相互に矛盾しています。

Line 13 では再配布・再利用を全面的に禁止していますが、Line 21 では notice 同梱を条件に use, copy, and publish を許可しています。publish は実質的に再配布を含み得るため、このままだと downstream が同梱・配布してよいのか判断できず、ライセンス解釈がぶれます。禁止したい行為と許可したい行為を一つの方針に揃えて、条文を整理したほうがよいです。

整合させるための最小修正例
-are owned by MisskeyHQ and are subject to copyright, with all rights reserved by MisskeyHQ. These files may not be redistributed, modified, or reused in any way without explicit permission from MisskeyHQ.
+are owned by MisskeyHQ and are subject to copyright, with all rights reserved by MisskeyHQ. These files may be used, copied, and published only under the terms below.

-Permission is hereby granted to the user of this digital asset and related documentation to use, copy, and publish the asset for any purpose, provided that this notice and disclaimer of warranty appears in all copies. The user is expressly prohibited from distributing, sublicensing, selling, reselling, or transferring the asset for profit. Any form of modification or tampering with the asset is strictly prohibited.
+Permission is hereby granted to use, copy, and publish the asset, provided that this notice and disclaimer of warranty appears in all copies. Modification is prohibited. Distribution for profit, sublicensing, selling, reselling, or transferring the asset is prohibited unless explicitly permitted by MisskeyHQ.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/assets/LICENSE_MisskeyHQ` around lines 13 - 21, The license
contains contradictory clauses: one sentence forbids
redistribution/modification/reuse while a later paragraph ("Permission is hereby
granted to the user... to use, copy, and publish the asset for any purpose,
provided that this notice... appears in all copies") appears to permit
publishing; resolve this by choosing a single policy and rewriting the
conflicting phrases accordingly — either (A) make redistribution allowed under
the stated terms by replacing "These files may not be redistributed, modified,
or reused in any way without explicit permission" with a scoped restriction that
permits redistribution and publication only when the notice is retained and
commercial resale/sublicensing is prohibited, or (B) tighten the permission
paragraph by removing "publish" and adding explicit prohibitions on
redistribution/sublicensing to match the original ban; update the sentences
containing "These files may not be redistributed, modified, or reused..." and
"Permission is hereby granted..." so they state a single, consistent rule
(reference these exact phrases to locate and edit).
packages/backend/migration/1753868431598-remove_note_constraints.js-14-16 (1)

14-16: ⚠️ Potential issue | 🟠 Major

ロールバック時に孤児参照で ADD CONSTRAINT が失敗します。

up() 適用後は replyId / renoteId が孤児化し得るので、Line 15-16 の外部キー再作成は既存データ検証で失敗します。これだと一度でも不整合データが入った環境では down() が実質使えません。制約を戻す前に孤児参照を掃除してください。

修正例
     async down(queryRunner) {
+        await queryRunner.query(`
+            UPDATE "note" AS child
+            SET "replyId" = NULL
+            WHERE "replyId" IS NOT NULL
+              AND NOT EXISTS (
+                  SELECT 1
+                  FROM "note" AS parent
+                  WHERE parent."id" = child."replyId"
+              )
+        `);
+        await queryRunner.query(`
+            UPDATE "note" AS child
+            SET "renoteId" = NULL
+            WHERE "renoteId" IS NOT NULL
+              AND NOT EXISTS (
+                  SELECT 1
+                  FROM "note" AS parent
+                  WHERE parent."id" = child."renoteId"
+              )
+        `);
         await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
         await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1753868431598-remove_note_constraints.js` around
lines 14 - 16, down(queryRunner) currently re-adds foreign keys
"FK_17cb3553c700a4985dff5a30ff5" and "FK_52ccc804d7c69037d558bac4c96" without
cleaning orphaned replyId/renoteId, so ADD CONSTRAINT will fail if orphan rows
exist; before the two ALTER TABLE calls in down(), run SQL via queryRunner.query
to remove or fix orphaned references (e.g., DELETE or UPDATE to set
replyId/renoteId to NULL for rows where replyId/renoteId NOT IN (SELECT id FROM
"note") or where referenced note is missing) so the subsequent ALTER TABLE ...
ADD CONSTRAINT statements succeed.
cypress/e2e/basic.cy.ts-81-82 (1)

81-82: ⚠️ Potential issue | 🟠 Major

この変更で重複ユーザー名テストが別条件で通るようになります。

Line 82 で招待コードが送信可能条件に加わったので、下の signup with duplicated username は招待コード未入力の時点で submit が無効のままです。つまり、いまの最終アサーションは「ユーザー名重複」ではなく「招待コード不足」で成立してしまい、重複チェックの回帰を見逃します。あちらのケースにも有効な招待コードを入れて、重複ユーザー名固有の失敗を検証してください。

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

In `@cypress/e2e/basic.cy.ts` around lines 81 - 82, テスト "signup with duplicated
username" が招待コード未入力のままでは最終アサーションが「招待コード不足」を検証してしまうので、同テスト内で
'[data-cy-signup-invitation-code] input' に有効な招待コードを入力して
'[data-cy-signup-submit]'
が送信可能になる状態を作ってから重複ユーザ名固有のエラーメッセージを検証するように修正してください(テスト名: signup with duplicated
username、確認対象セレクタ: [data-cy-signup-submit] と重複エラーメッセージ)。
cypress/e2e/basic.cy.ts-35-41 (1)

35-41: ⚠️ Potential issue | 🟠 Major

cy.wait('@update-meta') だけでは API エラーを見逃す可能性があります。

cy.wait() はインターセプトされた通信の完了しか待つため、/api/admin/update-meta が 4xx/5xx で応答しても待機は完了してしまいます。初期セットアップの失敗を検出するには、Line 41 で成功ステータスを検証してください。

修正例
-		cy.wait('@update-meta');
+		cy.wait('@update-meta')
+			.its('response.statusCode')
+			.should('be.gte', 200)
+			.should('be.lt', 300);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cypress/e2e/basic.cy.ts` around lines 35 - 41, cy.wait('@update-meta') only
waits for the request to finish but not that it succeeded; after the existing
cy.wait('@update-meta') call, assert the intercepted response status is in the
2xx range by inspecting the alias's response (the '@update-meta' alias from
cy.intercept) and failing the test if statusCode is >= 400; update the test
around the selectors used (cy.get('[data-cy-next]', '[data-cy-server-name]
input', '[data-cy-server-setup-wizard-apply]') and the cy.wait('@update-meta')
line to include an assertion on the intercepted response.statusCode (or
response.status) to ensure the setup API returned success.
packages/backend/migration/1752509043847-migration-cleanup.js-23-24 (1)

23-24: ⚠️ Potential issue | 🟠 Major

既存データを埋めずに NOT NULL を追加すると移行で止まります。

Line 24 は、長く nullable だった flash.visibility にそのまま SET NOT NULL をかけています。既存環境に NULL が 1 件でも残っているとこの migration 全体が失敗するので、先に backfill する UPDATENULL チェックを挟いでから制約を付けた方が安全です。

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

In `@packages/backend/migration/1752509043847-migration-cleanup.js` around lines
23 - 24, The migration directly runs ALTER TABLE "flash" ALTER COLUMN
"visibility" SET NOT NULL which will fail if any existing rows have NULL; before
calling queryRunner.query for the NOT NULL change (in
1752509043847-migration-cleanup.js), add a safe backfill or guard: run an UPDATE
to set a concrete default for flash.visibility for rows where visibility IS NULL
(or run a SELECT check and abort with a clear error), then perform the ALTER;
reference the ALTER TABLE statement and the "flash.visibility" column so you
update the correct rows and ensure the NOT NULL constraint can be applied
safely.
packages/backend/migration/1752509043847-migration-cleanup.js-36-39 (1)

36-39: ⚠️ Potential issue | 🟠 Major

Line 56 の down() メソッドで userId コメントが正しく復元されていません。

up() では replyId、renoteId、channelId と同じく userId にコメントを設定していますが、down() で userId だけが他の 3 カラムと異なる復元方法になっています。

元の createNoteDraft migration には COMMENT ON COLUMN ステートメントがないため、元の状態ではすべてのコメントが NULL です。したがって down() メソッドでは以下のように統一されるべきです:

  • replyId → NULL ✓
  • renoteId → NULL ✓
  • channelId → NULL ✓
  • userId → NULL (現在は 'The ID of author.' となっており不正)

Line 56 を await queryRunner.query(\COMMENT ON COLUMN "note_draft"."userId" IS NULL`);` に変更してください。

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

In `@packages/backend/migration/1752509043847-migration-cleanup.js` around lines
36 - 39, In the down() method of the migration (function down), restore the
userId column comment the same way as replyId/renoteId/channelId by setting it
back to NULL; replace the current incorrect restoration that reassigns 'The ID
of author.' with a COMMENT ON COLUMN "note_draft"."userId" IS NULL statement so
all four columns revert to no comment consistently.
packages/backend/eslint.config.js-28-28 (1)

28-28: 🛠️ Refactor suggestion | 🟠 Major

no-unused-vars を完全に無効化すると、未使用変数の検出機構がなくなります。

packages/backend の tsconfig.json で noUnusedLocalsnoUnusedParameters明示的に false に設定されているため、TypeScript コンパイラ側の保護がありません。ESLint ルールも無効化されているので、未使用の import / local / parameter を一切検出できない状態になっています。

以下のいずれかの対応が必須です:

  • ignore pattern(例:^_ プレフィックス)を活用してルールを部分的に無効化する
  • または、tsconfig で noUnusedLocals / noUnusedParameterstrue に設定する
🔧 提案:ignore pattern を使った改善案
-			'@typescript-eslint/no-unused-vars': 'off',
+			'@typescript-eslint/no-unused-vars': ['warn', {
+				argsIgnorePattern: '^_',
+				varsIgnorePattern: '^_',
+				caughtErrorsIgnorePattern: '^_',
+			}],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/eslint.config.js` at line 28, The
'@typescript-eslint/no-unused-vars' rule is fully turned off in
packages/backend/eslint.config.js which, combined with tsconfig.json having
noUnusedLocals and noUnusedParameters set to false, removes all unused-variable
detection; fix by either re-enabling the ESLint rule with an ignore pattern
(e.g. change the '@typescript-eslint/no-unused-vars' setting from 'off' to a
configured error level and set
varsIgnorePattern/argsIgnorePattern/caughtErrorsIgnorePattern to '^_' so
intentionally underscored identifiers are exempt) or enable TypeScript compiler
checks by setting noUnusedLocals and noUnusedParameters to true in the package's
tsconfig.json; apply one of these changes and run lint/tsc to verify unused
imports/locals/params are reported.
packages/backend/src/core/AiService.ts-73-75 (1)

73-75: ⚠️ Potential issue | 🟠 Major

tf.node.decodeImage() 経由での形式対応制限を確認して対応すること

@tensorflow/tfjs-nodetf.node.decodeImage() は BMP、GIF、JPEG、PNG のみ対応で、WebP・AVIF には非対応です。Line 74-75 は MIME 情報を使わず完全にこの API に依存するため、WebP/AVIF を受け付けているシステムにおいて、mime パラメータなしの呼び出しまたは Buffer 渡しの場合、これらの形式は無言で失敗します。

後続の catch ブロック(86 行)で例外を吸収しているため、呼び出し側は null 返却を見ることになります。メソッドのオーバーロード(37-38 行)では mime を省略可能なため、実際に WebP/AVIF が予期せず mime なし で渡される可能性があります。

64-72 行の Sharp 経由パスと 74-75 行の tf.node パスで形式サポートの整合性を保証するか、mime なし呼び出しは Sharp 処理へ統一するかの対応が必要です。

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

In `@packages/backend/src/core/AiService.ts` around lines 73 - 75, The
tf.node.decodeImage(...) path (tf.node.decodeImage in AiService) only supports
BMP/GIF/JPEG/PNG so when mime is omitted or a Buffer for WebP/AVIF is passed it
silently fails and gets swallowed by the catch; update the method to detect the
image type and route unsupported formats to the Sharp branch (or use Sharp to
convert WebP/AVIF to a supported format before calling tf.node.decodeImage)
and/or require/validate mime when calling tf.node.decodeImage; specifically:
when handling the sourceOrPath branch that currently calls
tf.node.decodeImage(buffer, 3) check the MIME (or probe the buffer with a
file-type detector) and if it's WebP/AVIF use the existing Sharp processing path
(or convert the buffer to PNG/JPEG via Sharp) instead of calling
tf.node.decodeImage, and ensure errors from tf.node.decodeImage are not silently
swallowed by the catch so callers can distinguish unsupported formats from other
failures.
packages/backend/src/core/NoteCreateService.ts-253-255 (1)

253-255: ⚠️ Potential issue | 🟠 Major

visibleUserIds の欠落を黙って無視しないでください。

findBy() は存在するユーザーだけ返すので、今の実装だと typo や削除済み ID を含んでも一部の宛先が静かに落ちます。fileIds と同じように、重複除去後の要求件数と取得件数を突き合わせて不足時はエラーにした方が安全です。

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

In `@packages/backend/src/core/NoteCreateService.ts` around lines 253 - 255, The
code silently ignores missing/typo/deleted IDs when resolving visibleUserIds by
using usersRepository.findBy (assigned to visibleUsers); instead deduplicate
data.visibleUserIds, fetch users with findBy, then compare the number of unique
requested IDs to the number of fetched users and throw a validation/error if
they differ (similar to how fileIds are validated). Update NoteCreateService to
perform the dedupe + count check and raise a clear error referencing
data.visibleUserIds when any requested user IDs are missing.
packages/backend/src/core/NoteCreateService.ts-273-320 (1)

273-320: ⚠️ Potential issue | 🟠 Major

リノート対象にも isVisibleForMe() 相当の可視性チェックが必要です。

返信側は Line 334 で this.noteEntityService.isVisibleForMe() を通していますが、こちらは存在確認と一部の可視性分岐だけです。チャンネル制約など isVisibleForMe() 側の条件を素通りできるので、リノート対象にも同じ判定を入れてください。

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

In `@packages/backend/src/core/NoteCreateService.ts` around lines 273 - 320, The
renote path in NoteCreateService currently skips the full visibility rules;
update the renote handling block (where variable renote is fetched) to call
this.noteEntityService.isVisibleForMe(renote, user, { channelId: data.channelId,
... }) (same check used later at Line 334) and if it returns false, throw the
appropriate IdentifiableError(s) as the visibility gate would — ensuring channel
constraints, specified/direct and other visibility rules are enforced for renote
targets just like for the main note.
idea/MkAnimatedBg.dotted.vue-142-150 (1)

142-150: ⚠️ Potential issue | 🟠 Major

Chromatic 用の静止画が毎回変わります。

isChromatic() 分岐でも u_seedMath.random() で初期化しているので、1 枚描画の結果が実行ごとに変わります。これだと Chromatic のスナップショットが不安定になります。

修正案
- gl.uniform1f(u_seed, Math.random() * 1000);
+ gl.uniform1f(u_seed, isChromatic() ? 0 : Math.random() * 1000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@idea/MkAnimatedBg.dotted.vue` around lines 142 - 150, The Chromatic snapshots
are unstable because u_seed is being set with Math.random() before the
isChromatic() branch; change the logic so that when isChromatic() returns true
you set a deterministic seed instead of Math.random() (e.g., a fixed constant or
a seed derived from props), i.e. update the code that calls gl.uniform1f(u_seed,
...) (and/or the isChromatic() branch that currently calls gl.uniform1f(u_time,
0) then draw) to use a stable value for u_seed so each Chromatic render is
repeatable.
.github/workflows/test-frontend.yml-42-45 (1)

42-45: ⚠️ Potential issue | 🟠 Major

.node-version 単独更新でこの workflow が走りません。

Line 45 で Node.js の参照元を .node-version に変えていますが、Line 11-25 の paths にそのファイルが入っていません。Node.js バージョンだけを更新した PR/commit ではこのジョブがスキップされ、変更自体を CI で検証できなくなります。

修正案
 on:
   push:
     branches:
       - main
       - next
       - beta
       - io
       - host
     paths:
       - packages/frontend/**
       # for permissions
       - packages/misskey-js/**
+      - .node-version
       - pnpm-lock.yaml
       - .github/workflows/test-frontend.yml
       - .github/misskey/test.yml
   pull_request:
     paths:
       - packages/frontend/**
       # for permissions
       - packages/misskey-js/**
+      - .node-version
       - pnpm-lock.yaml
       - .github/workflows/test-frontend.yml
       - .github/misskey/test.yml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test-frontend.yml around lines 42 - 45, The workflow uses
actions/setup-node@v4.4.0 with node-version-file: '.node-version' but the
workflow's on: push (paths) list does not include '.node-version', so updates to
that file alone won't trigger the job; update the workflow's paths array (the
on: push paths block referenced in lines around the top of the file) to include
'.node-version' so changes to the file will trigger the job that runs the
actions/setup-node step.
packages/backend/scripts/dev.mjs-31-36 (1)

31-36: ⚠️ Potential issue | 🟠 Major

killProc() が既に終了したプロセスで無限に待機します。

backendProcess がクラッシュなどで既に終了した状態で killProc() が呼ばれると、Line 35 の Promise は新しい exit イベントを待ち続けて再起動ループが止まります。プロセスが実行中かどうかを確認してから kill() と待機を実行する必要があります。

修正案
 async function killProc() {
 	if (backendProcess) {
 		backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
-		backendProcess.kill();
-		await new Promise(resolve => backendProcess.on('exit', resolve));
+		if (backendProcess.exitCode === null && backendProcess.signalCode === null) {
+			backendProcess.kill();
+			await new Promise(resolve => backendProcess.once('exit', resolve));
+		}
 		backendProcess = undefined;
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/scripts/dev.mjs` around lines 31 - 36, killProc() can hang
waiting for an 'exit' event if backendProcess already exited; update killProc to
first check the ChildProcess state (use backendProcess.exitCode !== null or
backendProcess.killed / exitCode) and only call backendProcess.kill() and await
the 'exit' event when the process is still running, otherwise skip
kill()/waiting and simply clear backendProcess; reference symbols: killProc,
backendProcess, backendProcess.kill(), and the 'exit' event so the fix guards
before killing and awaiting.
packages/backend/migration/1761569941833-add-channel-muting.js-17-17 (1)

17-17: ⚠️ Potential issue | 🟠 Major

channel_muting に一意制約を付けてください

("userId", "channelId") が非 UNIQUE のため、同一ユーザー・同一チャンネルの重複 mute が並行実行で作成され得ます。解除や判定の一貫性が崩れる原因になります。

💡 修正例
-        await queryRunner.query(`CREATE INDEX "IDX_b96870ed326ccc7fa243970965" ON "channel_muting" ("userId", "channelId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b96870ed326ccc7fa243970965" ON "channel_muting" ("userId", "channelId") `);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1761569941833-add-channel-muting.js` at line 17,
現在のCREATE INDEX "IDX_b96870ed326ccc7fa243970965" ON "channel_muting" ("userId",
"channelId")は非 UNIQUE のため重複行が作成され得ます。修正手順は、まず重複する (userId, channelId)
ペアを削除または統合して一意性を担保し、その後既存のインデックス "IDX_b96870ed326ccc7fa243970965"
を削除してから同列に対してUNIQUE制約を付与する(例: ALTER TABLE "channel_muting" ADD CONSTRAINT ...
UNIQUE ("userId","channelId") もしくはCREATE UNIQUE INDEX ... ON "channel_muting"
("userId","channelId"))ようにマイグレーションを更新してください。必ずデータ移行(重複削除)をマイグレーション内で行い、マイグレーションが失敗しないようトランザクションか安全な順序で実行してください。
packages/backend/src/core/ChannelFollowingService.ts-59-76 (1)

59-76: ⚠️ Potential issue | 🟠 Major

idOnly: true のときの戻り値型が正確ではありません。

線 68 では Promise<MiChannel[]> を返す型宣言になっていますが、線 74-76 の idOnly: true 分岐では { id: string } だけのオブジェクトを MiChannel にキャストして返しています。JSDoc でも「ID以外のフィールドに値がセットされなくなる」と明記されているのに、型システムがこの制約を表現していません。

現在の呼び出し箇所はすべて .then(x => x.map(x => x.id))id だけを抽出しているため実運用上の問題はありませんが、型安全性のため TypeScript のオーバーロードを使い、idOnly: true のときは Array<Pick<MiChannel, 'id'>> を返すように修正してください。

修正例
+	public async list(
+		params: {
+			requestUserId: MiUser['id'],
+		},
+		opts: {
+			idOnly: true;
+		},
+	): Promise<Array<Pick<MiChannel, 'id'>>>;
+	public async list(
+		params: {
+			requestUserId: MiUser['id'],
+		},
+		opts?: {
+			idOnly?: false;
+			joinUser?: boolean;
+			joinBannerFile?: boolean;
+		},
+	): Promise<MiChannel[]>;
 	public async list(
 		params: {
 			requestUserId: MiUser['id'],
 		},
 		opts?: {
 			idOnly?: boolean;
 			joinUser?: boolean;
 			joinBannerFile?: boolean;
 		},
-	): Promise<MiChannel[]> {
+	): Promise<MiChannel[] | Array<Pick<MiChannel, 'id'>>> {
 		if (opts?.idOnly) {
 			const q = this.channelFollowingsRepository.createQueryBuilder('channel_following')
 				.select('channel_following.followeeId')
 				.where('channel_following.followerId = :userId', { userId: params.requestUserId });

 			return q
 				.getRawMany<{ channel_following_followeeId: string }>()
-				.then(xs => xs.map(x => ({ id: x.channel_following_followeeId } as MiChannel)));
+				.then(xs => xs.map(x => ({ id: x.channel_following_followeeId })));
 		} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/ChannelFollowingService.ts` around lines 59 - 76,
The list method's declared return type Promise<MiChannel[]> is incorrect for the
idOnly branch; add TypeScript overloads for ChannelFollowingService.list so that
when called with opts: { idOnly: true } it returns
Promise<Array<Pick<MiChannel,'id'>>> and the existing signature remains for the
general case, then keep a single implementation (the current function body) that
satisfies both overloads and returns the mapped raw rows as
Array<Pick<MiChannel,'id'>> when opts?.idOnly is true and MiChannel[] otherwise;
update the method declarations above the implementation accordingly (referencing
ChannelFollowingService.list, MiChannel, and the opts.idOnly branch).
packages/backend/src/cli/CommandService.ts-15-22 (1)

15-22: ⚠️ Potential issue | 🟠 Major

未使用の logger フィールドと import を削除してください。

Line 15 の private logger: Logger; と line 9 の import type Logger from '@/logger.js'; はコード内で一度も使用されていません。削除してください。

修正例
 import { Inject, Injectable } from '@nestjs/common';
 import type { Config } from '@/config.js';
 import { DI } from '@/di-symbols.js';
-import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
 import { MetaService } from '@/core/MetaService.js';

 `@Injectable`()
 export class CommandService {
-	private logger: Logger;
-
 	constructor(
 		`@Inject`(DI.config)
 		private config: Config,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/cli/CommandService.ts` around lines 15 - 22, Remove the
unused Logger import and class field: delete the import line "import type Logger
from '@/logger.js';" and the field declaration "private logger: Logger;" in the
CommandService class (constructor and other methods do not reference it); ensure
no other references to Logger remain in this file and run a quick
typecheck/build to confirm no residual usages.
packages/backend/src/core/NoteDraftService.ts-58-76 (1)

58-76: ⚠️ Potential issue | 🟠 Major

下書き上限チェックが非原子的です。

Line 60-65 の countBy() と Line 72-76 の insertOne() が分かれているので、同一ユーザーから並列に create() が来ると noteDraftLimit を超えた件数を保存できます。上限を厳密に守るなら、同一トランザクション内で検査と挿入を行うか、ユーザー単位のロックが必要です。

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

In `@packages/backend/src/core/NoteDraftService.ts` around lines 58 - 76, The
create method in NoteDraftService performs a non-atomic draft-limit check using
roleService.getUserPolicies + noteDraftsRepository.countBy followed by
noteDraftsRepository.insertOne, which allows race conditions; wrap the check and
insert in a single atomic operation (use a DB transaction or a per-user
mutex/lock) so the count and insert occur together, or enforce the limit at the
database level and handle constraint errors; update create (and related
idService.gen and validate usage) to run inside that transaction/lock and
translate DB/locking failures into the existing IdentifiableError when the draft
limit is exceeded.
locales/id-ID.yml-403-403 (1)

403-403: ⚠️ Potential issue | 🟠 Major

enableMcaptcha の訳文が空です。

Line 403 のままだと mCaptcha の設定ラベルが空表示になり、何を有効化する項目なのか判別できません。

修正例
-enableMcaptcha: ""
+enableMcaptcha: "Nyalakan mCaptcha"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/id-ID.yml` at line 403, The translation for the key enableMcaptcha is
currently empty; update the id-ID locale entry for enableMcaptcha by providing
an appropriate Indonesian label such as "Aktifkan mCaptcha" (or similar phrasing
like "Mengaktifkan mCaptcha") so the mCaptcha setting displays a clear label;
modify the enableMcaptcha value in locales id-ID.yml (key: enableMcaptcha) to
the chosen translated string.
packages/backend/src/core/NoteDraftService.ts-82-99 (1)

82-99: ⚠️ Potential issue | 🟠 Major

部分更新で既存ドラフトとの整合性チェックを回避できます。

Line 92 で data だけを validate() に渡しているため、既存の replyId / renoteId / channelId と組み合わせた制約が再評価されません。たとえば既存ドラフトが限定公開ノートへの返信を持っていても、visibility だけを変更すれば Line 230-231 の制約をすり抜けた状態で保存できます。更新時は保存前に draftdata をマージした最終状態を検証してください。

修正イメージ
-		await this.validate(me, data);
+		await this.validate(me, {
+			...draft,
+			...data,
+		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/core/NoteDraftService.ts` around lines 82 - 99, The
update flow calls validate(me, data) with only the partial update, which lets
constraints involving existing draft fields
(replyId/renoteId/channelId/visibility) be bypassed; before persisting in
update() (the block that uses
noteDraftsRepository.createQueryBuilder().update()), merge the existing draft
and the incoming data into a single finalDraft object (e.g., finalDraft = {
...draft, ...data } but only with DTO fields) and pass that merged object to
validate(me, finalDraft), then proceed to save; ensure validate() sees the
complete final state so cross-field checks on
replyId/renoteId/channelId/visibility run correctly.
locales/ja-KS.yml-3001-3028 (1)

3001-3028: ⚠️ Potential issue | 🟠 Major

サーバー初期設定ウィザードのキーがまだ揃っていません。

このブロックは追加されていますが、並行して追加されている他ロケール相当の serverSetting_use.single/group/open_scale.small/medium/largeapplyTheseSettings などが見当たりません。新しいセットアップフローで関西弁と fallback が混在表示になります。

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

In `@locales/ja-KS.yml` around lines 3001 - 3028, The _serverSetupWizard block was
added but the complementary keys (e.g., serverSetting,
_use.single/_use.group/_use.open, _scale.small/_scale.medium/_scale.large,
applyTheseSettings) are missing causing mixed Kansai and fallback text; update
locales/ja-KS.yml to include the full set of server setup keys that other
locales define by copying/translating the corresponding entries (serverSetting,
_use.*, _scale.*, applyTheseSettings, etc.) from a complete locale (like ja or
en) into the ja-KS namespace so the new setup flow has a consistent, complete
keyset and avoids fallback mixing.
locales/it-IT.yml-2576-2577 (1)

2576-2577: ⚠️ Potential issue | 🟠 Major

disableFederation の訳が別機能に見えます。

Gestisci la federazione だと「連合を管理する」意味になっていて、投稿を他サーバーへ送らない設定だと伝わりません。公開範囲まわりの文言なので、このままだと誤操作を招きます。

修正例
-  disableFederation: "Gestisci la federazione"
+  disableFederation: "Disabilita la federazione"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/it-IT.yml` around lines 2576 - 2577, The label disableFederation
currently reads like "manage federation" and is misleading; update the
translation for disableFederation to a concise phrase that clearly indicates
turning federation off (e.g., use an Italian label meaning "Disable federation"
or "Do not send activities to other instances"), and ensure
disableFederationDescription (the explanatory text) remains consistent with that
meaning so the UI clearly conveys that posts will not be sent to remote
instances.
locales/en-US.yml-3275-3275 (1)

3275-3275: ⚠️ Potential issue | 🟠 Major

空文字のまま追加しないでください。

Line 3275 / 3300 / 3302 が '' のままなので、検索条件ラベルと確認ダイアログ本文が空表示になります。locales/en-US.yml はフォールバック元でもあるため、このままだと無言 UI が出ます。

Also applies to: 3300-3302

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

In `@locales/en-US.yml` at line 3275, The YAML keys currently set to empty strings
(e.g., searchLimit and the other two empty entries) must not be left as '' —
replace each empty value with a meaningful English string: provide a concise
search condition label for searchLimit and proper confirmation dialog text for
the confirmation-body keys so the UI fallback isn’t blank; locate the empty
entries by their key names (searchLimit and the two other blank keys in the same
block) and update their values with appropriate human-readable phrases.
locales/ru-RU.yml-242-244 (1)

242-244: ⚠️ Potential issue | 🟠 Major

サイレンス設定の説明が原文と別の挙動を示しています。

Line 242-244 は locales/en-US.ymlsilencedInstancesDescription / mediaSilencedInstancesDescription と比べて、「silenced」を「blocked」と説明したり、follow request 周りの意味が崩れています。管理画面の説明としては誤設定を招くので、原文どおりの挙動に合わせて訳し直した方がよいです。

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

In `@locales/ru-RU.yml` around lines 242 - 244, The Russian translations for
silencedInstancesDescription and mediaSilencedInstancesDescription currently
change the meaning (using “blocked” and altering follow-request behavior);
update these two strings so their wording exactly reflects the original en-US
entries: keep the concept “silenced” (not “blocked”), preserve the original
notes about how accounts from listed instances are treated (what actions are
restricted, e.g., follow/follow request/local account behavior) and that media
access is disabled for those instances; replace the current Russian text for the
keys silencedInstancesDescription and mediaSilencedInstancesDescription with
faithful translations that mirror the en-US messages.
locales/ru-RU.yml-460-461 (1)

460-461: ⚠️ Potential issue | 🟠 Major

空文字の addModerationNote は埋めてください。

Line 461 が空なので、モデレーションノート追加のボタン/メニューが無表示になります。

🛠️ 修正案
-addModerationNote: ""
+addModerationNote: "Добавить заметку модератора"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ru-RU.yml` around lines 460 - 461, The translation key
addModerationNote in locales/ru-RU.yml is empty, causing the "add moderation
note" button/menu to not render; update the addModerationNote value to a proper
Russian translation (e.g., "Добавить заметку модерации" or similar) to ensure
the UI label appears, keeping moderationNoteDescription as-is and only replacing
the empty string for addModerationNote.
locales/ru-RU.yml-1176-1176 (1)

1176-1176: ⚠️ Potential issue | 🟠 Major

招待コード上限の案内が逆の意味になっています。

Line 1176 の locales/en-US.yml は「{time} に {limit} へリセットされる」ですが、現在の訳文は「無制限に作成できる」と読めます。利用上限を誤案内するので、ここは原文に寄せてください。

🛠️ 修正案
-inviteLimitResetCycle: "За определенное {time} Вы можете создать неограниченное количество пригласительных кодов {limit} "
+inviteLimitResetCycle: "Этот лимит будет сброшен до {limit} в {time}."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ru-RU.yml` at line 1176, The translation for the key
inviteLimitResetCycle in locales/ru-RU.yml incorrectly says users can create
unlimited invites; update the string to match locales/en-US.yml semantics so it
indicates the limit will reset to {limit} after {time} (e.g., "Через {time}
лимит будет сброшен до {limit}"), keeping the {time} and {limit} placeholders
intact.
locales/ru-RU.yml-1258-1260 (1)

1258-1260: ⚠️ Potential issue | 🟠 Major

Passkey の導線が別の認証手段になっています。

Line 1258 は signinWithPasskey なのに「пароль」と表示され、Line 1260 も Passkey 検証失敗の文言として不自然です。認証手段のラベルがずれるとログイン導線を誤らせます。

🛠️ 修正案
-signinWithPasskey: "Войдите в систему, используя свой пароль"
+signinWithPasskey: "Войти с помощью ключа доступа"

-passkeyVerificationFailed: "Ошибка проверка ключа доступа "
+passkeyVerificationFailed: "Не удалось проверить ключ доступа"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locales/ru-RU.yml` around lines 1258 - 1260, The Russian translations for
authentication labels are incorrect: update signinWithPasskey to mention passkey
(e.g., replace "Войдите в систему, используя свой пароль" with a phrase
indicating passkey/ключ доступа), change unknownWebAuthnKey to explicitly
reference WebAuthn (e.g., "Неизвестный ключ WebAuthn" or "Неизвестный
WebAuthn-ключ"), and fix passkeyVerificationFailed grammar to "Ошибка проверки
ключа доступа" (or similar natural Russian). Modify the values for
signinWithPasskey, unknownWebAuthnKey, and passkeyVerificationFailed in the
locales file to these corrected phrases.
packages/backend/src/boot/cli.ts-18-49 (1)

18-49: ⚠️ Potential issue | 🟠 Major

process.exit() 前に app.close() を通してください

このままだと Nest の application context を閉じずに終了するので、DB 接続や logger の flush がスキップされる可能性があります。process.exit() 直呼びではなく、終了コードを保持して finallyawait app.close() する形にした方が安全です。

修正案
 const app = await NestFactory.createApplicationContext(CommandModule, {
 	logger: new NestLogger(),
 });
 
 const commandService = app.get(CommandService);
 
 const command = process.argv[2] ?? 'help';
+let exitCode = 0;
 
-switch (command) {
-	case 'help': {
-		console.log('Available commands:');
-		console.log('  help - Displays this help message');
-		console.log('  reset-captcha - Resets the captcha');
-		break;
-	}
-	case 'ping': {
-		await commandService.ping();
-		break;
-	}
-	case 'reset-captcha': {
-		await commandService.resetCaptcha();
-		console.log('Captcha has been reset.');
-		break;
-	}
-	default: {
-		console.error(`Unrecognized command: ${command}`);
-		console.error('Use "help" to see available commands.');
-		process.exit(1);
-	}
+try {
+	switch (command) {
+		case 'help': {
+			console.log('Available commands:');
+			console.log('  help - Displays this help message');
+			console.log('  reset-captcha - Resets the captcha');
+			break;
+		}
+		case 'ping': {
+			await commandService.ping();
+			break;
+		}
+		case 'reset-captcha': {
+			await commandService.resetCaptcha();
+			console.log('Captcha has been reset.');
+			break;
+		}
+		default: {
+			console.error(`Unrecognized command: ${command}`);
+			console.error('Use "help" to see available commands.');
+			exitCode = 1;
+		}
+	}
+} finally {
+	await app.close();
 }
 
-process.exit(0);
+process.exitCode = exitCode;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/boot/cli.ts` around lines 18 - 49, The CLI currently
calls process.exit() directly and doesn't close the Nest application context;
refactor the top-level flow around the NestFactory.createApplicationContext(...)
and the switch on process.argv so you capture an exitCode (default 0), run the
command handlers (ping, resetCaptcha on commandService) inside try/catch to set
a non-zero exitCode on error, and in a finally block await app.close() before
calling process.exit(exitCode); reference the created app variable,
CommandModule/NestFactory.createApplicationContext, the commandService usage,
and the existing process.exit calls to locate where to change control flow.
packages/backend/migration/js/migration-config.js-16-23 (1)

16-23: ⚠️ Potential issue | 🟠 Major

空の YAML や null ドキュメントで TypeErrors が発生します

yaml.load()undefined または null を返す場合、ガード処理がないため config.disallowExternalApRedirect などへのアクセス時に即座に TypeError が発生します。このエラーは ?? 演算子の前段階で発生するため、デフォルト値に到達しません。オブジェクトに正規化してから各プロパティを読み取る必要があります。

修正案
 function loadConfigInternal() {
-	const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8'));
+	const loaded = yaml.load(fs.readFileSync(configYamlPath, 'utf-8'));
+	const config = loaded && typeof loaded === 'object' && !Array.isArray(loaded) ? loaded : {};
 
 	return {
 		disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false),
 		proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false),
 		signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true),
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/js/migration-config.js` around lines 16 - 23,
loadConfigInternal calls yaml.load(...) and then directly reads properties like
config.disallowExternalApRedirect, which throws when yaml.load returns
null/undefined; normalize the result first (e.g. const cfg = (config && typeof
config === 'object') ? config : {}) and then read
cfg.disallowExternalApRedirect, cfg.proxyRemoteFiles, cfg.signToActivityPubGet
so the null/undefined case falls back to the intended defaults.
packages/backend/migration/1745378064470-composite-note-index.js-15-20 (1)

15-20: ⚠️ Potential issue | 🟠 Major

並行モードでも DROP INDEX がブロッキングです

CREATE INDEX CONCURRENTLY にしても、既存 index の削除が通常の DROP INDEX のままだと、混雑時に note への書き込みを止める可能性があります。並行モードでは drop 側も CONCURRENTLY に揃えた方が狙いに合います。

さらに、down 側では既に mayConcurrently 変数が準備されているのに、DROP 時には使用されていません(CREATE 時には使われています)。

修正案
 	async up(queryRunner) {
 		const concurrently = isConcurrentIndexMigrationEnabled();
 
 		if (concurrently) {
 			const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
 			if (hasValidIndex.length === 0 || hasValidIndex[0].indisvalid !== true) {
-				await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
+				await queryRunner.query(`DROP INDEX CONCURRENTLY IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
 				await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
 			}
 		} else {
 			await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
@@
 	async down(queryRunner) {
 		const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
-		await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
+		await queryRunner.query(`DROP INDEX ${mayConcurrently} IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
 		await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/migration/1745378064470-composite-note-index.js` around
lines 15 - 20, 並行モードでも DROP をブロッキングな通常 DROP INDEX
にしているので、並行実行の意図に反します:concurrently フラグが true のときは queryRunner.query の DROP を
`DROP INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378"` に変更し、CREATE
と同様に並行オプションを使うようにしてください。また down 処理で既に用意されている mayConcurrently 変数を利用して DROP
の呼び出しを切り替える(mayConcurrently ? 'DROP INDEX CONCURRENTLY ...' : 'DROP INDEX
...')ように修正してください。

@mattyatea mattyatea changed the base branch from main to next March 21, 2026 12:55
@mattyatea mattyatea changed the base branch from next to beta March 21, 2026 13:05
@u1-liquid u1-liquid changed the base branch from beta to main March 21, 2026 21:56
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.