chore(llm/docs): .claude配下の再構成 (#17514)

* chore(docs): .claude配下の再構成

* fix AGENTS.md

* fix AGENTS.md

* fix review

* 行番号参照の除去

* docs: fix storybook note in vue reviewer agent

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix local review

* fix

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
おさむのひと 2026-06-03 09:03:10 +09:00 committed by GitHub
commit 2328ef3737
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 3463 additions and 1449 deletions

View file

@ -0,0 +1,10 @@
---
name: shipping-misskey-change
description: Use at every finish moment of a Misskey change, before committing, opening a PR, merging, or handing work back, especially when validation, SPDX, locale safety, migrations, misskey-js generation, or CHANGELOG checks may apply.
---
# shipping-misskey-change
This is the Codex entrypoint for the canonical Misskey pre-ship checklist.
Read and follow [.claude/skills/shipping-misskey-change/SKILL.md](../../../.claude/skills/shipping-misskey-change/SKILL.md). Treat that file and its `references/` directory as the source of truth.

View file

@ -0,0 +1,10 @@
---
name: working-on-backend
description: Use whenever editing or adding code under `packages/backend/`, including REST API endpoints, NestJS services/modules, TypeORM entities, migrations, backend tests, misskey-js generation, or backend validation commands.
---
# working-on-backend
This is the Codex entrypoint for the canonical Misskey backend skill.
Read and follow [.claude/skills/working-on-backend/SKILL.md](../../../.claude/skills/working-on-backend/SKILL.md). Treat that file and its `references/` directory as the source of truth.

View file

@ -0,0 +1,10 @@
---
name: working-on-frontend
description: Use whenever editing or adding code under `packages/frontend/`, Vue SFCs, SCSS Modules, Storybook stories, or frontend-facing UI text in `locales/ja-JP.yml`.
---
# working-on-frontend
This is the Codex entrypoint for the canonical Misskey frontend skill.
Read and follow [.claude/skills/working-on-frontend/SKILL.md](../../../.claude/skills/working-on-frontend/SKILL.md). Treat that file and its `references/` directory as the source of truth.

View file

@ -15,7 +15,7 @@
### 取り込んだファイル
| `.claude/` 内のパス | 上流パス | 上流 frontmatter `origin` | Misskey での改変 |
| `.claude/` 内のパス | 上流パス | 上流由来 | Misskey での改変 |
|---|---|---|---|
| `skills/context-budget/SKILL.md` | `skills/context-budget/SKILL.md` | ECC | description を日本語化、Misskey 固有メモを追記 |
| `commands/harness-audit.md` | `commands/harness-audit.md` | ECC | scripts 依存の自動採点を、Claude が `pnpm`/`git`/`grep` で手動採点する版に書き換え。Misskey 固有の評価軸 (SPDX / endpoint-list / migration / locales) を組み込み |
@ -61,7 +61,7 @@ Misskey 本体は **AGPL-3.0-only** で配布されているが、`.claude/` 配
- MIT が要求する条件 (copyright notice + license text の保持) を本ファイル + 各ファイル冒頭の SPDX/出典コメントで満たしている
- Misskey 全体の配布物としては AGPL-3.0-only で扱われるが、`.claude/` 配下の MIT ファイルは個別に MIT として識別可能
`.ts` / `.js` / `.vue` / `.scss` の SPDX 義務化 ([AGENTS.md §1](../AGENTS.md#1-spdx-ヘッダー必須)) は Misskey 本体コード向けで、`.claude/` 配下の `.md` / `.sh` には適用されない。
`.ts` / `.js` / `.vue` / `.scss` の SPDX 義務化 ([AGENTS.md](../AGENTS.md) の「絶対にやってはいけない事」§コード・データ関連) は Misskey 本体コード向けで、`.claude/` 配下の `.md` / `.sh` には適用されない。
---
@ -73,4 +73,4 @@ Misskey 本体は **AGPL-3.0-only** で配布されているが、`.claude/` 配
2. 各ファイル冒頭に SPDX ヘッダ + 出典コメントを追加
3. 本ファイル §1 のテーブルに 1 行追記
4. 必要なら新しいセクションでライセンス全文を同梱
5. AGENTS.md からの参照を確認 (現状の [AGENTS.md §ツール固有の補助ファイル](../AGENTS.md) で `THIRD_PARTY_LICENSES.md` を案内済。CLAUDE.md は `@AGENTS.md` 経由で読み込むので個別の追記は不要)
5. 本ファイルへの導線を確認 (`.claude/skills/README.md` / `.claude/commands/README.md` 等の各 README から本ファイルへリンクされている)。なお [CLAUDE.md](../CLAUDE.md) が `.claude/` 配下全体を「Claude Code 固有の補助」として案内しており本ファイルもそこに含まれる。CLAUDE.md は `@AGENTS.md` を取り込むだけなので AGENTS.md への個別追記は不要

View file

@ -2,22 +2,30 @@
Misskey の特定領域に特化したレビュー / 調査エージェントを `.claude/agents/<name>.md` 形式で配置する。
frontmatter (`name` + `description` + `tools`) は、Claude が **自動でエージェントを呼び出すか判断する** 唯一の手がかりになる。`description` には用途を具体的かつ網羅的に書くこと (動詞 + 対象 + トリガー条件)
frontmatter (`name` + `description` + `tools`) は、Claude が **自動でエージェントを呼び出すか判断する** 唯一の手がかりになる。`description` **起動判断に効くドメイン・パス・ファイル種別・固有チェックに絞って簡潔に** 書く (動詞 + 対象 + トリガー条件)。本文 checklist 項目を網羅的に列挙するのではなく、他の reviewer と区別できる高シグナル語を選ぶ
## 実装済サブエージェント
実装済エージェントの一覧は本ファイルでは管理しない (腐敗するため)。各 `<name>.md` の frontmatter が自己説明として機能する。
| エージェント名 | 役割 | 優先度 |
|---|---|---|
| [misskey-api-reviewer](misskey-api-reviewer.md) | NestJS DI + meta/paramDef + UUID 重複 + endpoint-list.ts 登録 + ApiError throw + misskey-js 再生成 + e2e + CHANGELOG をチェック | 高 (登録漏れで 404 / autogen CI 落ち頻発) |
| [vue-component-reviewer](vue-component-reviewer.md) | Mk\* 命名 / `<script lang="ts" setup>` / type-only defineProps / SCSS module / CSS 変数 / i18n.ts と i18n.tsx の使い分け / os.\* 経由 / a11y / `*.stories.impl.ts` 併設をチェック | 中 (CI 直撃は SPDX / locales 編集違反のみ。他は実害が出てから検出されるケースが多く API ほどの即死性はない) |
## 他のレビュー手段との使い分け
設計方針: `tools` を編集権限なし (Edit/Write を渡さない) に絞り、PR baseline (`git merge-base origin/develop HEAD`) との差分から自動的にレビュー対象を抽出する。
レビュー面を増やしすぎないよう、役割を分ける:
- **この `.claude/agents/` の 2 つ**: backend endpoint / Vue SFC の **Misskey 固有・機械的チェック** (endpoint-list 登録漏れ・misskey-js 再生成漏れ・ja-JP.yml 限定・SPDX 形式・Storybook 併設 等)。別コンテキストで差分を機械走査する価値がある領域に限定する
- **`pr-review-toolkit` プラグイン (code-reviewer / silent-failure-hunter 等)**: 言語非依存の一般的なコード品質・バグ・設計レビュー。Misskey 固有規約は見ない
- **`working-on-*` skill の checklist**: コードを **書いている最中** の自己チェック (レビュー専用ではなく実装ガイド)
Misskey 固有規約の機械チェックは本 agent、一般品質は pr-review-toolkit、実装中ガイドは skill、と棲み分ける。
## 構成方針
- `tools`**編集権限なし** (Edit/Write を渡さない) に絞り、PR baseline (`git merge-base origin/develop HEAD`) との差分から自動的にレビュー対象を抽出する設計
- 差分抽出は `git merge-base origin/develop HEAD` を baseline にする (PR / ブランチ全体を見るため)。`git diff HEAD` 単体は **未コミット差分しか取れず、コミット済の PR では空になって誤判定する** ので使わない
- `description` は呼び出し判断の手がかりであると同時に、(呼ばれなくても) Task ツール起動のたびに常時ロードされる。**他で代替できない高シグナルなトリガー語に絞って簡潔に** 書く (汎用 reviewer と被る語や冗長な列挙は context-budget 上の overhead になるだけで発見性に寄与しない)。健全性は [/harness-audit](../commands/harness-audit.md) / [context-budget skill](../skills/context-budget/SKILL.md) で確認できる
- 規約の **正本は `.claude/skills/*/references/` 側**。agent の checklist はその **派生コピー** (subagent が skill を読まなくても動くよう自己完結させる)。規約を変えるときは references を先に直し agent を追従させる ── 両者の食い違いは同期漏れなので references を正とする
## 新規エージェントを追加する場合
- `.claude/agents/<name>.md` に YAML frontmatter (`name` / `description` / `tools`) と本文 Markdown を書く。
- `description` は呼び出し判断に使われるため、対象ドメイン・主要チェック項目・トリガー条件を **具体的に** 列挙する。
- レビュー専門なら `tools: Read, Grep, Glob, Bash` に絞る (Edit/Write を渡さない)。**`Bash` は任意のシェルコマンドを実行できる強力な権限である点に注意**: レビュー用途では `git diff` / `git ls-files` / `grep` / `sed` 等の **読み取り系コマンドに限定して使う** こと。書き込み・削除・ネットワーク送信を伴う操作は本文中の例示・指示に含めないこと (エージェント本文がガードレールになる)。
- 主要参照ファイルへのリンクは、リポジトリルートからの相対パス (例: `../../packages/backend/...`) で貼る。絶対パスは contributor のホームディレクトリ依存になるので使わない。
- 差分抽出は `git merge-base origin/develop HEAD` を baseline にする (PR / ブランチ全体を見るため)。`git diff HEAD` 単体は **未コミット差分しか取れず、コミット済の PR では空になって誤判定する** ので使わない。
- 完成したらこの README の表にも 1 行追加する。
- `.claude/agents/<name>.md` に YAML frontmatter (`name` / `description` / `tools`) と本文 Markdown を書く
- `description` は呼び出し判断に使われるため、対象ドメイン・主要チェック項目・トリガー条件を挙げる。ただし常時ロードされるので **高シグナル語に絞って簡潔に** (構成方針の該当項目を参照)
- レビュー専門なら `tools: Read, Grep, Glob, Bash` に絞る (Edit/Write を渡さない)。**`Bash` は任意のシェルコマンドを実行できる強力な権限である点に注意**: レビュー用途では `git diff` / `git ls-files` / `grep` / `sed` 等の **読み取り系コマンドに限定して使う** こと。書き込み・削除・ネットワーク送信を伴う操作は本文中の例示・指示に含めないこと (エージェント本文がガードレールになる)
- 主要参照ファイルへのリンクは、各エージェント markdown からの相対パスで貼る (`../../packages/backend/...` のような形)。絶対パスは contributor のホームディレクトリ依存になるので使わない

View file

@ -1,12 +1,12 @@
---
name: misskey-api-reviewer
description: Misskey の API エンドポイント (packages/backend/src/server/api/endpoints/) の追加・変更を専門レビューする。SPDX / meta / paramDef / UUID 重複 / endpoint-list.ts 登録 / ApiError throw / misskey-js 再生成 / e2e / CHANGELOG を機械的にチェック。バックエンド API を追加・変更した PR レビューで呼び出す
description: Misskey backend の REST API エンドポイント (packages/backend/src/server/api/endpoints/) 追加・変更を機械レビューする。endpoint-list 登録漏れ・misskey-js 再生成漏れ・meta/paramDef/UUID/SPDX を検査。backend API を変更した PR レビューで呼ぶ
tools: Read, Grep, Glob, Bash
---
# Misskey API エンドポイントレビュアー
Misskey バックエンド (`packages/backend`) の REST API エンドポイント追加・変更 PR を機械的にレビューする専門エージェント。規約の根拠は [.claude/skills/add-api-endpoint/SKILL.md](../skills/add-api-endpoint/SKILL.md)
Misskey バックエンド (`packages/backend`) の REST API エンドポイント追加・変更 PR を機械的にレビューする専門エージェント。規約の **正本** は [.claude/skills/working-on-backend/references/tasks/adding-api-endpoint.md](../skills/working-on-backend/references/tasks/adding-api-endpoint.md) と [.claude/skills/working-on-backend/references/knowledge/api-meta-paramdef.md](../skills/working-on-backend/references/knowledge/api-meta-paramdef.md)。本エージェントはそれを review-mode から機械チェックする mirror。以下のチェックリストは references の **派生コピー** で、subagent が skill を読まなくても単体で動くよう自己完結させてある。規約を変えるときは **references を先に直し、本ファイルを追従させる** (正本は references。両者が食い違うのは同期漏れ)。個別のチェックで判断に迷ったら、該当する references ファイルを Read して確認してよい
## 役割
@ -119,7 +119,7 @@ BASE=$(git merge-base origin/develop HEAD)
git diff --name-only "$BASE"...HEAD -- packages/misskey-js/src/autogen/
```
差分ゼロなら `pnpm build-misskey-js-with-types` の実行漏れ。CI の `check-misskey-js-autogen` ジョブで必ず落ちるため Critical 扱い。
差分ゼロなら `pnpm build-misskey-js-with-types` の実行漏れ。CI の `check-misskey-js-autogen` ワークフローで必ず落ちるため Critical 扱い。
### 8. e2e テスト (Major)
@ -158,7 +158,9 @@ git diff --name-only "$BASE"...HEAD -- packages/misskey-js/src/autogen/
## 参照
- [.claude/skills/add-api-endpoint/SKILL.md](../skills/add-api-endpoint/SKILL.md) — 実装側の規約 (本エージェントの根拠)
- [.claude/skills/working-on-backend/references/tasks/adding-api-endpoint.md](../skills/working-on-backend/references/tasks/adding-api-endpoint.md) — 実装側の手順
- [.claude/skills/working-on-backend/references/knowledge/api-meta-paramdef.md](../skills/working-on-backend/references/knowledge/api-meta-paramdef.md) — meta / paramDef / res の完全早見表 + 落とし穴
- [.claude/skills/working-on-backend/references/knowledge/endpoint-list.md](../skills/working-on-backend/references/knowledge/endpoint-list.md) — endpoint-list.ts 登録ガイド
- [endpoints.ts (meta/paramDef 型定義)](../../packages/backend/src/server/api/endpoints.ts)
- [endpoint-list.ts (★ 登録先)](../../packages/backend/src/server/api/endpoint-list.ts)
- [endpoint-base.ts (Endpoint 基底クラス)](../../packages/backend/src/server/api/endpoint-base.ts)

View file

@ -1,12 +1,12 @@
---
name: vue-component-reviewer
description: Misskey フロントエンド (packages/frontend/src/components/ / pages/) の Vue 3 SFC 変更を専門レビューする。SPDX (HTML コメント) / Mk* 命名 / <script lang="ts" setup> / type-only defineProps / <style lang="scss" module> / CSS 変数 / i18n.ts と i18n.tsx の使い分け / os.* 経由 / a11y / Storybook (*.stories.impl.ts) を機械的にチェック。フロントエンドの .vue 変更を含む PR レビューで呼び出す
description: Misskey frontend の Vue 3 SFC (packages/frontend/src/components/ / pages/ の *.vue) 変更を機械レビューする。SPDX (HTML コメント)・Mk* 命名・i18n.ts/tsx・SCSS 変数・os.* 経由・a11y・Storybook 併設 (*.stories.impl.ts) を検査。frontend の .vue を変更した PR レビューで呼ぶ
tools: Read, Grep, Glob, Bash
---
# Misskey Vue コンポーネントレビュアー
Misskey フロントエンド (`packages/frontend`) の Vue 3 SFC 変更を機械的にレビューする専門エージェント。規約の根拠は [.claude/skills/add-mk-component/SKILL.md](../skills/add-mk-component/SKILL.md)
Misskey フロントエンド (`packages/frontend`) の Vue 3 SFC 変更を機械的にレビューする専門エージェント。規約の **正本** は [.claude/skills/working-on-frontend/references/tasks/adding-mk-component.md](../skills/working-on-frontend/references/tasks/adding-mk-component.md) および同 `references/knowledge/` 配下の各ファイル。本エージェントはそれを review-mode から機械チェックする mirror。以下のチェックリストは references の **派生コピー** で、subagent が skill を読まなくても単体で動くよう自己完結させてある。規約を変えるときは **references を先に直し、本ファイルを追従させる** (正本は references。両者が食い違うのは同期漏れ)。個別のチェックで判断に迷ったら、該当する references ファイルを Read して確認してよい
## 役割
@ -46,14 +46,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
```
`/* ... */` (TS 形式) は禁止。既存 SFC の慣習・SFC 先頭として自然な形式に統一するため (CI の `spdx` ジョブはコメント形式ではなく SPDX 文字列の有無のみを検査するため、形式が違っても CI は通るが、規約違反として指摘する)。
`/* ... */` (TS 形式) は禁止 (CI の `spdx` ジョブはコメント形式ではなく SPDX 文字列の有無のみを検査するため、形式が違っても CI は通るが、規約違反として指摘する)。形式の根拠は references/knowledge 側を参照。
### 2. 命名規約 (Major)
- 共有 / 再利用コンポーネント (`packages/frontend/src/components/` 配下、サブディレクトリ含む) は `Mk` プレフィックス必須 (例: `MkButton.vue`, `global/MkAvatar.vue`, `grid/MkGrid.vue`)。
- ページ固有のものは `pages/` 配下に置き、`Mk` プレフィックスは不要。
> `<script setup>` SFC は named export を持たないため、「ファイル名と export 名の一致」を機械的に検査することはできない。SFC のデフォルトエクスポートはコンパイラ生成なので、ファイル名規約のみを基準にする。
**補足:** `<script setup>` SFC は named export を持たないため、「ファイル名と export 名の一致」を機械的に検査することはできない。SFC のデフォルトエクスポートはコンパイラ生成なので、ファイル名規約のみを基準にする。
### 3. `<script>` タグ (Major)
@ -117,7 +117,7 @@ git diff "$BASE"...HEAD -- 'packages/frontend/src/**/*.vue' \
### 8. Storybook 併設 (Major)
- 共有 `Mk*` コンポーネントを新規追加した場合、`Mk<Name>.stories.impl.ts` が同階層に併設されているか (サブディレクトリ含む。例: `components/global/MkAvatar.stories.impl.ts`, `components/grid/MkGrid.stories.impl.ts`)。
- **ファイル名は `.stories.impl.ts` 固定** (`.stories.ts`誤り)。
- **ファイル名は `.stories.impl.ts` 固定** (`.stories.ts`生成物なので手編集・コミット不可)。
- 既存 [MkButton.stories.impl.ts](../../packages/frontend/src/components/MkButton.stories.impl.ts) を雛形例として参照する。
検出 (新規追加された `Mk*.vue` をサブディレクトリ含めて拾う):
@ -167,8 +167,10 @@ git diff --name-only --diff-filter=A "$BASE"...HEAD -- \
## 参照
- [.claude/skills/add-mk-component/SKILL.md](../skills/add-mk-component/SKILL.md) — 実装側の規約 (本エージェントの根拠)
- [.claude/skills/add-i18n-key/SKILL.md](../skills/add-i18n-key/SKILL.md) — i18n キー追加のルール
- [.claude/skills/working-on-frontend/references/tasks/adding-mk-component.md](../skills/working-on-frontend/references/tasks/adding-mk-component.md) — 実装側の手順
- [.claude/skills/working-on-frontend/references/tasks/adding-i18n-key.md](../skills/working-on-frontend/references/tasks/adding-i18n-key.md) — i18n キー追加のルール
- [.claude/skills/working-on-frontend/references/knowledge/component-conventions.md](../skills/working-on-frontend/references/knowledge/component-conventions.md) — SFC 規約・a11y チェックリスト
- [.claude/skills/working-on-frontend/references/knowledge/scss-modules.md](../skills/working-on-frontend/references/knowledge/scss-modules.md) — SCSS Modules / CSS 変数
- [os.ts](../../packages/frontend/src/os.ts) — UI 操作 API
- [MkButton.vue](../../packages/frontend/src/components/MkButton.vue)
- [MkInput.vue](../../packages/frontend/src/components/MkInput.vue) — generic SFC 例

View file

@ -2,37 +2,17 @@
Misskey 開発で繰り返し使うワークフローを `/command-name` で呼び出せるよう、`.claude/commands/<name>.md` 形式で配置している。
## 実装済コマンド
実装済コマンドの一覧は本ファイルでは管理しない (腐敗するため)。各 `<name>.md` の frontmatter (`description`) が自己説明として機能する。
### Misskey オリジナル
現状残っているのは ECC ([everything-claude-code](https://github.com/affaan-m/everything-claude-code)) 由来の MIT ライセンスコマンドのみで、Misskey 固有のスラッシュコマンドは廃止して `.claude/skills/` 配下のスキルに統合した。MIT 出典は [.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) を参照。
| コマンド | 用途 | 典型ユースケース |
| --- | --- | --- |
| [`/check-misskey-js`](./check-misskey-js.md) | `pnpm build-misskey-js-with-types` を走らせ、`packages/misskey-js/src/autogen/` の差分を報告 | backend の API endpoint を追加・変更した直後 |
| [`/changelog-add`](./changelog-add.md) | `CHANGELOG.md``## Unreleased` 配下、対応するサブセクションに 1 行追記 | ユーザー影響のある変更をコミットする直前 |
| [`/migrate-new`](./migrate-new.md) | TypeORM `migration:create` の薄いラッパー (拡張子変換 + SPDX 付与 + `check-migrations` で pending DDL 検出) | 手書き SQL / データ移行用に空雛形が欲しい時 |
## 設計方針
### ECC ([everything-claude-code](https://github.com/affaan-m/everything-claude-code)) 由来 (MIT)
- Misskey 固有のワークフローは原則 `.claude/skills/` に統合する (description で自動索引されるため。コマンドはユーザーが `/name` でタイプしないと起動しない)
- 既存の `superpowers` / `pr-review-toolkit` などのプラグイン提供スラッシュコマンドで足りる場合は新規追加しない
ECC の MIT ライセンスファイルを Misskey の規約に合わせて再構成したもの。出典は [.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) を参照。
## 新規コマンドを追加する場合 (どうしてもスキルでは表現できない時のみ)
| コマンド | 用途 | 典型ユースケース |
| --- | --- | --- |
| [`/quality-gate`](./quality-gate.md) | `pnpm lint` + 各パッケージの unit test を順次実行する軽量品質ゲート | 完了前の軽量チェック (重い E2E は CI 側に委譲) |
| [`/harness-audit`](./harness-audit.md) | `.claude/` ハーネスを 7 カテゴリで採点し改善優先度を提示 | 設定の点検 / 新しい skill / agent / hook を入れた後 |
## 使い分け
- **`/migrate-new` vs [`create-migration` skill](../skills/create-migration/SKILL.md)**:
- 雛形だけ素早く欲しい → `/migrate-new`
- エンティティ差分から自動生成、または CONCURRENTLY などの注意点を含めて完全に誘導してほしい → `create-migration` skill (`migration:generate`)
- **`/changelog-add` vs 手動編集**:
- サブセクションの placeholder `-` 置換や、過去リリースセクションへの誤編集を避けるため、原則コマンドを使う。
- **`/quality-gate` のスコープ**:
- 編集途中の軽量チェック (lint + unit test) は `/quality-gate` で十分。重い e2e / federation / Cypress は CI 側で実行されるため、ローカルでは原則回さない。
## 新規追加時の方針
- 既存の `superpowers` / `pr-review-toolkit` などのプラグイン提供スラッシュコマンドで足りる場合は新規追加しない。
- frontmatter には最低限 `description` を指定し、引数を取るなら `argument-hint`、可能なら `allowed-tools` も指定する (permission prompt を最小化するため)。
- 長時間ビルド (2 分超) を伴うコマンドはインライン `` !`<cmd>` `` を使わず、本文で `Bash` ツール呼び出し時の `timeout` を指示する。
- frontmatter には最低限 `description` を指定する。引数を取るなら `argument-hint`、可能なら `allowed-tools` も指定する (permission prompt を最小化するため)
- 長時間ビルド (2 分超) を伴うコマンドはインライン `` !`<cmd>` `` を使わず、本文で `Bash` ツール呼び出し時の `timeout` を指示する
- 主要参照ファイルへのリンクは、各コマンド markdown からの相対パスで貼る。絶対パスは contributor のホームディレクトリ依存になるので使わない

View file

@ -1,49 +0,0 @@
---
description: CHANGELOG.md の Unreleased セクションに 1 行追記する
argument-hint: <general|client|server> <Prefix>: <description>
allowed-tools: Bash(awk:*), Bash(git diff:*), Read, Edit
---
## 引数
引数: `$ARGUMENTS`
## 現在の Unreleased セクション
!`awk '/^## Unreleased/,/^## [0-9]/' CHANGELOG.md`
## タスク
1. **引数の解析**
`$ARGUMENTS` を以下の形式として解釈する:
- 第 1 トークン: scope = `general` / `client` / `server` のいずれか (case-insensitive)
- 残り: エントリ本文。`Enhance:` / `Fix:` / `Feat:` のいずれかで始まる前提
- 不正な scope や、Prefix が見当たらない場合はエラー終了し、ユーザーに `argument-hint` の書式を提示する
scope は次のように見出しに変換する: `general``### General` / `client``### Client` / `server``### Server`
2. **対象サブセクションの状態判定**
上の context (現在の Unreleased セクション) を見て、対象サブセクションが以下のどちらかを判定する:
- **空 (placeholder のみ)**: 見出し直下に `-` 単独行のみがある状態
- **既存エントリあり**: `- Enhance: ...` / `- Fix: ...` / `- Feat: ...` の行が 1 つ以上ある状態
3. **CHANGELOG.md の編集**
`Read` で CHANGELOG.md 全体を確認した後、`Edit` ツールで以下のように更新する:
- **空の場合**: 該当サブセクションの placeholder `-` 行を `- <整形済みエントリ>` で置換する。例: `### General\n-\n``### General\n- Enhance: 新しい機能\n`
- **既存ありの場合**: 既存エントリ群の **末尾** (次の空行直前) に新エントリを **append** する。順序入れ替えはしない。
`Edit``old_string` には、置換対象のサブセクション付近のユニークな文脈 (見出し + 直後の数行) を含め、誤マッチを防ぐ。
4. **不可侵の徹底**
- `## Unreleased` 以下の対象サブセクションのみ編集する。
- `## 2026.x.x` 以下の過去リリースセクションは絶対に変更しない ([AGENTS.md §CHANGELOG](../../AGENTS.md#changelog) 参照)。
5. **結果確認**
`git diff CHANGELOG.md` を実行し、想定通り 1 行のみ追加されていることを表示して、ユーザーに確認させる。
## 例
- `/changelog-add server Fix: 通知が遅延する問題を修正``### Server` 末尾に追記
- `/changelog-add client Enhance: ノートの表示を改善``### Client` 末尾に追記
- `/changelog-add general Feat: 新機能の追加``### General` 末尾に追記 (placeholder 置換)

View file

@ -1,42 +0,0 @@
---
description: backend の API 変更後に misskey-js を再生成し、生成物の差分を報告する
allowed-tools: Bash(pnpm build-misskey-js-with-types:*), Bash(git status:*), Bash(git diff:*), Bash(git branch:*)
---
## 概要
backend の API endpoint やスキーマを変更した後、`packages/misskey-js/src/autogen/` の自動生成型を最新化するためのコマンド。内部で `pnpm build-misskey-js-with-types` (backend build → `api.json` 生成 → misskey-js 型生成 → ビルド → API extractor) を一括実行する。
## 現在の状態 (再生成前)
- 現ブランチ: !`git branch --show-current`
- 既存の misskey-js 関連変更: !`git status --short -- packages/misskey-js/`
## タスク
以下の手順を順番に実行してください。
1. **再生成の実行**
`Bash` ツールで以下のコマンドを `timeout: 600000` (10 分) を指定して実行する。内部で backend ビルドと型再生成を行うため、デフォルトの 2 分タイムアウトでは不足する。
```bash
pnpm build-misskey-js-with-types
```
2. **差分の確認**
完了後、以下を実行して `packages/misskey-js/src/autogen/` の差分を確認する (`built/``.gitignore` 対象なので追跡対象外):
```bash
git status --short -- packages/misskey-js/
git diff --stat -- packages/misskey-js/src/autogen/
```
3. **結果報告**
- **差分なし** → 「backend の変更は misskey-js の公開型に影響していません」と報告する。追加コミットは不要。
- **差分あり** → 変更ファイル一覧をユーザーに示し、`git add packages/misskey-js/src/autogen/` で再生成物もコミット対象に含めるよう案内する。`api.json` の差分が大きい場合は、API endpoint 側の `meta` / `paramDef` / `res` 定義を確認するよう促す。
## 注意
- このコマンドは **backend 編集後の確認** が目的。backend を変更していないのに走らせると、ビルドキャッシュ次第で no-op になる。
- 実行中は `packages/backend/built/``packages/misskey-js/built/` などの中間生成物が更新されるが、これらは `.gitignore` 対象。
- 生成物以外 (`packages/misskey-js/src/` のうち `autogen/` 以外) に予期せぬ差分が出た場合は、ローカルの編集が混入している可能性があるため、一旦中止して原因を調査する。

View file

@ -34,8 +34,8 @@ Misskey リポジトリの `.claude/` 構成を 7 カテゴリで採点し、改
| 1 | Tool Coverage | skill / agent / command の数、欠けているワークフロー段、重複なし |
| 2 | Context Efficiency | frontmatter description の冗長度、SKILL.md の長さ分布、重複情報、CLAUDE.md の肥大化 |
| 3 | Quality Gates | Stop / PreToolUse / PostToolUse hook の整備、`/quality-gate` 等の完了前ゲートの有無、自動 lint/typecheck |
| 4 | Memory Persistence | docs/* の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する |
| 5 | Eval Coverage | testing.md の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド |
| 4 | Memory Persistence | `.claude/skills/*/SKILL.md``references/` の同期状態を評価。プロジェクト側 `.claude/memory/` は未採用方針 (auto-memory はユーザーホーム側で自動運用) のため、ここを採点起点にせず既定 5/10 から開始する |
| 5 | Eval Coverage | `working-on-backend` / `working-on-frontend` の testing リファレンス (backend-testing.md / frontend-testing.md) の網羅、Misskey 固有の e2e/fed/Storybook/Cypress 適用ガイド |
| 6 | Security Guardrails | SPDX 規約適用、migration 不変性ルール、ja-JP.yml 限定編集ルール、secrets 検出 |
| 7 | Cost Efficiency | enabledPlugins の重複・過剰、context-budget の整備、MCP 過剰登録なし |
@ -111,7 +111,7 @@ Tool Coverage: 9/10 (skills 5, agents 2, commands 5 — 偏りなし)
Context Efficiency: 8/10 (description 平均 3-5 行、肥大なし)
Quality Gates: 5/10 (Stop hook 共有設定に未登録 / `/quality-gate` あり)
Memory Persistence: 5/10 (プロジェクト側 memory/ 未採用方針 = 既定値)
Eval Coverage: 7/10 (testing.md 網羅、Storybook 一部抜け)
Eval Coverage: 7/10 (backend/frontend testing リファレンス網羅、Storybook 一部抜け)
Security Guardrails: 10/10 (SPDX 100%, locales OK, migrations clean)
Cost Efficiency: 8/10 (context-budget 導入済 / MCP 0)
@ -126,7 +126,7 @@ Top 3 Actions:
2) [Quality Gates] backend の console.log 3 件を logger に置換。
git grep "console\.log" packages/backend/src
3) [Cost Efficiency] enabledPlugins から未使用のものを外す。
.claude/docs/plugins.md と照合。
`.claude/settings.json``enabledPlugins` と実プロジェクト利用状況を照合。
Suggested next skills to apply:
- /quality-gate で完了前に lint + unit test を回す

View file

@ -1,81 +0,0 @@
---
description: TypeORM migration の空雛形を生成する。スキーマ差分から自動生成したい時は create-migration skill を使うこと
argument-hint: <PascalCaseName>
allowed-tools: Bash(pnpm:*), Bash(ls:*), Bash(test:*), Bash(head:*), Read, Edit
---
## 引数
引数: `$ARGUMENTS`
## タスク
1. **PascalCaseName の検証**
`$ARGUMENTS``^[A-Z][A-Za-z0-9]+$` に一致するか確認する。一致しない場合はエラー終了し、`AddFooBar` / `BirthdayIndex` のような形式を案内する。
2. **既存ファイルの存在確認**
```bash
ls packages/backend/migration/*$ARGUMENTS.{js,ts} 2>/dev/null
```
既に同名 (タイムスタンプ違い) のファイルが存在する場合、上書きせずユーザーに別名を促す。
3. **TypeORM 公式 CLI で空雛形を生成 (`-o --esm` 必須)**
`create-migration` skill の方針に従い、`Date.now()` を手書きするのではなく TypeORM CLI を使う。`-o --esm`**最初から JS(ESM) を生成** させ、後続の `.ts → .js` 変換や `import { MigrationInterface }` 削除といった TS 固有構文の除去を不要にする (`-o --esm` を付けないと `.ts` + CommonJS / `implements MigrationInterface` 付きで生成され、Misskey の `ormconfig.js` (`migration/*.js` のみロード) と既存 migration スタイルに合わない):
```bash
pnpm --filter backend exec typeorm migration:create -o --esm migration/$ARGUMENTS
```
出力: `packages/backend/migration/<UnixMs>-<PascalCaseName>.js`
4. **生成ファイルパスの取得**
後続ステップで使うパスを変数に受ける (`<ms>` を手書きしない):
```bash
dst=$(ls -t packages/backend/migration/*$ARGUMENTS.js | head -1)
```
以降のステップでは `$dst` を編集対象として扱う。完成後の典型的な形は次のようになる (参考: [packages/backend/migration/1767169026317-birthday-index.js](../../packages/backend/migration/1767169026317-birthday-index.js)):
```js
export class <PascalCaseName><ms> {
name = '<PascalCaseName><ms>'
async up(queryRunner) {
}
async down(queryRunner) {
}
}
```
5. **SPDX ヘッダーの追加**
`Edit` ツールで、ファイル冒頭に以下を挿入する。CI の `spdx` ジョブが失敗するため必須:
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
6. **migration の pending DDL 検査**
```bash
pnpm --filter backend check-migrations
```
TypeORM schema builder で pending DDL を検出する検査 ([scripts/check_migrations_clean.js](../../packages/backend/scripts/check_migrations_clean.js))。空雛形を作っただけの段階ではエンティティ差分との不整合が残る場合があるため、`up`/`down` を埋めた後にも再実行して 0 件になるか確認する。
7. **結果報告**
- 生成ファイルパスを示す。
- `up()` / `down()` の中身が空であることを伝え、SQL を書く必要があると案内する。
- `down()` を空のまま放置すると本番ロールバック時に詰むため、必ず `up` の完全な巻き戻しを実装するよう促す。
- 詳細な手順 (`migration:generate` を使うべきケース、CONCURRENTLY などの注意点) は `create-migration` skill を参照するよう案内する。
## 注意
- このコマンドは **空雛形を素早く出して手書きする** 用途。エンティティ (`packages/backend/src/models/*.ts`) を変更した差分から SQL を自動生成したい場合は、このコマンドではなく `create-migration` skill 経由で `migration:generate` を使うこと。
- マージ済み migration ファイルは絶対に編集しない ([AGENTS.md §3](../../AGENTS.md#3-マージ済み-migration-を絶対に編集しない))。

View file

@ -112,8 +112,9 @@ Frontend ut: PASS (87/87)
## 関連 skill / コマンド
- `/check-misskey-js` コマンド — API 変更時の misskey-js 再生成
- [AGENTS.md §必須コマンド](../../AGENTS.md#必須コマンド) — pnpm コマンド一覧の正典
- [`shipping-misskey-change` スキル](../skills/shipping-misskey-change/SKILL.md) — commit / PR 直前の最終チェックリスト (misskey-js 再生成 / SPDX / CHANGELOG 等)
- [`shipping-misskey-change/references/tasks/regenerate-misskey-js.md`](../skills/shipping-misskey-change/references/tasks/regenerate-misskey-js.md) — API 変更時の `pnpm build-misskey-js-with-types` 実行手順
- [.github/copilot-instructions.md §Validation コマンド](../../.github/copilot-instructions.md) — pnpm コマンド一覧 (Copilot / Codex 向けに再掲)
## 元 ECC 版との差分

View file

@ -1,18 +0,0 @@
# Misskey Claude Code 補助ドキュメント
ルート `CLAUDE.md` には書かれていないが、開発時に参照すると便利な情報を分野別にまとめている。**Claude は必要になったタイミングで該当ファイルを Read すれば良い** (auto-load しない)。
## 索引
| ファイル | いつ読むか |
|---|---|
| [architecture.md](./architecture.md) | パッケージ構成・ビルド構造を把握したい時 / 新パッケージを跨ぐ変更を計画する時 |
| [backend.md](./backend.md) | `packages/backend` を編集する時 (NestJS / TypeORM / API endpoint / migration) |
| [frontend.md](./frontend.md) | `packages/frontend` を編集する時 (Vue 3 / Mk* / i18n / SCSS Modules / `os.ts`) |
| [testing.md](./testing.md) | テストを書く・走らせる時 (Vitest 構成、Cypress、Storybook) |
| [plugins.md](./plugins.md) | 有効化済の Claude Code プラグインの用途を確認したい時 |
## 補足: ルール vs ドキュメント
- 事故直結ルール (SPDX / locales / migration) と必須コマンド・CHANGELOG 書式は、リポジトリルートの [AGENTS.md](../../AGENTS.md) に集約されている。Claude Code は CLAUDE.md からの `@AGENTS.md` で常時コンテキストに乗せる。Codex / Copilot も同じファイルを読む。
- `.claude/docs/*.md` (このディレクトリ) は **オンデマンド参照**。Claude が「知っておいた方が良いが常に持つ必要はない」内容をここに置く。

View file

@ -1,47 +0,0 @@
# アーキテクチャ概要
## モノレポ構成 (pnpm workspaces)
pnpm workspace の正は [pnpm-workspace.yaml](../../pnpm-workspace.yaml) で、以下 11 パッケージと、`packages/misskey-js` 内の sub-workspace `packages/misskey-js/generator` (型生成用の内部ジェネレータ。直接編集しない) で構成される。`package.json``workspaces` 配列も併記しているが、実体は pnpm-workspace.yaml が読まれる:
| パッケージ | 役割 |
|---|---|
| `packages/backend` | NestJS 11 + Fastify 5 + TypeORM 0.3 (PostgreSQL) + Redis。HTTP/WebSocket/ActivityPub サーバー本体。 |
| `packages/frontend` | Vue 3.5 + Vite。Web クライアント本体。 |
| `packages/frontend-embed` | 埋め込み専用ビュー (ノート単体プレビュー等)。 |
| `packages/frontend-shared` | frontend と frontend-embed で共有するユーティリティ・コンポーネント。 |
| `packages/frontend-builder` | フロントエンドビルド支援 (Vite plugin など)。 |
| `packages/sw` | Service Worker。 |
| `packages/misskey-js` | JS/TS クライアント SDK (MIT サブパッケージ)。`src/autogen/` 配下のみ backend の OpenAPI から `pnpm build-misskey-js-with-types` で自動生成され、それ以外 (`src/index.ts` / `src/api.ts` 等) は手書き保守する。autogen 配下を直接編集しないこと。 |
| `packages/misskey-reversi` | 内蔵リバーシゲームのロジック。 |
| `packages/misskey-bubble-game` | 内蔵バブルゲームのロジック。 |
| `packages/i18n` | locales 読み込み/型生成のサポート。 |
| `packages/icons-subsetter` | アイコンのサブセット化ツール。 |
その他に `packages/shared` (workspaces には含まれないが共有ファイル置き場) もある。
## 重要な依存関係
```
frontend ── misskey-js (auto-generated) ── backend (OpenAPI)
└── frontend-embed, sw も依存
```
- backend の API (meta / paramDef / response) を変更したら **必ず** `pnpm build-misskey-js-with-types` を実行し、misskey-js の生成物を更新する。忘れると CI の `check-misskey-js-autogen` ジョブが落ちる。
## ビルドツール
- **Backend**: `rolldown` (Rust 製・Rollup 互換 API のバンドラ) でバンドル。型チェックは `tsgo` (TypeScript native preview)。
- **Frontend**: Vite。型チェックは `vue-tsc`
- **Lint**: ESLint 9 (Flat Config) + `@misskey-dev/eslint-plugin`
## 国際化
- `locales/` 直下に 40 言語の YAML (ja-JP.yml + 他 39 言語)。
- **`ja-JP.yml` のみ手動編集可** (Crowdin 経由で他言語へ自動配信)。
- フロントエンドからの参照は引数なしか引数ありかで使い分ける。詳細は [frontend.md](./frontend.md#国際化-i18n)。
## ライセンス
リポジトリ本体は AGPL-3.0-only。**AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリ** の新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルには冒頭に SPDX ヘッダー必須。`packages/misskey-js` は MIT サブパッケージなので AGPL ヘッダーを一律に付けない。条件と除外の詳細は [AGENTS.md §1](../../AGENTS.md#1-spdx-ヘッダー必須) 参照。

View file

@ -1,124 +0,0 @@
# Backend (`packages/backend`) 規約
NestJS 11 + Fastify 5 + TypeORM 0.3 (PostgreSQL) + Redis。
## アーキテクチャ
- **DI コンテナ**: NestJS の `@Injectable()` サービス + Repository (TypeORM) パターン。
- **DI トークン**: `@/di-symbols.js``DI` から `@Inject(DI.xxx)` で注入。
- **ビルド**: `rolldown -c``built/` にバンドル。型チェックは `tsgo`
## API エンドポイント
### 配置
`packages/backend/src/server/api/endpoints/<category>/<name>.ts` (一部はトップ直下)。
### 三点セット (`endpoints/ping.ts` 参照)
各エンドポイントファイルは以下の 3 つを export する:
```ts
export const meta = {
tags: ['<tag>'],
requireCredential: true, // または false (必ず明示)
requireModerator: false, // 必要なら true
kind: 'read:account', // OAuth scope
res: {
type: 'object',
optional: false, nullable: false,
properties: { /* ... */ },
},
errors: {
sampleError: {
message: 'Sample error message.',
code: 'SAMPLE_ERROR',
id: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', // UUID v4 (`x`=hex, `y`=8/9/a/b)。`crypto.randomUUID()` で生成し、他エンドポイントと重複させない
},
},
} as const;
export const paramDef = {
type: 'object',
properties: { /* JSON Schema */ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
// @Inject(DI.xxx) private xxxRepository: XxxRepository,
) {
super(meta, paramDef, async (ps, me) => {
// 実装。エラーは throw new ApiError(meta.errors.xxx);
});
}
}
```
### 注意点
- **公開 API エラーとしてクライアントに返したいものは `throw new ApiError(meta.errors.<key>)` を使う**`meta.errors` に列挙して `ApiError` でラップしないと misskey-js 側の型に出ず、レスポンスも 500 になる。
- 一方で **想定外の例外 (DB 不整合 / 下層サービスの bug 等) は握り潰さず再 throw する**。既存 endpoint も「期待される業務エラーは `ApiError` に変換し、それ以外は `throw err;` で再 throw」の二段構え (例: [`endpoints/i/pin.ts`](../../packages/backend/src/server/api/endpoints/i/pin.ts) の `catch` 節)。生 `throw` を全面禁止すると未知例外が 200 で潰れて debug 困難になる。
- `meta.errors.<key>.id`**UUID** 形式。新規追加時は他エンドポイントと重複しないよう確認する。
- `requireCredential``true` / `false` を必ず明示する。
- 新規エンドポイント追加後は **`pnpm build-misskey-js-with-types`** を実行する (`misskey-js` の自動生成ファイルを更新)。
### ルート登録
エンドポイントは **glob 自動収集されない**。新規ファイルを `endpoints/<category>/<name>.ts` に置いただけでは API ルーティングに乗らず、404 になる。`packages/backend/src/server/api/endpoint-list.ts` にアルファベット順で 1 行追加するのが必須:
```ts
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
```
`EndpointsModule.ts` がこのファイルの全エクスポートを `Object.entries()` で反復し、NestJS の provider (`provide: 'ep:<path>'`) を生成する。詳細は [.claude/skills/add-api-endpoint/SKILL.md](../skills/add-api-endpoint/SKILL.md) のステップ 4 を参照。
## モデル / Repository
- エンティティ: `packages/backend/src/models/<Name>.ts` (`@Entity` + `@Column`)。
- DI 経由で注入される Repository を経由してアクセス。
## Migration
詳細手順 (手書き方式 = AGENTS.md §3 と整合):
> エンティティ差分からの自動生成や `CREATE INDEX CONCURRENTLY` 等のオプションを使いたい場合は [.claude/skills/create-migration/SKILL.md](../skills/create-migration/SKILL.md) の TypeORM CLI 手順を使う。手書き / CLI どちらでも `check-migrations` (pending DDL 検出) さえ通せば等価。
1. **タイムスタンプ取得**: `node -e "console.log(Date.now())"`
2. **ファイル名**: `packages/backend/migration/{timestamp}-{PascalCaseName}.js` (拡張子は `.js`)
3. **雛形**:
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PascalCaseName1234567890123 {
name = 'PascalCaseName1234567890123'
async up(queryRunner) {
// 前進マイグレーション
}
async down(queryRunner) {
// up を完全に巻き戻す
}
}
```
4. **検証**:
- `pnpm --filter backend check-migrations` (TypeORM schema builder で pending DDL を検出する。エンティティと migration の不一致が残っているとここで非ゼロ終了する。実体は [scripts/check_migrations_clean.js](../../packages/backend/scripts/check_migrations_clean.js))
- `pnpm migrate` (ローカル DB に適用)
- `pnpm revert` (ロールバック確認)
5. **エンティティとの整合性**: 関連する `src/models/*.ts``@Column` / `@Entity` も同時に更新する。
> マージ済み migration の編集は **絶対禁止** ([AGENTS.md §3](../../AGENTS.md#3-マージ済み-migration-を絶対に編集しない))。
## テスト
- Unit: `pnpm --filter backend test` (`vitest.config.unit.ts`)
- E2E: `pnpm --filter backend test:e2e` (`vitest.config.e2e.ts`)
- Federation: `pnpm --filter backend test:fed` (`vitest.config.fed.ts`)
- 配置: `packages/backend/test/` 配下。

View file

@ -1,76 +0,0 @@
# Frontend (`packages/frontend`) 規約
Vue 3.5 + Vite + Storybook + Cypress E2E。
## コンポーネント命名
- 共有 / 再利用コンポーネントは **`Mk` プレフィックス** (例: `MkButton.vue`, `MkInput.vue`, `MkAbuseReport.vue`)。
- ページ単位のものは `packages/frontend/src/pages/` 配下に置く。
## SFC スタイル
Composition API + `<script setup lang="ts">` を基本とする (Options API は新規導入しない)。型宣言や module スコープのユーティリティを置きたい時は、setup ブロックと**併用**する形で追加の `<script lang="ts">` ブロックを置いて構わない (例: [`MkInput.vue`](../../packages/frontend/src/components/MkInput.vue) は `SupportedTypes` 型を別ブロックで宣言してから setup を書いている)。SCSS は **CSS Modules** で書き、`<style lang="scss" module>` を使う:
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<!-- ... -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// ...
</script>
<style lang="scss" module>
.root {
/* ... */
}
</style>
```
## 国際化 (i18n)
- 文字列リテラルを直書きしない。
- 引数なし: `i18n.ts.<path>` で参照する (例: `i18n.ts.deleted`)。
- 引数あり: `i18n.tsx.<path>(...)` で関数呼び出しする (例: `i18n.tsx.takeOverConfirm({ name })`)。
- 新規キーは **`locales/ja-JP.yml` のみ** に追加する (他言語は Crowdin で自動配信)。
- `i18n``packages/frontend/src/i18n.ts` (または共有モジュール) から import する。
## モーダル / 通知
- `os.ts` (`packages/frontend/src/os.ts`) 経由で呼ぶ。
- `os.alert(...)` / `os.confirm(...)` / `os.popup(...)` / `os.success(...)` など。
- ブラウザ標準の `window.alert()` / `window.confirm()`**直接呼ばない**
## アクセシビリティ (PR レビューで指摘されやすい点)
- クリックハンドラを付けるなら `<button>` を使うか、`role="button"` + `tabindex` を付ける。
- フォーム要素には `<label>` または `aria-label` を付ける。
- キーボード操作可能であること。
## Storybook
新規共有コンポーネントには `<ComponentName>.stories.impl.ts` を併設するのが慣習 (`MkButton.stories.impl.ts` 等の例多数)。
```bash
pnpm --filter frontend storybook-dev # localhost:6006
```
## ビルド・開発
- 開発: `pnpm dev` (ルート) で backend + frontend が watch で立ち上がる。
- ビルド: `pnpm --filter frontend build`
- 型チェック: `pnpm --filter frontend typecheck` (vue-tsc)
- ESLint: `pnpm --filter frontend eslint`
## テスト
- Unit (Vitest): `pnpm --filter frontend test`
- Cypress E2E: `pnpm e2e` (ルートから; `start-server-and-test` で起動)

View file

@ -1,28 +0,0 @@
# 有効化済 Claude Code プラグイン
`.claude/settings.json` で 14 プラグインが有効化されている。それぞれの典型的な利用シーンを 1 行で示す。
| プラグイン | 用途 |
| --- | --- |
| `frontend-design` | UI コンポーネント / ページの設計・デザイン作業 (Vue 3 編集に有効) |
| `superpowers` | TDD・debugging・brainstorming・planning 等のメタスキル群 |
| `context7` | OSS ドキュメントの取得 (Vue 3, NestJS, TypeORM, Vitest 等) — 訓練データの古さを補う |
| `code-review` | コードレビュー (`/code-review`) |
| `code-simplifier` | コード整理 (`code-simplifier:code-simplifier` サブエージェント経由) |
| `github` | GitHub PR / Issue 操作 (gh ベースだが補助コマンドあり) |
| `skill-creator` | 新スキルの作成・改善・評価 |
| `feature-dev` | 機能開発ガイド (`/feature-dev:feature-dev` / 内部に `code-architect` / `code-explorer` / `code-reviewer` サブエージェント) |
| `claude-md-management` | CLAUDE.md の作成・改善 (`/claude-md-management:revise-claude-md` / `claude-md-improver` エージェント) |
| `typescript-lsp` | TypeScript LSP 連携 (型情報を活用) |
| `security-guidance` | セキュリティレビュー (`/security-review`) |
| `pr-review-toolkit` | PR レビュー一式。サブエージェント: `code-reviewer` / `code-simplifier` / `comment-analyzer` / `pr-test-analyzer` / `silent-failure-hunter` / `type-design-analyzer` |
| `claude-code-setup` | Claude Code 自動化セットアップ提案 |
| `playwright` | ブラウザ自動操作 (フロントエンド動作確認時に有用) |
## 使い分けの指針
- **API 関連の調査**: `context7` で対象ライブラリのドキュメントを取得 → 編集。
- **PR 作成前**: `pr-review-toolkit` の各エージェント (code-reviewer / silent-failure-hunter 等) を並列で走らせる。
- **新機能の設計**: `feature-dev` → brainstorming → 実装の流れ。
- **UI 確認**: `playwright``pnpm dev` の画面を直接操作。
- **将来追加検討**: PostgreSQL MCP — TypeORM + 342 migration の調査効率化。read-only ロールで登録し、接続先 (`misskey` DB) と権限分離に注意する。

View file

@ -1,69 +0,0 @@
# テスト構成
## Backend 全般の前提: `.config/test.yml`
backend のテストスクリプト (`test` / `test:e2e` / `test:fed`) はすべて内部で `cross-env NODE_ENV=test pnpm compile-config` を実行し、`.config/test.yml` を読み込む ([packages/backend/package.json](../../packages/backend/package.json), [packages/backend/scripts/compile_config.js](../../packages/backend/scripts/compile_config.js))。**未作成だとテスト自体が起動しない。**
未作成なら以下を 1 回だけ手動コピーする (どちらでも可):
```bash
ncp .github/misskey/test.yml .config/test.yml
# または
cp .github/misskey/test.yml .config/test.yml
```
補足:
- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要。
- すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない。
- `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと)。
## Backend (Vitest 4, 3 設定)
| 種別 | 設定ファイル | 実行コマンド |
| --- | --- | --- |
| Unit | `packages/backend/vitest.config.unit.ts` | `pnpm --filter backend test` |
| E2E (HTTP / DB) | `packages/backend/vitest.config.e2e.ts` | `pnpm --filter backend test:e2e` |
| Federation | `packages/backend/vitest.config.fed.ts` | `pnpm --filter backend test:fed` |
- 配置: `packages/backend/test/`
- 事前準備は [§Backend 全般の前提: `.config/test.yml`](#backend-全般の前提-configtestyml) を参照。
- カバレッジ: `pnpm --filter backend test-and-coverage`
## Frontend (Vitest)
```bash
pnpm --filter frontend test # 1 回実行
pnpm --filter frontend test-and-coverage # カバレッジ付き
```
- 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`)。
- ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [`packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts`](../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts))。
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー)。
## E2E (Cypress)
ルートから実行する:
```bash
pnpm e2e # start:test サーバーを立てて Cypress run
pnpm cy:open # 対話的に開く
```
- 設定: ルート `cypress.config.ts`。テスト本体は `cypress/` 配下。
## Storybook (frontend)
```bash
pnpm --filter frontend storybook-dev # http://localhost:6006
pnpm --filter frontend build-storybook # 静的ビルド
```
- 各コンポーネント横に `*.stories.impl.ts` を併設する慣習 (例: `MkButton.stories.impl.ts`)。
- Chromatic (`pnpm --filter frontend chromatic`) で視覚回帰チェック。
## ローカル DB / Redis (テスト・開発共通)
```bash
docker compose -f compose.local-db.yml up -d
```

View file

@ -2,32 +2,31 @@
Misskey 固有の繰り返しタスクを Claude にスムーズに実行させるための **カスタムスキル**`.claude/skills/<name>/SKILL.md` 形式で配置する。
frontmatter (`name` + `description`) は、Claude が **自動でスキルを呼び出すか判断する** 唯一の手がかりになる。`description` には用途を具体的かつ網羅的に書く (動詞 + 対象 + トリガー条件)
frontmatter (`name` + `description`) は、Claude が **自動でスキルを呼び出すか判断する** 唯一の手がかりになる。`description` には用途を具体的かつ網羅的に書き、pushy なトリガー語 (例: "Use whenever ...", "Must be consulted before any ...") で発見されやすくする
## 実装済スキル
実装済スキルの一覧は本ファイルでは管理しない (腐敗するため)。各サブディレクトリの `SKILL.md` の frontmatter が自己説明として機能する。
### Misskey 固有 (本リポジトリ向け書き起こし)
## 構成方針
| スキル名 | 役割 | 優先度 |
| --- | --- | --- |
| [create-migration](create-migration/SKILL.md) | TypeORM CLI (`migration:generate` / `migration:create`) でマイグレーションを生成し、SPDX / up-down / `check-migrations` まで誘導 | 高 (342 既存 / 規約厳しい) |
| [add-api-endpoint](add-api-endpoint/SKILL.md) | NestJS DI + meta/paramDef 規約で API エンドポイント追加。`endpoint-list.ts` 登録と `misskey-js` 再生成を含む | 高 |
| [add-i18n-key](add-i18n-key/SKILL.md) | `locales/ja-JP.yml` のみ編集する補助。型は `packages/i18n` が自動再生成 | 中 |
| [add-mk-component](add-mk-component/SKILL.md) | `Mk*` 命名 + SPDX (HTML) + SCSS module + `*.stories.impl.ts` 併設の Vue コンポーネントを一括スキャフォールド | 中 |
Anthropic 公式の [Agent Skills ベストプラクティス](https://platform.claude.com/docs/ja/agents-and-tools/agent-skills/best-practices) に従い、以下の構造を採用する:
### ECC (everything-claude-code) 由来 — MIT セレクトインポート
- **SKILL.md 本体は 500 行以下** (理想は 30-80 行の索引)
- 詳細は `references/tasks/` (手順) と `references/knowledge/` (規約・背景知識) に分離 (progressive disclosure)
- リンクは原則 **references への 1 段リンク** に留める (例外: 他 skill / agent への導線は可)
- ファイルシステム上の references は読まれるまでゼロコンテキストコスト
[.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) §1 に出典・改変メモ・MIT 全文を集約。
| スキル名 | 役割 | 優先度 |
| --- | --- | --- |
| [context-budget](context-budget/SKILL.md) | agents / skills / MCP / CLAUDE.md の token overhead を見える化し、肥大コンポーネントを検出 | 中 |
設計方針: `create-migration` は手動の `Date.now()` 命名ではなく TypeORM 公式 CLI (`migration:generate` / `migration:create`) を採用。Storybook ファイル名は `*.stories.impl.ts` 規約に準拠する。
ECC (everything-claude-code) 由来の MIT スキルが含まれる場合は、ファイル冒頭の SPDX ヘッダー + [.claude/THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md) §1 に出典を記載する。
## 新規スキルを追加する場合
- `.claude/skills/<name>/SKILL.md` に YAML frontmatter (`name` + `description`) と本文 Markdown を書く。
- `disable-model-invocation: true` は付けない (auto-invoke させたいため)。
- 主要参照ファイルへのリンクは、リポジトリルートからの相対パス (例: `../../packages/backend/...`) で貼る。絶対パスは contributor のホームディレクトリ依存になるので使わない。
- 完成したらこの README の表にも 1 行追加する。
- `.claude/skills/<name>/SKILL.md` に YAML frontmatter (`name` + `description`) と本文 Markdown を書く
- description は **三人称の "Use when ..." 形式** で、主要キーワード網羅。pushy なトリガー語 ("Must be consulted before ...") を入れる
- `disable-model-invocation: true` は付けない (auto-invoke させたいため)
- 主要参照ファイルへのリンクは、各 markdown ファイルからの相対パスで貼る (`../../../../packages/backend/...` のような形)。絶対パスは contributor のホームディレクトリ依存になるので使わない
- 詳細を分ける場合は `references/tasks/` (手順) / `references/knowledge/` (知識) の二分に従う
- スキル作成は `/skill-creator` (公式の skill-creator スキル) のガイドを経由するのが推奨
## 関連
- 各スキルの description で自動索引される設計のため、実装済スキルの手書き索引 (一覧表) は本ファイルにも `AGENTS.md` にも持たない方針 (手書き索引は腐敗するため、frontmatter の description を唯一の索引とする)
- スキルそのものの健全性検査は [/harness-audit](../commands/harness-audit.md) で採点できる

View file

@ -1,253 +0,0 @@
---
name: add-api-endpoint
description: Misskey の REST API エンドポイント (/api/<category>/<name>) を NestJS DI + meta/paramDef 規約で追加する。バックエンドに新しい API ルートを足す時に必ず使う。endpoint-list.ts への手動登録、e2e テスト、misskey-js 再生成、CHANGELOG までの一連の手順を含む。
---
# Misskey API エンドポイント追加スキル
`packages/backend/src/server/api/endpoints/<category>/<name>.ts` に新規エンドポイントを追加するためのワークフロー。**手順 4 (endpoint-list.ts 登録) を忘れると 404 になる** 点に最大の注意を払う。
## 最重要事実 (見落とすと壊れる)
1. エンドポイントは **glob 自動収集されない**。[packages/backend/src/server/api/endpoint-list.ts](../../../packages/backend/src/server/api/endpoint-list.ts) への 1 行追加が必須。
2. `meta` / `paramDef` を変えたら **misskey-js の再生成が必須**`pnpm build-misskey-js-with-types` を忘れると CI の `check-misskey-js-autogen` で必ず落ちる。
3. `meta.errors` の各 `id`**UUID**。重複させない (既存全 UUID と衝突確認)。
## ステップ 1: ファイル配置と SPDX
`packages/backend/src/server/api/endpoints/<category>/<name>.ts` に新規作成する。`<category>` は機能領域 (例: `notes`, `users`, `admin/announcements`)。
冒頭に SPDX ヘッダーを必ず付ける:
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
## ステップ 2: 最小テンプレート (シンプル read 系)
[endpoints/ping.ts](../../../packages/backend/src/server/api/endpoints/ping.ts) をベースに書く。認証不要・パラメータなし・小さなレスポンスの例:
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
export const meta = {
tags: ['<tag>'],
requireCredential: false,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
// ...
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
) {
super(meta, paramDef, async (ps, me) => {
// 実装
});
}
}
```
## ステップ 3: 認証付き / DI / errors を含むテンプレート
[endpoints/notes/create.ts](../../../packages/backend/src/server/api/endpoints/notes/create.ts) を参照する。要点:
```ts
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { DI } from '@/di-symbols.js';
// import ms from 'ms'; // limit.duration に ms('1hour') 等を渡すとき (default import)
export const meta = {
tags: ['notes'],
requireCredential: true, // 認証必須なら true
prohibitMoved: false, // moved user を拒否するか
kind: 'write:notes', // OAuth scope (requireCredential 時に必須)
limit: {
duration: 3600000, // ms('1hour')
max: 300,
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', // ★ UUID v4 を必ず生成 (`x`=hex, `y`=8/9/a/b)。下の「UUID 生成」を参照
},
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Note', // packed entity に揃える場合
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['noteId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.notesRepository.findOneBy({ id: ps.noteId });
if (note == null) throw new ApiError(meta.errors.noSuchNote);
// 実装
});
}
}
```
### meta フィールド早見表
| フィールド | 用途 |
|---|---|
| `tags` | OpenAPI タグ (機能領域) |
| `requireCredential` | 認証必須か |
| `requireModerator` / `requireAdmin` | 権限制限 |
| `prohibitMoved` | アカウント移行済ユーザーを拒否 |
| `kind` | OAuth scope (`read:notes` / `write:notes` 等)。`requireCredential: true` 時必須 |
| `limit` | レート制限 (`{ duration, max, key?, minInterval? }`) |
| `errors` | エラー定義。各要素に `message` / `code` / `id` (UUID v4) 必須 |
| `res` | JSON Schema or `ref: '<EntityName>'` (packed entity 参照) |
| `requireFile` | ファイルアップロード必須 |
| `secure` | secure cookie 必要 |
| `allowGet` | GET メソッド許可 |
| `cacheSec` | レスポンスキャッシュ秒数 |
| `description` | OpenAPI 説明 |
詳細は [endpoints.ts](../../../packages/backend/src/server/api/endpoints.ts) の型定義 (lines 11-125) を参照。
### paramDef の特殊フォーマット
JSON Schema (AJV) ベースだが、Misskey 拡張を使える:
- `format: 'misskey:id'` — ID 文字列バリデーション
- `allOf` / `anyOf` / `oneOf` — 複合条件
- `default` — デフォルト値
詳細は [endpoint-base.ts](../../../packages/backend/src/server/api/endpoint-base.ts) を参照。
### エラー throw
**「公開 API エラーとして API クライアントに返したいもの」は必ず `throw new ApiError(meta.errors.<key>)` を使う**。`meta.errors` に列挙した上で `ApiError` でラップしないと、misskey-js 側の型情報に出ず、レスポンスも 500 になる。第 2 引数で追加情報を渡せる:
```ts
throw new ApiError(meta.errors.invalidParam, { reason: 'too short' });
```
一方で、**想定外の例外 (DB 不整合 / 下層サービスの bug など) を握り潰すために `try/catch``ApiError` に変換するのは避ける**。既存 endpoint も「期待される業務エラーは `ApiError` に変換し、それ以外は `throw err;` で再 throw する」という二段構えになっている。`packages/backend/src/server/api/endpoints/notes/create.ts``catch` 節 (末尾の `throw err;`) を参照。生の `throw` を全面禁止すると未知例外も 200 で潰れて debug が困難になるので、このバランスを保つ。
詳細は [error.ts](../../../packages/backend/src/server/api/error.ts) の `ApiError` クラスを参照。
### UUID 生成
```bash
node -e "console.log(crypto.randomUUID())"
```
その UUID が他のエンドポイントの `id` と衝突していないか必ず確認:
```bash
grep -r "id: '<生成した UUID>'" packages/backend/src/server/api/endpoints/
```
## ステップ 4: ★必須 — endpoint-list.ts に登録
[packages/backend/src/server/api/endpoint-list.ts](../../../packages/backend/src/server/api/endpoint-list.ts) の同カテゴリ末尾に 1 行追加する(既存の並びを崩さない):
```ts
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
```
ファイル冒頭のコメント (`When you add new endpoint, you should add it to this file.`) の通り、このリストが API ルーティングの単一の真実。**忘れると 404**。
`EndpointsModule.ts` がこのファイルの全エクスポートを `Object.entries()` で反復し、NestJS provider (`provide: 'ep:<path>'`) を生成する。
## ステップ 5: e2e テスト追加
[packages/backend/test/e2e/endpoints.ts](../../../packages/backend/test/e2e/endpoints.ts) に対応する `describe` / `test` を追加する。`api()` ヘルパーで叩く:
```ts
describe('<category>/<name>', () => {
test('正常系', async () => {
const res = await api('<category>/<name>', { /* params */ }, alice);
assert.strictEqual(res.status, 200);
});
});
```
実行: `pnpm --filter backend test:e2e`
## ステップ 6: misskey-js 再生成 (★必須)
`meta` / `paramDef` / `res` を変えたら必ず実行する:
```bash
pnpm build-misskey-js-with-types
```
これで以下が更新される:
- `packages/backend/built/api.json` (OpenAPI spec)
- `packages/misskey-js/generator/api.json`
- `packages/misskey-js/src/autogen/*.ts` (TypeScript 型)
PR に `packages/misskey-js/src/autogen/` 配下の差分が含まれていないと、CI の `check-misskey-js-autogen` で落ちる。
## ステップ 7: Lint と typecheck
```bash
pnpm --filter backend lint
```
(typecheck = `tsgo --noEmit` / ESLint = `eslint`)
## ステップ 8: CHANGELOG
ユーザー影響がある (新機能 / 既存挙動変更) なら、`CHANGELOG.md``## Unreleased``### Server` に 1 行追加する ([AGENTS.md §CHANGELOG](../../../AGENTS.md#changelog) 参照):
```
- Feat: /api/<category>/<name> を追加
```
純粋なリファクタや内部用なら不要。
## 参照ファイル
- [endpoints.ts (meta/paramDef 型定義)](../../../packages/backend/src/server/api/endpoints.ts)
- [endpoint-base.ts (Endpoint 基底クラス)](../../../packages/backend/src/server/api/endpoint-base.ts)
- [endpoint-list.ts (★ ここに登録)](../../../packages/backend/src/server/api/endpoint-list.ts)
- [error.ts (ApiError)](../../../packages/backend/src/server/api/error.ts)
- [endpoints/ping.ts (最小例)](../../../packages/backend/src/server/api/endpoints/ping.ts)
- [endpoints/notes/create.ts (DI + errors の典型)](../../../packages/backend/src/server/api/endpoints/notes/create.ts)
- [test/e2e/endpoints.ts (テスト例)](../../../packages/backend/test/e2e/endpoints.ts)
- [scripts/generate_api_json.js (misskey-js 生成元)](../../../packages/backend/scripts/generate_api_json.js)

View file

@ -1,117 +0,0 @@
---
name: add-i18n-key
description: Misskey の i18n キーを追加・修正する。locales/ja-JP.yml のみ編集可能で、他言語ファイル (en-US.yml 等 39 言語) は Crowdin の自動配信先のため絶対に触らない。型は packages/i18n が ja-JP.yml から自動再生成する。frontend からは i18n.ts.<key> または i18n.tsx.<key>(...) で参照する。
---
# Misskey i18n キー追加スキル
UI 文言の追加・変更を行う際の規約。**手動編集して良いのは `locales/ja-JP.yml` のみ。**
## 大前提 (絶対 NG)
- **`locales/<lang>.yml` (ja-JP.yml 以外) の編集は禁止**。これらは Crowdin の自動配信先で、手動編集すると次の同期で上書き喪失する ([locales/README.md](../../../locales/README.md), [crowdin.yml](../../../crowdin.yml))。
- 文字列リテラルを SFC に直書きしない (`<span>こんにちは</span>` 等)。必ず `i18n.ts.<key>` を経由する。
- 既存キーの破壊的リネームは Crowdin 翻訳資産も道連れになるので慎重に。追加・改名併用 (新キー追加 → 移行 → 旧キー削除) を検討する。
## ステップ 1: ja-JP.yml にキーを追加
[locales/ja-JP.yml](../../../locales/ja-JP.yml) を編集する。YAML の階層構造を維持し、関連するセクションに配置する:
```yaml
# トップレベル単純キー
save: "保存"
# ネストしたカテゴリ (アンダースコア接頭辞は内部カテゴリ)
_settings:
general: "全般"
appearance: "外観"
# パラメータ付き (単純なプレースホルダ置換)
# ICU MessageFormat の plural / select / number / date などは非対応
# 使えるのは `{name}` のような単純な置換のみ
greeting: "こんにちは、{name}さん"
```
### 命名のお作法
- 単純キー: lowerCamelCase (例: `saveChanges`, `confirmDelete`)。
- カテゴリ: アンダースコア接頭辞 (例: `_settings`, `_abuseUserReport`)。
- 既存セクション内に置く場合はアルファベット順を維持する (新セクション全体を末尾に追加するのは可)。
## ステップ 2: 型定義の自動再生成
`packages/i18n/build.ts``ja-JP.yml` を解析し、TypeScript インターフェースを [packages/i18n/src/autogen/locale.ts](../../../packages/i18n/src/autogen/locale.ts) に出力する。
### 自動 (推奨)
`pnpm dev` 実行中なら、`packages/i18n` の watch スクリプトが yml の変更を検知して自動再生成する。
### 手動
```bash
pnpm --filter i18n generate
```
実体は `tsx scripts/generateLocaleInterface.ts`
### 失敗パターン
これを実行せずに frontend 側で `i18n.ts.<newKey>` を参照すると、`Locale` インターフェースに追加されていないため、typecheck で「Property '<newKey>' does not exist on type 'Locale'」というエラーになる。`pnpm --filter frontend lint` で発覚する。
## ステップ 3: frontend での参照
```ts
import { i18n } from '@/i18n.js';
```
| 用途 | 書き方 |
|---|---|
| 単純文字列 | `i18n.ts.save` |
| ネスト | `i18n.ts._settings.general` |
| パラメータ付き | `i18n.tsx.greeting({ name: userName })` |
| Vue テンプレート内 | `{{ i18n.ts.save }}` / `{{ i18n.tsx.greeting({ name }) }}` |
`i18n.ts` は型付き文字列、`i18n.tsx` は MessageFormat 関数。
## ステップ 4: 検証
```bash
# i18n パッケージの型再生成 + typecheck
pnpm --filter i18n lint
# frontend で新キー参照箇所の型チェック
pnpm --filter frontend lint
```
## 例: 「ノートを削除しますか?」確認ダイアログを追加する
1. `locales/ja-JP.yml`:
```yaml
_notes:
deleteConfirm: "このノートを削除しますか?"
```
2. `pnpm --filter i18n generate` (または `pnpm dev` で watch 中)
3. SFC:
```vue
<script setup lang="ts">
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
async function onDelete() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._notes.deleteConfirm,
});
if (canceled) return;
// 削除処理
}
</script>
```
## 参照ファイル
- [locales/README.md (★ 編集ポリシー根拠)](../../../locales/README.md)
- [locales/ja-JP.yml](../../../locales/ja-JP.yml)
- [packages/i18n/build.ts](../../../packages/i18n/build.ts)
- [packages/i18n/src/autogen/locale.ts (生成物)](../../../packages/i18n/src/autogen/locale.ts)
- [packages/frontend/src/i18n.ts](../../../packages/frontend/src/i18n.ts)

View file

@ -1,174 +0,0 @@
---
name: add-mk-component
description: Misskey フロントエンドの新規 Vue 3 コンポーネントを追加する。Mk* 命名 / SPDX (HTML コメント) / <script setup lang="ts"> / <style lang="scss" module> / *.stories.impl.ts 併設の規約をまとめて適用する。新しい共有 UI コンポーネントを packages/frontend/src/components/ に作る時に使う。
---
# Misskey Vue コンポーネント追加スキル
`packages/frontend/src/components/` に新しい共有コンポーネントを追加するための規約。
## 大前提
- 共有 / 再利用コンポーネントは **必ず `Mk` プレフィックス** (例: `MkButton`, `MkInput`)。ページ固有部品など `Mk` プレフィックスでないものは原則 `pages/` 側に置く。
- 新規では `<style lang="scss" module>` (CSS Modules) を既定とする。古い `scoped` 形式が混在しているが、新規では使わない。
- 文字列リテラルの直書きは禁止。文言は必ず `i18n.ts.<key>` 経由で参照する (新キーは `add-i18n-key` スキルを参照)。
- `alert()` / `confirm()` / `window.prompt()` は使わない。`os.alert` / `os.confirm` / `os.popup` などを使う。
## ステップ 1: ファイル配置
`packages/frontend/src/components/Mk<Name>.vue` に新規作成する。
ストーリーが必要 (= ほぼ常に必要) なら、同階層に `Mk<Name>.stories.impl.ts` も作る。Storybook の規約は `*.stories.impl.ts` であって、`*.stories.ts` ではない。
## ステップ 2: SPDX ヘッダー (HTML コメント形式)
`.vue` ファイル冒頭に必須:
```html
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
`/* ... */` (TS / JS 形式) ではなく **HTML コメント** で書くこと。既存の `.vue` ファイルがすべて HTML コメント形式を使っており、SFC の先頭として自然な形式に統一するため (CI の `spdx` ジョブはコメント形式ではなく SPDX 文字列の有無のみを検査する)。
## ステップ 3: 最小テンプレート
[MkInfo.vue](../../../packages/frontend/src/components/MkInfo.vue) をベースにする (シンプルな表示コンポーネント):
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
variant?: 'primary' | 'secondary';
}>();
const emit = defineEmits<{
(ev: 'click'): void;
}>();
</script>
<style lang="scss" module>
.root {
padding: 12px 14px;
border-radius: var(--MI-radius);
background: var(--MI_THEME-panel);
}
</style>
```
### 規約ポイント
| 項目 | 規約 |
|---|---|
| `<script>` | `<script lang="ts" setup>`。型パラメータが必要なら `generic="T extends ..."` を付ける ([MkInput.vue 参照](../../../packages/frontend/src/components/MkInput.vue)) |
| `defineProps` / `defineEmits` | **type-only** (`<{ ... }>`) 形式。runtime の object 形式は使わない |
| `<style>` | `lang="scss" module` を既定。クラス参照は `:class="$style.foo"` |
| CSS 変数 | `var(--MI_THEME-...)` (テーマ) / `var(--MI-radius)` (UI 共通) — ハードコードしない |
| アイコン | Tabler icons のクラス (`<i class="ti ti-info-circle">`) を使う |
## ステップ 4: i18n と os の利用
```vue
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
async function onClick() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._notes.deleteConfirm,
});
if (canceled) return;
os.toast(i18n.ts.deleted);
}
</script>
```
### `os` の主なヘルパー (詳細は [os.ts](../../../packages/frontend/src/os.ts))
| 関数 | 用途 |
|---|---|
| `os.alert({ type, title?, text })` | 単方向アラート |
| `os.confirm({ type, title, text })` | yes/no 確認 (`{ canceled }` を返す) |
| `os.toast(message)` | 一時通知 |
| `os.popup(component, props, handlers)` | 任意コンポーネントの非同期ポップアップ |
| `os.popupMenu(items, anchor?)` | コンテキストメニュー |
| `os.form(title, fields)` | フォームダイアログ |
| `os.apiWithDialog(endpoint, data)` | API 呼出し + エラー時ダイアログ表示 |
## ステップ 5: Storybook ストーリー併設
[MkButton.stories.impl.ts](../../../packages/frontend/src/components/MkButton.stories.impl.ts) を雛形として参考にする。`.stories.impl.ts``packages/frontend/src/` 配下の `.ts` ファイルなので [AGENTS.md §1 SPDX ヘッダー必須](../../../AGENTS.md#1-spdx-ヘッダー必須) の対象であり、冒頭に SPDX ヘッダーを必ず付ける (HTML コメント形式ではなく `/* */` 形式)。形式 (以下の `MkXxx` は実際のコンポーネント名に置換する):
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import MkXxx from './MkXxx.vue';
export const Default = {
render(args) {
return {
components: { MkXxx },
setup() {
return { args };
},
template: '<MkXxx v-bind="args">slot content</MkXxx>',
};
},
args: {
variant: 'primary',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkXxx>;
```
`Vue` SFC は default export なので、`import MkXxx from './MkXxx.vue';` のように名前付き import ではなく default import で書く。実行確認は `pnpm --filter frontend storybook-dev`
## ステップ 6: Lint と typecheck
```bash
pnpm --filter frontend lint
```
(typecheck = vue-tsc 等、ESLint = `@misskey-dev/eslint-plugin` 含む)
ESLint --fix をピンポイントで:
```bash
pnpm exec eslint --fix packages/frontend/src/components/Mk<Name>.vue
```
## ステップ 7: 既存コンポーネントとの整合性確認
- 似た用途の既存 `Mk*` コンポーネントを参考に、スタイルやプロップ命名を揃える。
- `_button` / `_panel` / `_selectable` などの **共通 utility class** (グローバルスタイルにある) を活用できるか確認する。
- 大きな機能なら、Storybook stories で各バリエーションを網羅する。
## 参照ファイル
- [MkInfo.vue (シンプル例)](../../../packages/frontend/src/components/MkInfo.vue)
- [MkButton.vue (汎用ボタン例)](../../../packages/frontend/src/components/MkButton.vue)
- [MkInput.vue (generics + 多機能例)](../../../packages/frontend/src/components/MkInput.vue)
- [MkButton.stories.impl.ts (Storybook 雛形)](../../../packages/frontend/src/components/MkButton.stories.impl.ts)
- [packages/frontend/src/os.ts](../../../packages/frontend/src/os.ts)
- [packages/frontend/src/i18n.ts](../../../packages/frontend/src/i18n.ts)

View file

@ -1,156 +0,0 @@
---
name: create-migration
description: Misskey の TypeORM マイグレーションを公式 CLI (migration:generate / migration:create) で正しく生成し、SPDX ヘッダー付与・up/down 整合・check-migrations 確認まで誘導する。エンティティのスキーマ変更を含むあらゆる DB 変更、または手書き SQL によるデータ移行が必要な時に使用する。
---
# Misskey マイグレーション作成スキル
`packages/backend/migration/` に新規 TypeORM マイグレーションを追加するためのワークフロー。
## 大前提 (絶対 NG)
- **既にマージ済み (develop / master) のマイグレーションファイルを編集しない** ([AGENTS.md §3](../../../AGENTS.md#3-マージ済み-migration-を絶対に編集しない))。本番履歴の改変は深刻なデータ不整合を引き起こす。スキーマ変更は **常に新しいタイムスタンプで新規ファイル** を作る。
- ファイル名のタイムスタンプ部分を後から書き換えない (順序が壊れる)。
> 作り方は AGENTS.md §3 の「`Date.now()` で UNIX ms を取得 → `{ms}-{PascalName}.js` を手書き」が最低ライン。エンティティ差分から自動生成したい (= TypeORM の `migration:generate` を使う) 場合は本 skill の手順に従う。**どちらでも構わない**が、エンティティ変更を伴う時は CLI 経由のほうが取り漏れが減るので推奨。
## ステップ 1: どちらの方式を使うか決める
| 状況 | 方式 |
|---|---|
| エンティティ (`packages/backend/src/models/*.ts`) を `@Column` / `@Index` / `@Entity` 等で先に変更し、差分から自動生成したい | `typeorm migration:generate` (本 skill の手順) |
| 手書き SQL / データ移行 / `CREATE INDEX CONCURRENTLY` など、エンティティ差分では表現できない変更 | `typeorm migration:create` で空雛形を作るか、`migrate-new` command で手書き雛形を作る |
| 列追加 1 本のような小規模変更で、既存ファイルをコピーした方が速い | AGENTS.md §3 の手順 (`Date.now()` + 手書き) でよい |
迷ったら **まずエンティティを変更 → `migration:generate`** が原則。既存 342 ファイルのほぼすべてが `queryRunner.query(\`SQL...\`)` の raw SQL なので、CLI 出力でも手書きでもスタイルは揃う。
## ステップ 2: CLI 実行
ルートディレクトリから以下を実行する。`<PascalName>` は変更内容を表す PascalCase (例: `AddBirthdayIndex`, `AddCategoryToAvatarDecorations`)。
### 2-A. エンティティ差分から生成
[CONTRIBUTING.md §Migration作成方法](../../../CONTRIBUTING.md#migration作成方法) に記載の基本形:
```bash
# packages/backend ディレクトリで実行する場合 (CONTRIBUTING.md 記載形式)
pnpm dlx typeorm migration:generate -d ormconfig.js -o --esm <PascalName>
```
**リポジトリルートから実行する場合** (AI が使う推奨形式。`pnpm --filter backend exec` を使うと backend の TypeORM バージョンと一致するため確実):
```bash
pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>
```
> **`--esm` について**: `-o` / `--outputJs` は「TS ではなく JS を出力する」オプション、`--esm` は「ESM 形式 (`export class ...`) で出力する」オプション。Misskey の既存 migration はすべて ESM JS であるため **両方が必須**`--esm` を省略すると CommonJS 形式の JS が生成されスタイルが揃わない。
事前準備:
- `pnpm build-pre` を実行して `built/meta.json` を生成する (`loadConfig()``built/meta.json` を必須とするため。`pnpm build` 済みであれば不要)。
- `.config/default.yml` が存在すること (なければ `.config/example.yml` を参考に作成する)。
- `pnpm --filter backend compile-config` を実行して `built/.config.json` を生成する (`ormconfig.js``loadConfig()` 経由で必須とする。未実行だと "Compiled configuration file not found." エラーになる)。
- `pnpm --filter backend build` でエンティティを最新ビルド (CLI は `built/` を読む)。
- ローカル DB を起動する (`docker compose -f compose.local-db.yml up -d`)。
### 2-B. 空の手書きマイグレーション
```bash
pnpm --filter backend exec typeorm migration:create -o --esm migration/<PascalName>
```
ローカル DB の起動とビルドは不要。空の `up` / `down` だけが生成される。
> `-o --esm`**必ず付ける**。これが無いと `<UnixMs>-<PascalName>.ts` (CommonJS / TS 出力) が生成されるが、Misskey の `ormconfig.js``migration/*.js` だけを読み、既存の他 migration も全て `export class ... { async up(queryRunner) {...} }` の ESM JS 形式なので、後で手作業で `.ts → .js` リネーム + `import { MigrationInterface }` 削除 + `class ... implements MigrationInterface` 削除をしないと走らない。`-o --esm` を付ければそのまま `.js` ESM で出るので、後処理は SPDX ヘッダー付与 (ステップ 3) だけで済む。
## ステップ 3: SPDX ヘッダー付与
CLI 出力には SPDX ヘッダーが含まれない。**必ず冒頭に追加する** (CI の `spdx` ジョブが失敗するため)。
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
## ステップ 4: up / down の整合確認
- `up()` の各ステートメントに対し、`down()` で完全に巻き戻せること。
- 列追加 (`ADD COLUMN`) ↔ 列削除 (`DROP COLUMN`)、テーブル作成 ↔ テーブル削除、
FK 追加 ↔ FK 削除、インデックス作成 ↔ インデックス削除 を必ずペアで書く。
- `down()` を空のまま残さない。本番ロールバック時に詰む。
### インデックス追加時の注意 (CREATE INDEX CONCURRENTLY)
大規模テーブルへの `CREATE INDEX` は本番で長時間ロックする恐れがある。`CONCURRENTLY` で発行するときは **migration 側にも対応が必要**: PostgreSQL は `CREATE INDEX CONCURRENTLY` を transaction 内で実行できないため、migration class に以下を仕込んで TypeORM に「この migration は transaction を張らない」と指示する。
参照実装: [packages/backend/migration/1745378064470-composite-note-index.js](../../../packages/backend/migration/1745378064470-composite-note-index.js)。
```js
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
export class CompositeNoteIndex1745378064470 {
name = 'CompositeNoteIndex1745378064470';
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
async up(queryRunner) {
const concurrently = isConcurrentIndexMigrationEnabled;
if (concurrently) {
// CREATE INDEX CONCURRENTLY ...
} else {
// CREATE INDEX ...
}
}
async down(queryRunner) {
// 同様に環境変数で分岐
}
}
```
要点:
- **`transaction = isConcurrentIndexMigrationEnabled ? false : undefined;`** が必須。これがないと `CREATE INDEX CONCURRENTLY` が transaction 内で実行されて `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block` で失敗する。
- 環境変数 `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` がデフォルト OFF。OFF のときは普通の `CREATE INDEX` (transaction 内) で動く必要がある。`up`/`down` 双方を環境変数で分岐させる。
- `ormconfig.js``migrationsTransactionMode`**環境変数で切り替わる**: `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` のときだけ `'each'` (各 migration が個別 transaction)、未設定時は `'all'` (全 migration を 1 つの transaction でラップ) ([ormconfig.js:19](../../../packages/backend/ormconfig.js#L19))。普段は `'all'` 前提なので、CONCURRENTLY を使う migration を書く時だけこのフラグの存在を意識すれば良い。
### 関連エンティティとの一致
`migration:generate` を使った場合、エンティティ側の `@Column` / `@Entity` 修正と DB スキーマが食い違うとビルド全体がズレる。生成後に該当エンティティと SQL の対応を目視確認すること。
## ステップ 5: 検証
ルートから実行:
```bash
# 未反映の差分が無いか (新規 migration が生成すべき DDL を取り逃していないか)
pnpm --filter backend check-migrations
# ローカル DB に適用
pnpm migrate
# ロールバック (down が壊れていないか)
pnpm revert
# 再適用 (順方向にもう一度通す)
pnpm migrate
```
`check-migrations` の実体は [scripts/check_migrations_clean.js](../../../packages/backend/scripts/check_migrations_clean.js)。TypeORM の `dataSource.driver.createSchemaBuilder().log()` で pending DDL を取得し、`upQueries` / `downQueries` のいずれかが残っていれば非ゼロ終了する。**順序検査ではなく**「エンティティと migration が同期しているか」の検査。
## ステップ 6: 既存ファイル参照テンプレ
新規ファイルを書くときは、変更パターンが近い既存ファイルを **必ずひとつ開いて並べて書く**。スタイルが激しくズレた PR は差し戻されやすい。
| パターン | 参照ファイル |
|---|---|
| インデックス追加 + 関数定義 | [packages/backend/migration/1767169026317-birthday-index.js](../../../packages/backend/migration/1767169026317-birthday-index.js) |
| 列追加のみ | [packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js](../../../packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js) |
| テーブル新規作成 + FK | [packages/backend/migration/1761569941833-add-channel-muting.js](../../../packages/backend/migration/1761569941833-add-channel-muting.js) |
クラス命名規則は **PascalCase 名 + 13 桁タイムスタンプ** (例: `class BirthdayIndex1767169026317`)。`name` プロパティもクラス名と同一文字列にする。
## ステップ 7: CHANGELOG (ユーザー影響がある場合)
スキーマ変更がユーザーに見える挙動を生む場合のみ、`CHANGELOG.md``## Unreleased``### Server` または `### General` に 1 行追加する ([AGENTS.md §CHANGELOG](../../../AGENTS.md#changelog) 参照)。内部リファクタや純粋なインデックス追加は不要。

View file

@ -0,0 +1,33 @@
---
name: shipping-misskey-change
description: Use at every "finish" moment of a Misskey change — immediately before committing, opening a PR, merging, or handing the work back to the user even without a commit. Runs the final pre-ship checklist — `pnpm lint`, misskey-js regeneration (`pnpm build-misskey-js-with-types`) when backend API changed, `pnpm --filter backend check-migrations` when entities or migrations changed, SPDX header verification on new files, locale safety check (no edits to non-`ja-JP` locale yml files), and `CHANGELOG.md` Unreleased entry for user-visible changes. Must be consulted as the last step of every change — including uncommitted handoffs — to avoid CI failures and lost translations. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this regardless of what preceded it.
---
# shipping-misskey-change
Misskey の変更の **finish 局面** (commit / PR / merge する直前、またはコミットせずユーザーに作業を返す直前) に必ず走らせる最終チェックリスト。
CI で落ちやすい / レビュアーから指摘されやすいポイントを 1 箇所に集めている。後で references を辿る余裕を作らないため、チェックリストは SKILL.md 本体に直書きする。
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、作業を返す直前・commit 直前のタイミングでこのスキルを呼ぶこと。
## 最終チェックリスト
このリストを TodoWrite に展開して 1 項目ずつ確認すること。**該当しない項目は飛ばして良いが、判断は明示する**。
- [ ] lint が通る — ECC 由来の [/quality-gate](../../commands/quality-gate.md) コマンドで lint (typecheck + eslint) + 高速テストをまとめて回すのが基本。lint だけ単発で確認したいなら `pnpm lint` 直接でもよい
- [ ] backend で `meta` / `paramDef` / `res` を変更した → `pnpm build-misskey-js-with-types` を実行して `packages/misskey-js/src/autogen/` の差分も commit に含めた → 詳細手順は [references/tasks/regenerate-misskey-js.md](references/tasks/regenerate-misskey-js.md)
- [ ] エンティティ (`packages/backend/src/models/*.ts``@Column` / `@Entity` / `@Index`) を変更した → `pnpm --filter backend check-migrations` が pending DDL 0 件で通る
- [ ] migration ファイルを追加した → `up()``down()` の両方を実装した / 既存のマージ済 migration は一切触っていない
- [ ] 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルを追加した → SPDX ヘッダーを付けた (`.vue` / `.html` は HTML コメント形式、その他は TS コメント形式)
- [ ] `locales/` を編集した → **`ja-JP.yml` だけ** を変更しており、他言語 yml の diff は出ていない (`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空)
- [ ] ユーザーから見える変更 (機能追加 / 既存挙動変更) → `CHANGELOG.md``## Unreleased` 直下の該当サブセクション (General / Client / Server) に 1 行追記した → 詳細書式は [references/tasks/changelog-update.md](references/tasks/changelog-update.md)
- [ ] backend API endpoint を追加・変更した → [misskey-api-reviewer](../../agents/misskey-api-reviewer.md) agent を Task で起動して機械レビューする (endpoint-list 登録漏れ / misskey-js 再生成漏れ / meta・UUID / SPDX。lint や CI では拾いにくい 404・登録漏れの最終関門なので、該当する変更があれば飛ばさない)
- [ ] frontend の `.vue` を追加・変更した → [vue-component-reviewer](../../agents/vue-component-reviewer.md) agent を Task で起動して機械レビューする (SPDX 形式 / 命名 / i18n / SCSS 変数 / os.* / a11y / Storybook 併設)
- [ ] (任意) `.claude/` ハーネス自体の健全性を確認したい → ECC 由来の [/harness-audit](../../commands/harness-audit.md) コマンドを実行
## 何のためのスキルか
これは「**作業中に何を作るか**」を決めるスキルではなく、「**作り終わった後に CI を通す**」スキル。`working-on-backend` / `working-on-frontend` から始まった作業の **出口** として機能する。
該当する変更がある場合は各 references/tasks/ を Read して詳細手順を踏むこと。`pnpm lint` だけは references を読まずに直接走らせて良い (`/quality-gate` でまとめて回せる)。

View file

@ -0,0 +1,61 @@
# CHANGELOG.md の Unreleased セクションに 1 行追記する
ユーザー影響のある変更 (機能追加・修正・改善) は `CHANGELOG.md` の冒頭 `## Unreleased` セクションに 1 行追加する。リファクタリング等の内部変更は不要。
## セクション構造
`## Unreleased` 配下に **3 つのサブセクション** が用意されている:
- `### General` — 共通 / 横断的な変更
- `### Client``packages/frontend`
- `### Server``packages/backend`
## エントリ書式
該当サブセクションに `- <Prefix>: <概要>` の形式で追加。Prefix は先頭大文字。
```text
- Enhance: ノートの詳細表示での公開範囲の表示を改善
- Fix: 通知が約10秒遅延する問題を修正
- Feat: 新機能の追加
```
| Prefix | 用途 |
|---|---|
| `Feat:` | 新機能の追加 |
| `Enhance:` | 既存機能の改善 |
| `Fix:` | バグ修正 |
| `Note:` | 機能変更ではないが利用者に知らせたい事項 (設定の初期化・config 項目の追加・非互換な挙動変更など) |
`Note:` は Feat / Enhance / Fix のような変更そのものではなく、「アップデート後に利用者が知っておくべき注意」を伝えるためのもの (例: `- Note: アップデート後、サウンドに関する設定が初期化されます`)。該当サブセクション内に `- Note: ...` として置く。リリースによっては `## <version>` 直下に `### Note` 専用サブセクションを設ける形もある (既存履歴に両パターンあり)。新規追加時は近傍の既存エントリの書き方に合わせる。
## 触ってはいけない範囲
- `## Unreleased` **以外** のセクション (過去リリース) は変更しない
- `## Unreleased` の見出しと 3 つの空サブセクション骨格自体は維持する (リリーススクリプトが期待する構造)
## 作業手順 (手で書く場合)
1. `CHANGELOG.md` を開いて `## Unreleased` セクションを探す
2. 対象サブセクション (`### General` / `### Client` / `### Server`) の状態を確認
- **空 (placeholder のみ)**: 見出し直下に `-` 単独行のみがある → これを `- Feat: ...` 等で **置換**
- **既存エントリあり**: `- Enhance: ...` / `- Fix: ...` 等の行が 1 つ以上ある → 既存エントリ群の **末尾** に **追記**
3. 順序入れ替えはしない (差分レビューしやすさのため)
4. `git diff CHANGELOG.md` で 1 行のみ追加されていることを確認
## 例
| 引数イメージ | 結果 |
|---|---|
| server, `Fix: 通知が遅延する問題を修正` | `### Server` 末尾に `- Fix: 通知が遅延する問題を修正` を追記 |
| client, `Enhance: ノートの表示を改善` | `### Client` 末尾に `- Enhance: ノートの表示を改善` を追記 |
| general, `Feat: 新機能の追加` | `### General` の placeholder `-``- Feat: 新機能の追加` で置換 |
## コミットメッセージ書式との違い
CHANGELOG とコミットメッセージは **書式が異なる**:
- CHANGELOG: `- Enhance: ノートの表示を改善` (先頭大文字の英語 Prefix + コロン + 日本語本文)
- コミットメッセージ: `enhance(frontend): improve note display` (小文字 + スコープ + コロン + 英語本文。詳細は [CONTRIBUTING.md](../../../../../CONTRIBUTING.md))
両方を 1 つの PR で更新するときに混同しないこと。

View file

@ -0,0 +1,78 @@
# misskey-js の自動生成型を再生成する
backend の API endpoint やスキーマ (`meta` / `paramDef` / `res`) を変更した後、`packages/misskey-js/src/autogen/` の自動生成型を最新化するための手順。
**忘れると CI の `check-misskey-js-autogen` で必ず落ちる**。最頻ミスのひとつ。
## いつ実行するか
以下のいずれかに該当する変更を加えたとき:
- 新規エンドポイント追加 (`packages/backend/src/server/api/endpoints/<category>/<name>.ts`)
- 既存エンドポイントの `meta` (errors / res / kind / requireCredential 等) を変更
- 既存エンドポイントの `paramDef` (入力 schema) を変更
- packed entity (`packages/backend/src/models/json-schema/*.ts`) を変更
実質「`packages/backend/src/server/api/` 配下を触ったら必ず」と考えてよい。
## 実行コマンド
```bash
# リポジトリルートから実行する
pnpm build-misskey-js-with-types
```
内部で以下が一括実行される:
1. backend ビルド (`pnpm --filter backend build`)
2. OpenAPI spec 生成 (`packages/backend/built/api.json`)
3. misskey-js 用 schema 生成 (`packages/misskey-js/generator/api.json`)
4. misskey-js の TypeScript 型再生成 (`packages/misskey-js/src/autogen/{types,entities,endpoint,models,apiClientJSDoc}.ts`)
5. misskey-js ビルド + API extractor
実行時間は 1-3 分程度。タイムアウト警告が出る場合は `--timeout=600000` 相当の長めの設定を使う。
## 実行後の確認
```bash
# 何が変わったかを軽く確認
git status --short -- packages/misskey-js/
git diff --stat -- packages/misskey-js/src/autogen/
# 内容を見たい場合
git diff -- packages/misskey-js/src/autogen/
```
## 差分のパターン
- **差分なし** → backend の変更は misskey-js の公開型に影響していない (内部リファクタなど)。追加コミット不要
- **差分あり**`packages/misskey-js/src/autogen/` 配下のファイルを **必ず commit に含める**
```bash
git add packages/misskey-js/src/autogen/
```
`api.json` の差分が大きい場合は、API endpoint 側の `meta` / `paramDef` / `res` 定義が想定通りか確認する。
## 注意
- このコマンドは **backend 編集後の確認** が目的。backend を変更していないのに走らせるとビルドキャッシュ次第で no-op になる
- 実行中は `packages/backend/built/``packages/misskey-js/built/` などの中間生成物が更新されるが、これらは `.gitignore` 対象
- 生成物以外 (`packages/misskey-js/src/` のうち `autogen/` 以外) に予期せぬ差分が出た場合は、ローカルの編集が混入している可能性があるため、一旦中止して原因を調査する
- `packages/misskey-js/` 配下は **MIT ライセンスのサブパッケージ** なので、`autogen/` ファイルには AGPL の SPDX ヘッダーを付けない / 不要
## CI で落ちた場合のメッセージ例
```
CI: check-misskey-js-autogen
> Please regenerate misskey-js by running:
> pnpm build-misskey-js-with-types
> and commit the changes under packages/misskey-js/src/autogen/.
```
ローカルでもう一度上記コマンドを実行 → 差分を commit → push し直す。
## 関連
- API endpoint 追加の全手順 → [working-on-backend/references/tasks/adding-api-endpoint.md](../../../working-on-backend/references/tasks/adding-api-endpoint.md)
- `meta` / `paramDef` / `res` の規約 → [working-on-backend/references/knowledge/api-meta-paramdef.md](../../../working-on-backend/references/knowledge/api-meta-paramdef.md)

View file

@ -0,0 +1,35 @@
---
name: working-on-backend
description: Use whenever editing or adding code under `packages/backend/` — including REST API endpoints, NestJS services/modules, TypeORM entities, migrations, and backend tests. Covers NestJS DI patterns, TypeORM entity conventions, endpoint-list registration, meta/paramDef/res, misskey-js regeneration, migration up/down rules, and the `.config/test.yml` prerequisite. Must be consulted before any backend change to avoid CI failures and production incidents. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this at implementation time regardless of what preceded it.
---
# working-on-backend
`packages/backend/` (Misskey サーバー本体) を編集するとき、最初に参照するスキル。NestJS / TypeORM / API endpoint / migration / backend テストの **手順****背景知識** をまとめている。
SKILL.md 本体は references への索引だけ。具体的な手順や規約は該当ファイルを Read すること (progressive disclosure)。
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、`packages/backend/` に触れる実装フェーズに入る時点でこのスキルを呼ぶこと。
## 作業別ワークフロー (tasks)
タスク単位の完結したチェックリスト + チェックポイント。新しい何かを足すときに開く。
- 新規 REST API endpoint を追加する → [references/tasks/adding-api-endpoint.md](references/tasks/adding-api-endpoint.md)
- DB migration を作成する (TypeORM CLI / 手書きどちらも) → [references/tasks/creating-migration.md](references/tasks/creating-migration.md)
## 共通知識 (knowledge)
タスクに紐付かない参照リファレンス。複数のタスクから引かれる規約・背景説明。
- NestJS DI / module 登録 / `@Injectable` パターン → [references/knowledge/nestjs-di.md](references/knowledge/nestjs-di.md)
- TypeORM entity / `@Column` / `@Index` パターン (難ケース込み) → [references/knowledge/typeorm-patterns.md](references/knowledge/typeorm-patterns.md)
- API endpoint の `meta` / `paramDef` / `res` 完全早見表 + 落とし穴集 → [references/knowledge/api-meta-paramdef.md](references/knowledge/api-meta-paramdef.md)
- `endpoint-list.ts` への登録方法 (★ 漏れると 404) → [references/knowledge/endpoint-list.md](references/knowledge/endpoint-list.md)
- backend テストの前提 (`.config/test.yml`) と書き方 / e2e ヘルパー一覧 → [references/knowledge/backend-testing.md](references/knowledge/backend-testing.md)
## 必ず最後に通る場所
backend の変更を commit / PR にする前に、必ず [shipping-misskey-change](../shipping-misskey-change/SKILL.md) の最終チェックリストに従う。`pnpm lint` / misskey-js 再生成 / `check-migrations` / SPDX / CHANGELOG をまとめて確認する。
API endpoint を追加・変更したなら、その出口で [misskey-api-reviewer](../../agents/misskey-api-reviewer.md) agent (この skill の規約を review-mode から機械チェックする専門 reviewer) を Task で起動すると、endpoint-list 登録漏れや misskey-js 再生成漏れを取りこぼしにくい。

View file

@ -0,0 +1,368 @@
# API endpoint の meta / paramDef / res 完全早見表
[`IEndpointMeta`](../../../../../packages/backend/src/server/api/endpoints.ts) の全フィールドと AJV `paramDef` の実用パターン、それと PR レビューで頻発する落とし穴を 1 つにまとめたページ。新規 / 既存 endpoint 編集時に開く。
## 目次
- [全フィールド一覧](#全フィールド一覧)
- [権限制限フィールドの使い分け](#権限制限フィールドの使い分け)
- [`kind` の値](#kind-の値)
- [`errors` の書き方](#errors-の書き方)
- [`res` の書き方](#res-の書き方)
- [`paramDef` (AJV) 実用パターン](#paramdef-ajv-実用パターン)
- [OpenAPI への反映マップ](#openapi-への反映マップ)
- [落とし穴](#落とし穴)
## 全フィールド一覧
[endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の `IEndpointMetaBase` 型より。
| フィールド | 型 | デフォルト | 用途 |
|---|---|---|---|
| `stability` | `'deprecated' \| 'experimental' \| 'stable'` | (未指定) | 安定度のヒント。`'deprecated'` を付けた API は新規利用を避ける |
| `tags` | `ReadonlyArray<string>` | — | OpenAPI タグ。実質 `tags[0]` のみが反映される |
| `errors` | `Record<key, { message, code, id }>` | — | クライアントに返す業務エラー定義。各 `id` は UUID v4 で一意 |
| `res` | `Schema` (`@/misc/json-schema.js`) | — | レスポンス JSON Schema。`ref: 'Note'` のような packed entity 参照も可 |
| `requireCredential` | `boolean` | `false` | 認証必須か。`true` のとき `kind` を必ず設定する |
| `requireModerator` | `boolean` | `false` | isModerator ロール必須。`true` のとき `kind` 必須 |
| `requireAdmin` | `boolean` | `false` | isAdministrator ロール必須。`true` のとき `kind` 必須 |
| `requiredRolePolicy` | `KeyOf<'RolePolicies'>` | (未指定) | 特定のロールポリシー (例: `'canCreateChannel'`) を満たすロールを要求 |
| `prohibitMoved` | `boolean` | `false` | アカウント移行済ユーザーを拒否 (主に write 系で検討) |
| `limit` | `{ key?, duration?, max?, minInterval? }` | なし | レート制限。`duration``max` はセットで設定する |
| `requireFile` | `boolean` | `false` | multipart/form-data でファイル添付必須。`true` だと `exec``file` 引数が確実に渡る |
| `secure` | `boolean` | `false` | サードパーティアプリからは利用不可。OpenAPI に "Internal Endpoint" 表記が出る |
| `kind` | `(typeof permissions)[number]` | — | OAuth スコープ。`'read:account'` / `'write:notes'` 等。型は require* 系と相互排他制約あり ([endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の型ユニオン定義) |
| `description` | `string` | — | OpenAPI の operation description に入る |
| `allowGet` | `boolean` | `false` | GET メソッドを許可するか (デフォルトは POST のみ)。冪等な read 系で有用 |
| `cacheSec` | `number` | — | 正常応答に `Cache-Control: public, max-age=<秒>` を付与 |
## 権限制限フィールドの使い分け
[endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) で型ユニオンとして表現されており、組み合わせに制約がある:
| ケース | `requireCredential` | `requireModerator` | `requireAdmin` | `kind` |
|---|---|---|---|---|
| 認証不要 | `false` または省略 | (省略) | (省略) | 不要 |
| 一般ユーザー認証必須 | `true` | (省略) | (省略) | **必須** (`'read:account'` 等) |
| モデレーター以上必須 | (省略) | `true` | (省略) | **必須** (例: `'read:admin:show-user'`) |
| 管理者必須 | (省略) | (省略) | `true` | **必須** (例: `'write:admin:emoji'`) |
| Misskey 本体専用 (`secure: true`) | 任意 | 任意 | 任意 | **不要** (型 union で除外) |
**`secure: true` の例外**: [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の `secure: true` union variant は他の require* と独立しており、`kind` を要求しない。実例: [auth/accept.ts](../../../../../packages/backend/src/server/api/endpoints/auth/accept.ts) (`secure: true + requireCredential: true``kind` なし)、[i/export-user-lists.ts](../../../../../packages/backend/src/server/api/endpoints/i/export-user-lists.ts) も同様。サードパーティアプリから叩けないので OAuth scope の必要がない。
加えて以下も使える:
- **`requiredRolePolicy: 'canCreateChannel'`** — 特定のロールポリシーが許可されているユーザーだけに絞る。**`requireCredential: true` 必須**: [ApiCallService.ts](../../../../../packages/backend/src/server/api/ApiCallService.ts) が `requiredRolePolicy` 分岐で `user!.id` を非null前提アクセスするため、匿名許可と組み合わせると TypeError で 500 になる。匿名も許したいなら、`meta` ではなく実行時に `RoleService.getUserPolicies(me ? me.id : null)` で判定する ([endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) のパターン)。ポリシーの一覧は [`RolePolicies`](../../../../../packages/backend/src/core/RoleService.ts) を参照
- **`secure: true`** — Misskey 本体フロントエンドからしか叩けないようにする (OAuth トークンで叩けなくなる)。上記の通り `kind` は不要
## `kind` の値
完全な一覧は [`packages/misskey-js/src/consts.ts`](../../../../../packages/misskey-js/src/consts.ts) の `permissions` 配列。代表例:
| パターン | 例 |
|---|---|
| 一般 read | `'read:account'`, `'read:notifications'`, `'read:drive'`, `'read:reactions'` |
| 一般 write | `'write:account'`, `'write:notes'`, `'write:reactions'`, `'write:drive'` |
| Admin read | `'read:admin:meta'`, `'read:admin:server-info'`, `'read:admin:show-user'`, `'read:admin:user-ips'` |
| Admin write | `'write:admin:reset-password'`, `'write:admin:suspend-user'`, `'write:admin:emoji'`, `'write:admin:roles'` |
新しい操作領域を追加する場合は `consts.ts``permissions` 配列にも追加する必要がある。
## `errors` の書き方
```ts
errors: {
noSuchNote: { // ← キーは camelCase
message: 'No such note.', // ← 英語ハードコード (バックエンドに i18n 機構なし)
code: 'NO_SUCH_NOTE', // ← code は SCREAMING_SNAKE_CASE
id: '17a0e0fa-3f3e-4f3e-9f3e-3f3e3f3e3f3e', // ← UUID v4。リポジトリ内で一意
httpStatusCode: 404, // ← オプション。HTTP ステータスを上書き
kind: 'client', // ← オプション。'client' (デフォルト) / 'server' / 'permission'
},
},
```
`httpStatusCode``kind` は [error.ts](../../../../../packages/backend/src/server/api/error.ts) の型 `E` 経由で受け付けられる。指定しないとデフォルト挙動 (クライアントエラーは 400 系) になる。
命名規則 (既存実装で一貫):
- キー: `camelCase` (`noSuchNote`, `cannotReRenote`, `alreadyBlocking`, `youHaveBeenBlocked`)
- `code`: `SCREAMING_SNAKE_CASE` (`'NO_SUCH_NOTE'`, `'CANNOT_RENOTE_TO_A_PURE_RENOTE'`)
- 接頭辞パターン: `NO_SUCH_*` / `CANNOT_*` / `ALREADY_*` / `TOO_MANY_*` / `INVALID_*` / `*_REQUIRED`
`throw new ApiError(meta.errors.noSuchNote, { reason: '詳細情報' })` の第 2 引数は `info` に入り、レスポンス JSON の `error.info` として返却される。
## `res` の書き方
JSON Schema または packed entity への参照:
```ts
// 単純なオブジェクト
res: {
type: 'object',
optional: false, nullable: false,
properties: {
count: { type: 'integer' },
},
},
// packed entity 参照
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Note', // ← packages/backend/src/models/json-schema/*.ts の定義名
},
// 配列
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
```
各プロパティに `optional: false, nullable: false`**必ず明示する**。省略すると schema が緩くなり、生成される misskey-js 型も曖昧になる。
## `paramDef` (AJV) 実用パターン
`paramDef` は AJV (`new Ajv({ useDefaults: true })`) でコンパイルされた JSON Schema 7 互換のスキーマ。詳細は [endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の AJV 初期化を参照。
### カスタム format
**`format: 'misskey:id'`** だけが Misskey 独自 ([endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の `addFormat`):
```ts
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
```
その他 (`'date-time'`, `'email'`, `'url'` 等) は JSON Schema 標準。AJV はデフォルトでは format 検証を行わないが、Misskey の AJV 設定ではフォーマット名はバリデーションエラーを出さず通過する程度の動作になっている (ID パターンのみ実際に正規表現検証される)。
### 基本パターン
```ts
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' }, // 必須 ID
text: { type: 'string', minLength: 1, maxLength: 500 }, // 文字長制約
count: { type: 'integer', minimum: 0, maximum: 100, default: 10 },
isPublic: { type: 'boolean', default: false },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
},
required: ['noteId'],
} as const;
```
`as const` を必ず付ける。これで `SchemaType<typeof paramDef>` が型推論される。
### ページネーション (sinceId / untilId / limit)
[notes/timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/timeline.ts):
```ts
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
```
`QueryService.makePaginationQuery(qb, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)` で TypeORM クエリビルダに反映する。
### 配列とアイテム制約
```ts
properties: {
// 一意・最小1・最大100 個のID リスト
noteIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 100,
items: { type: 'string', format: 'misskey:id' },
},
},
```
実例: [notes/show-partial-bulk.ts](../../../../../packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts) (`noteIds`), [notes/drafts/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/drafts/create.ts) (`fileIds` / `visibleUserIds``uniqueItems` 付き)
### `oneOf` / `anyOf` (排他的選択)
複数のリクエストパラメータ形態を許す場合:
```ts
properties: {
userId: { type: 'string', format: 'misskey:id' },
username: { type: 'string' },
host: { type: 'string', nullable: true },
},
anyOf: [
{ required: ['userId'] },
{ required: ['username'] },
],
```
`res` 側でも `oneOf` を使ってバリアントレスポンスを表現できる ([ap/show.ts](../../../../../packages/backend/src/server/api/endpoints/ap/show.ts) の `res`):
```ts
res: {
optional: false, nullable: false,
oneOf: [
{ type: 'object', properties: { type: { enum: ['User'] }, object: { ref: 'UserDetailedNotMe' } } },
{ type: 'object', properties: { type: { enum: ['Note'] }, object: { ref: 'Note' } } },
],
},
```
### `additionalProperties` (動的キー)
固定の `properties` ではなく「任意のキー → 値の型」を表すとき:
```ts
data: {
type: 'object',
additionalProperties: {
anyOf: [{ type: 'number' }],
},
},
```
実例: [retention.ts](../../../../../packages/backend/src/server/api/endpoints/retention.ts), [admin/get-table-stats.ts](../../../../../packages/backend/src/server/api/endpoints/admin/get-table-stats.ts)
`type: 'object', additionalProperties: true` だと「任意の中身を受け入れる」(検証なし) になる。
### `default` (値補完)
AJV を `useDefaults: true` で構築しているため、`default` を書くとリクエストに値が無い場合に自動で埋まる:
```ts
properties: {
includeMyRenotes: { type: 'boolean', default: true },
},
```
クライアントの省略を吸収できるため、後方互換変更で重宝する。
### nullable プロパティ
```ts
properties: {
parentId: { type: 'string', format: 'misskey:id', nullable: true },
},
```
`nullable: true` を付けると `null` を明示的に受け付ける。
## OpenAPI への反映マップ
[gen-spec.ts](../../../../../packages/backend/src/server/api/openapi/gen-spec.ts) より:
| meta フィールド | OpenAPI への反映 |
|---|---|
| `description` | operation description (先頭) |
| `secure: true` | description に "**Internal Endpoint**: ..." の警告 |
| `requireCredential: true` | description に "**Credential required**: *Yes*" + `security: [bearerAuth]` |
| `kind` | description に "**Permission**: *<kind>*" |
| `tags[0]` | operation tag (実質 1 個目のみ) |
| `requireFile: true` | requestBody が `multipart/form-data` になり `file: { type: 'string', format: 'binary' }` が追加される |
| `errors` | examples (operation の `responses` 配下) |
| `res` | response body schema |
| `limit` | `429 Too many requests` レスポンスが `responses` に追加される |
| `allowGet` | 同一 path に `get` operation が追加される (POST と両方が生える) |
**OpenAPI に反映されない (内部のみ)**: `requireModerator` / `requireAdmin` / `requiredRolePolicy` / `prohibitMoved` / `cacheSec` / `stability`
## 落とし穴
PR レビューで頻発するミスを「**症状 → 原因 → 修正**」で集めた。
### 1. エンドポイントが 404 になる
- **症状**: 開発サーバーで叩くと `{"error": {"code": "UNKNOWN_API_ENDPOINT", ...}}` (GET の catch-all 経由)、または素の 404 (POST など)
- **原因**: [endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) への登録漏れ。エンドポイントは glob 自動収集されない
- **修正**: → [knowledge/endpoint-list.md](endpoint-list.md)
### 2. CI `check-misskey-js-autogen` で落ちる
- **症状**: PR に `Please regenerate misskey-js` のコメント
- **原因**: `meta` / `paramDef` / `res` を変えたのに misskey-js の自動生成物を再生成していない
- **修正**: → [shipping-misskey-change/references/tasks/regenerate-misskey-js.md](../../../shipping-misskey-change/references/tasks/regenerate-misskey-js.md)
### 3. CI `spdx` ジョブで落ちる
- **症状**: `SPDX header missing` のメッセージ
- **原因**: 新規 `.ts` ファイルに SPDX ヘッダーが無い
- **修正**: ファイル冒頭に SPDX を貼る。注: `packages/misskey-js/` 配下は MIT 別ライセンスなので SPDX 不要
### 4. クライアントが 500 + error 型不在 を受け取る
- **症状**: フロントエンド側で `result.error.code` を分岐したいが、misskey-js の型に出てこない。レスポンスは 500
- **原因**: `meta.errors` に列挙していないエラーを `throw new ApiError({...})` または `throw new Error(...)` した
- **修正**: 業務エラーは必ず `meta.errors` に登録してから `throw new ApiError(meta.errors.<key>)`
- **逆方向の罠**: 「想定外バグまで全部 `ApiError` で包む」のもダメ。`endpoints/notes/create.ts``catch` 節末尾の `throw err;` が手本
### 5. `me.id``Cannot read properties of null`
- **症状**: 認証なしリクエストで TypeError
- **原因**: `requireCredential: false` のとき `me``MiLocalUser | null` なのに null チェックなしで `me.id` を使った
- **修正**: null チェックを入れるか、認証必須なら `requireCredential: true` に変更
### 6. UUID が他エンドポイントと衝突
- **症状**: `errors.id` を再利用してしまうと misskey-js 側で型が混線
- **原因**: UUID をハードコードして再利用
- **修正**: 衝突確認
```bash
grep -r "id: '<生成した UUID>'" packages/backend/src/server/api/endpoints/
```
新規生成は `node -e "console.log(crypto.randomUUID())"`
### 7. `paramDef``policies` を書く
- **症状**: 「`gtlAvailable: true` を payload で渡してください」のような不自然な API になっている / クライアントが指定したらバイパスできる
- **原因**: ロールポリシーは **動的に取得するもの**
- **修正**: paramDef からは外し、`exec` 内で `RoleService.getUserPolicies(me?.id)` を呼んで判定する
### 8. エラーメッセージを日本語で書く
- **症状**: `message: 'ノートが見つかりません'` のような日本語が i18n されずクライアントに渡る
- **原因**: バックエンドに i18n 機構が無い
- **修正**: `message` は英語ハードコードに統一。フロントエンドは `error.id` (UUID) または `error.code` をキーに自前で localize する
### 9. `as const` を忘れる
- **症状**: `Endpoint<typeof meta, typeof paramDef>` の型推論が壊れて `ps` の型が `any` になる
- **修正**: `export const meta = { ... } as const;``export const paramDef = { ... } as const;` を必ず付ける
### 10. `requireCredential: true` なのに `kind` を書き忘れる
- **症状**: TypeScript の型エラー (`Property 'kind' is missing`)
- **原因**: [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) のユニオン制約で `kind` が型レベルで必須
- **修正**: 適切な OAuth スコープを `kind` に設定する
- **例外**: `secure: true` (Misskey 本体専用) のエンドポイントは [endpoints.ts](../../../../../packages/backend/src/server/api/endpoints.ts) の別 union variant 扱いで `kind` 不要
### 11. `requireFile: true` の cleanup を呼び忘れて一時ファイルが残る
- **症状**: アップロード後にエンドポイントが正常終了/例外終了しても OS の一時ディレクトリにファイルが残り続け、ディスクが埋まる
- **原因**: [endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) が `cleanup` を自動で呼ぶのは **AJV バリデーション失敗時のみ**
- **修正**: `try { ... } finally { cleanup!(); }` で囲む ([drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) の `finally { cleanup!(); }` が手本)
### 12. `requiredRolePolicy` だけで匿名許可してしまう
- **症状**: API を匿名で叩くと 500 + `TypeError: Cannot read properties of null (reading 'id')`
- **原因**: [ApiCallService.ts](../../../../../packages/backend/src/server/api/ApiCallService.ts) が `requiredRolePolicy` ありのエンドポイントで `user!.id` を非null前提でアクセス
- **修正**: 静的に必須ポリシーを宣言するなら `requireCredential: true` と必ず併用する。匿名ユーザーにも違うポリシーセットを適用したいなら、実行時に `RoleService.getUserPolicies(me ? me.id : null)` で判定 ([notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) パターン)
### 13. e2e テストが起動しない
- **症状**: `pnpm --filter backend test:e2e` 実行直後にこける / DB 接続エラー
- **原因**: `.config/test.yml` が無い
- **修正**: → [knowledge/backend-testing.md §前提](backend-testing.md)

View file

@ -0,0 +1,209 @@
# Backend テストの前提と書き方
Misskey backend のテスト構成、`.config/test.yml` の前提、e2e テストのヘルパー関数集を 1 つにまとめたページ。
## 目次
- [前提: `.config/test.yml`](#前提-configtestyml)
- [テスト種別と実行コマンド](#テスト種別と実行コマンド)
- [e2e テストの配置](#e2e-テストの配置)
- [共通 setup](#共通-setup)
- [`api()` ヘルパー](#api-ヘルパー)
- [`signup()` / `post()` / `uploadFile()` 等](#signup--post--uploadfile-等)
- [ローカル DB / Redis](#ローカル-db--redis)
## 前提: `.config/test.yml`
backend のテストスクリプト (`test` / `test:e2e` / `test:fed`) はすべて内部で `cross-env NODE_ENV=test pnpm compile-config` を実行し、`.config/test.yml` を読み込む ([packages/backend/package.json](../../../../../packages/backend/package.json), [packages/backend/scripts/compile_config.js](../../../../../packages/backend/scripts/compile_config.js))。**未作成だとテスト自体が起動しない**。
未作成なら以下を 1 回だけ手動コピーする (どちらでも可):
```bash
ncp .github/misskey/test.yml .config/test.yml
# または
cp .github/misskey/test.yml .config/test.yml
```
補足:
- ルートの `pnpm start:test` (Cypress 用にテストサーバーを起動するコマンド) を使う経路では実行時に `ncp` で自動コピーされる ([package.json](../../../../../package.json))。それ以外で backend テストを直接走らせる時は上記の手動コピーが必要
- すでに `.config/test.yml` があれば各テストスクリプトの内部 `compile-config` で十分なので、追加で `pnpm --filter backend compile-config` を叩く必要はない
- `pnpm start:test` は backend e2e テスト (`pnpm --filter backend test:e2e`) の前提ではない (ポート競合の元になるため使わないこと)
## テスト種別と実行コマンド
| 種別 | 設定ファイル | 実行コマンド |
| --- | --- | --- |
| Unit | `packages/backend/vitest.config.unit.ts` | `pnpm --filter backend test` |
| E2E (HTTP / DB) | `packages/backend/vitest.config.e2e.ts` | `pnpm --filter backend test:e2e` |
| Federation | `packages/backend/vitest.config.fed.ts` | `pnpm --filter backend test:fed` |
- 配置: `packages/backend/test/` 配下
- カバレッジ: `pnpm --filter backend test-and-coverage`
## e2e テストの配置
`packages/backend/test/e2e/` の現状ファイル例:
```
note.ts ノート関連 (作成・renote・visibility・添付ファイル等)
users.ts ユーザー関連
timelines.ts タイムライン
drive.ts ドライブ (アップロード/ダウンロード)
clips.ts クリップ
oauth.ts OAuth フロー
streaming.ts WebSocket
api.ts API レイヤ全般 (認証・レート制限など)
api-visibility.ts 公開範囲チェック
endpoints.ts 上記カテゴリに収まらない雑多なもの
2fa.ts 2FA
block.ts / mute.ts / antennas.ts / clips.ts / move.ts / nodeinfo.ts / ...
```
**`admin.ts` は存在しない**。admin 系エンドポイントの e2e は `api.ts` (API レイヤ挙動として) または `endpoints.ts` (雑多枠) に置くのが現実的。
### 判断ルール
1. 自分の追加するエンドポイントが既存カテゴリファイル (`note.ts`, `users.ts` 等) に所属するなら、そこに `describe('...', () => { test(...) })` を追加
2. どのカテゴリにも収まらないなら `endpoints.ts` に追加
3. テストケースが多くなり (>200 行)、独立性が高い場合のみ新ファイル化
`describe` のラベル名は **人間可読** で OK (`describe('Note', ...)`, `describe('管理者操作', ...)` のような形式)。`<category>/<name>` 形式である必要はない。
## 共通 setup
`packages/backend/test/setup.e2e.ts` (vitest の `setupFiles`) が各テストファイル共通の `beforeAll` (テスト DB 初期化 + 環境リセット) を登録する。テストサーバーの起動/停止は別途 vitest の `globalSetup` (`test-server/entry.ts``setup()` / `teardown()`) が担う。各テストファイルでは自前の `beforeAll` でユーザーを用意する:
```ts
import { describe, test, beforeAll, afterAll } from 'vitest';
import * as assert from 'node:assert';
import { api, signup, post, role, uploadFile } from '../utils.js';
import type { UserToken } from '../utils.js';
describe('機能名', () => {
let alice: UserToken;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
});
test('正常系', async () => {
const res = await api('<category>/<name>', { /* params */ }, alice);
assert.strictEqual(res.status, 200);
});
});
```
## `api()` ヘルパー
[test/utils.ts](../../../../../packages/backend/test/utils.ts) の `api()`:
```ts
const res = await api('<category>/<name>', params, me?);
// res.status : HTTP ステータス (200 / 400 / 401 / 403 / 500 等)
// res.headers : Headers
// res.body : レスポンス JSON (型は misskey.Endpoints から自動推論)
```
`me?` を省略すると未認証リクエスト。`me` を渡すとそのユーザーの token で叩く。
### エラーレスポンスの検証
```ts
test('存在しないノートで怒られる', async () => {
const res = await api('notes/show', { noteId: '0000000000000000' }, alice);
assert.strictEqual(res.status, 400);
assert.strictEqual(castAsError(res.body as any).error.code, 'NO_SUCH_NOTE');
});
```
`castAsError(...).error.code``meta.errors.<key>.code` を検証できる ([test/utils.ts](../../../../../packages/backend/test/utils.ts) の `castAsError`)。
## `signup()` / `post()` / `uploadFile()`
### `signup()` — テストユーザー作成
```ts
const alice = await signup({ username: 'alice' }); // 既定パスワード 'test'
const bob = await signup({ username: 'bob', password: 'secret123' });
```
戻り値はサインアップレスポンス (token を含む) で、`api()` の第 3 引数にそのまま渡せる。
### `post()` — ノート投稿
```ts
const note = await post(alice, { text: 'hello' });
// 戻り値は misskey.entities.Note
```
複雑な公開範囲・添付ファイル付きでも `post(alice, { text: ..., visibility: 'specified', visibleUserIds: [...], fileIds: [...] })` のように渡せる。
### `uploadFile()` — ドライブにファイルアップロード
```ts
const file = await uploadFile(alice); // resources/192.jpg をアップロード
const file2 = await uploadFile(alice, { path: '192.png' }); // resources/192.png
const file3 = await uploadFile(alice, { blob: new Blob([...]) }); // 任意 Blob
// file.body.id を fileIds に渡せる
```
### `role()` — ロール作成 + アサイン
[test/utils.ts](../../../../../packages/backend/test/utils.ts) の `role()`:
```ts
const myRole = await role(adminUser, { name: 'tester' }, { canCreateChannel: { useDefault: false, priority: 0, value: true } });
// admin/roles/create を叩く。policies 引数で個別ポリシーを上書き可能
```
モデレーター・管理者ロールが要るテストは事前に `signup({ ... })` + `role(...)` で作る。
### `createAppToken()` — アプリ scope 付きトークン
```ts
const token = await createAppToken(alice, ['write:notes', 'read:account']);
// token は文字列。api() の me.token として使うか、{ token, bearer: true } で渡せば Bearer Auth で叩く
```
OAuth scope (`kind`) のテストに使う。
### その他のヘルパー
[test/utils.ts](../../../../../packages/backend/test/utils.ts) には以下も用意されている:
- `userList()` — ユーザーリスト作成
- `page()` / `play()` — Page / Flash 作成
- `clip()` / `galleryPost()` / `channel()` — 各種リソース作成
- `react()` — リアクション
- `simpleGet()` — fetch ラッパ (raw HTTP)
- `testPaginationConsistency()` — ページネーション挙動の網羅検証
- `sendEnvUpdateRequest()` / `sendEnvResetRequest()` — テスト用環境変数の更新
- `connectStream()` / `waitFire()` — WebSocket (Streaming API)
詳細はソースを直接参照。
### 既存テスト例
- [test/e2e/note.ts](../../../../../packages/backend/test/e2e/note.ts) — `describe('Note', ...)` で多数の `test(...)` を並べる伝統的なスタイル
- [test/e2e/endpoints.ts](../../../../../packages/backend/test/e2e/endpoints.ts) — カテゴリ不問の雑多なエンドポイント
- [test/e2e/api.ts](../../../../../packages/backend/test/e2e/api.ts) — API レイヤ (認証・レート制限) の挙動
## ローカル DB / Redis
backend の **テスト****開発** では用途別に別の compose ファイルを使う。ポートが異なるので混同すると接続できない。
| 用途 | compose ファイル | host ポート (db / redis) |
| --- | --- | --- |
| テスト (`test` / `test:e2e` / `test:fed`) | [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml) | `54312` / `56312` ([.github/misskey/test.yml](../../../../../.github/misskey/test.yml) のポート設定と一致) |
| 開発 (`pnpm dev` 等) | `compose.local-db.yml` (リポジトリルート) | `5432` / `6379` |
```bash
# テスト用 DB / Redis (テスト時はこちら)
docker compose -f packages/backend/test/compose.yml up -d
# 開発用 DB / Redis (Misskey 本体は起動せず postgres / redis / meilisearch だけ立てる)
docker compose -f compose.local-db.yml up -d
```
`compose.local-db.yml` は開発向け (標準ポート `5432` / `6379`) で、テスト用 DB (`test-misskey` / ポート `54312` / `56312`) とは別物。CI (`.github/workflows/test-backend.yml`) は docker compose ではなく GitHub Actions の `services:` で同じテスト用ポートの postgres / redis コンテナを立ててから走る。

View file

@ -0,0 +1,50 @@
# `endpoint-list.ts` への登録
新規 API endpoint を追加する際の **最大の落とし穴**。エンドポイントは glob 自動収集されないため、ここへの 1 行追加を忘れると 404 になる。
## なぜ必要か
[`packages/backend/src/server/api/EndpointsModule.ts`](../../../../../packages/backend/src/server/api/EndpointsModule.ts) が [`endpoint-list.ts`](../../../../../packages/backend/src/server/api/endpoint-list.ts) の全エクスポートを `Object.entries()` で反復し、NestJS provider (`provide: 'ep:<path>'`) を生成している。**このリストが API ルーティングの単一の真実** で、ここに無いものは存在しないものとして扱われる。
## 登録方法
[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) の **同カテゴリ内** に 1 行追加する:
```ts
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
```
`<category>` は機能領域 (`notes`, `users`, `admin/announcements` 等)、`<name>` はエンドポイント名 (`create`, `show`, `delete` 等)。両方ともケバブケース / スラッシュ区切りで、ファイルシステムのパス構造と一致する。
例: `endpoints/notes/create.ts` を追加するなら:
```ts
export * as 'notes/create' from './endpoints/notes/create.js';
```
## 並び順
**並び順は厳密ではない**。同じディレクトリ (例: `admin/queue/*`) の中でも、アルファベット順ではなく追加された経緯どおりの順になっている箇所が多い。
- **新規追加**: 同カテゴリ内の末尾に追加すれば OK
- **既存近傍**: 同カテゴリ内の関連エンドポイントの近くに置く判断もあり
- **過度に整理しない**: 既存の並びを全部 sort し直すような PR は不要 (review コストだけ増える)
## 登録確認
ファイルを追加した後、grep で 1 行存在することを確認する:
```bash
grep -F "'<category>/<name>'" packages/backend/src/server/api/endpoint-list.ts
```
ヒットしなければ登録漏れ。
## 既存例 (登録漏れに気づくための grep 例)
`endpoint-list.ts` の冒頭コメントに「このリストが API ルーティングの単一の真実」という旨が記載されている。新規開発時はこのファイルを開いてカテゴリ単位の構造を把握してから新規 endpoint ファイルを書くのが効率的。
## 関連
- 新規 endpoint 追加の全手順 → [tasks/adding-api-endpoint.md](../tasks/adding-api-endpoint.md)
- NestJS DI / module 構造 → [nestjs-di.md](nestjs-di.md)

View file

@ -0,0 +1,97 @@
# NestJS DI / module 登録パターン
Misskey の backend は NestJS 11 + Fastify 5 + TypeORM 1 (PostgreSQL) + Redis の構成。DI コンテナと Repository パターンが軸。
## アーキテクチャ
- **DI コンテナ**: NestJS の `@Injectable()` サービス + Repository (TypeORM) パターン
- **DI トークン**: [`@/di-symbols.js`](../../../../../packages/backend/src/di-symbols.ts) の `DI` から `@Inject(DI.xxx)` で注入
- **ビルド**: `rolldown -c``built/` にバンドル。型チェックは `tsgo`
## エンドポイント内での DI
API endpoint は `Endpoint<typeof meta, typeof paramDef>` を extends するクラスとして書く。`@Injectable()` を付けてコンストラクタで Repository / Service を `@Inject(DI.xxx)` で注入する。
```ts
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
// 他にも RoleService, UserEntityService, GlobalEventService 等を必要なだけ inject
) {
super(meta, paramDef, async (ps, me) => {
// this.notesRepository.findOneBy(...) のように使う
});
}
}
```
`// eslint-disable-line import/no-default-export` は Endpoint のお約束 (NestJS が default export を要求する一方で、ESLint ルールでは制約されているため)。
## 主要 DI トークン
`@/di-symbols.js` から提供される。代表例:
| トークン | 型 | 用途 |
|---|---|---|
| `DI.notesRepository` | `NotesRepository` | notes テーブルの TypeORM Repository |
| `DI.usersRepository` | `UsersRepository` | users テーブル |
| `DI.driveFilesRepository` | `DriveFilesRepository` | drive_file テーブル |
| `DI.config` | `Config` | アプリ設定 |
| `DI.redis` | `Redis` | Redis クライアント |
| `DI.db` | `DataSource` | TypeORM DataSource (raw SQL を打ちたい時) |
Service 系 (例: `NoteCreateService`, `RoleService`, `UserEntityService`) は **トークン経由ではなく型をそのまま inject** する:
```ts
constructor(
private roleService: RoleService,
private userEntityService: UserEntityService,
) {}
```
## Service クラスの書き方
Service は `@Injectable()` を付け、必要な依存をコンストラクタで宣言する。NestJS の module (`packages/backend/src/core/CoreModule.ts` 等) に provider として登録される必要がある。
```ts
@Injectable()
export class MyService {
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private roleService: RoleService,
) {}
async doSomething(noteId: string) {
const note = await this.notesRepository.findOneBy({ id: noteId });
// ...
}
}
```
新規 Service を追加する場合は **module 側の `providers` 配列にも追加** する必要がある。既存 Service が `CoreModule` に登録されているか確認するのが手っ取り早い。
## Module 構造
主要 module は以下:
- **CoreModule** (`src/core/CoreModule.ts`) — Service 群を集約
- **EndpointsModule** (`src/server/api/EndpointsModule.ts`) — endpoint-list.ts を `Object.entries()` で反復して NestJS provider (`provide: 'ep:<path>'`) を自動生成
- **GlobalModule** (`src/GlobalModule.ts`) — Repository / Config / Redis / DataSource など低レベル依存
- **QueueModule** (`src/core/QueueModule.ts`) — BullMQ ジョブキュー
新規 endpoint 追加時に module への明示的な登録は不要 ([knowledge/endpoint-list.md](endpoint-list.md) 参照)。新規 Service 追加時は CoreModule (または該当 module) に provider 登録が必要。
## 既存例 (DI / 例外処理が綺麗な参考実装)
- [endpoints/notes/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts) — Service を型注入 (`NoteEntityService` / `NoteCreateService`) + `meta.errors` + `try/catch` で業務エラー変換 + 末尾 `throw err;` の二段構え
- [endpoints/i/pin.ts](../../../../../packages/backend/src/server/api/endpoints/i/pin.ts) — `.catch(err => { ... throw err; })` で同様にエラー変換
- [endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) — `RoleService.getUserPolicies()` で動的ポリシー判定

View file

@ -0,0 +1,160 @@
# TypeORM / migration パターン
Misskey backend は TypeORM 1 + PostgreSQL。エンティティ定義と migration の関係、そして migration で踏みうる難ケースをまとめる。
## モデル / Repository
- エンティティ: `packages/backend/src/models/<Name>.ts` (`@Entity` + `@Column`)
- DI 経由で注入される Repository を経由してアクセス (`@Inject(DI.notesRepository)` 等) → [nestjs-di.md](nestjs-di.md)
エンティティ側の `@Column` / `@Entity` / `@Index` 変更は migration の DDL と整合させる必要がある。`pnpm --filter backend check-migrations` がエンティティと migration の不一致を検出する ([scripts/check_migrations_clean.js](../../../../../packages/backend/scripts/check_migrations_clean.js))。
## migration ファイルの構造
各ファイル `packages/backend/migration/{unixMs}-{descriptive-name}.js` は ESM JS。最小形:
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PascalCaseName1234567890123 {
name = 'PascalCaseName1234567890123'
async up(queryRunner) {
await queryRunner.query(`...`);
}
async down(queryRunner) {
await queryRunner.query(`...`); // up の完全な巻き戻し
}
}
```
詳細手順は [tasks/creating-migration.md](../tasks/creating-migration.md) を参照。**マージ済 migration の編集は絶対禁止**。
## CONCURRENTLY (CREATE INDEX CONCURRENTLY) の扱い
大規模テーブルへの `CREATE INDEX` は本番で長時間ロックする恐れがある。`CONCURRENTLY` で発行するときは migration class に **「この migration は transaction を張らない」と指示する** 必要がある。PostgreSQL は `CREATE INDEX CONCURRENTLY` を transaction 内で実行できないため。
参照実装: [migration/1745378064470-composite-note-index.js](../../../../../packages/backend/migration/1745378064470-composite-note-index.js)
```js
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
export class CompositeNoteIndex1745378064470 {
name = 'CompositeNoteIndex1745378064470';
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
async up(queryRunner) {
const concurrently = isConcurrentIndexMigrationEnabled;
if (concurrently) {
// CREATE INDEX CONCURRENTLY ...
} else {
// CREATE INDEX ...
}
}
async down(queryRunner) {
// 同様に環境変数で分岐
}
}
```
要点:
- **`transaction = isConcurrentIndexMigrationEnabled ? false : undefined;`** が必須。これがないと `CREATE INDEX CONCURRENTLY` が transaction 内で実行されて `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block` で失敗
- 環境変数 `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` がデフォルト OFF。OFF のときは普通の `CREATE INDEX` (transaction 内) で動く必要がある。`up`/`down` 双方を環境変数で分岐させる
- `ormconfig.js``migrationsTransactionMode`**環境変数で切り替わる**: `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY=1` のときだけ `'each'`、未設定時は `'all'` (全 migration を 1 つの transaction でラップ) ([ormconfig.js](../../../../../packages/backend/ormconfig.js) の `migrationsTransactionMode`)。普段は `'all'` 前提
## migration 難ケース集
`migration:generate` / 手書きどちらでも踏み外しやすいパターンを「**なぜ危険か → up の形 → down 戦略 → 参照実装**」でまとめる。
共通の鉄則: `down()``up()`**完全な巻き戻し**。下記ケースは「単純な逆 SQL では戻らない」ものが多い。
### 1. NOT NULL 列の追加
**なぜ危険か**: 既存行があるテーブルに `NOT NULL` 列を `DEFAULT` 無しで足すと、既存行を埋められず `ALTER TABLE` が失敗する。
- **既定値で良い場合**`DEFAULT` を付ければ 1 文で済む。これが最も多い
```js
// up
await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`);
// down
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`);
```
参照: [migration/1758677617888-scheduled-post.js](../../../../../packages/backend/migration/1758677617888-scheduled-post.js)
- **行ごとに計算した値で埋めたい / 既定値を後で外したい場合** — 3 段に分ける: ①nullable で追加 → ②`UPDATE` でバックフィル (ケース 3 参照) → ③`ALTER COLUMN ... SET NOT NULL``down``DROP COLUMN` で良い。巨大テーブルでは ② の `UPDATE` と ③ の `SET NOT NULL` (全行スキャン) が長時間ロックし得る点に注意
**補足:** エンティティ側で `@Column({ default: ... })` を付けると `migration:generate``DEFAULT` 付き DDL を出す。アプリ実行時に常に値を入れるので DB 既定値が不要なら、生成後に `DEFAULT` 句だけ手で外す判断もある (既存 migration には両スタイルある)。
### 2. enum 型の値の追加・変更
**なぜ危険か**: PostgreSQL の enum は **値を削除できない** (`ALTER TYPE ... DROP VALUE` は存在しない) ため、`ADD VALUE` した変更を素直に巻き戻せない。さらに Misskey はデフォルトで migration 全体を 1 トランザクションにまとめる (`migrationsTransactionMode: 'all'`) ので、`ADD VALUE` で足した値を同一トランザクション内で使う処理もエラーになる。そこで TypeORM `migration:generate`**「旧型を rename → 新型を CREATE → 列を新型へ ALTER (USING キャスト) → 旧型を DROP」** という巻き戻し可能な手順を出す。手書きでもこの形に従うこと。
```js
// up: 値 'app' を追加する例 (新値を含む型へ載せ替える)
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', /* ... */ 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
```
```js
// down: 新値を含まない旧い値集合へ同じ手順で戻す
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', /* ... 'app' を除く ... */)`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
```
要点: ①列がデフォルトを持つ場合は ALTER 前に `DROP DEFAULT`、ALTER 後に `SET DEFAULT` を挟む。②配列列 (`mutingNotificationTypes` 等) は `TYPE "..."[] USING "col"::"text"::"..."[]` と配列キャストにする。③**`down` の落とし穴**: 削除する値を既存行が使っていると `USING` キャストが「該当 enum に存在しない」で失敗する。新値を追加しただけの直後の巻き戻しは安全だが、運用後に使われた値を消す巻き戻しは本質的に危うい — その場合は down で先に `UPDATE ... SET "type" = '<代替値>' WHERE "type" = '<消す値>'` で退避してからキャストする。
参照: [migration/1674118260469-achievement.js](../../../../../packages/backend/migration/1674118260469-achievement.js) (rename/recreate の完全な up/down)。型の新規作成は [migration/1580276619901-v12-10.js](../../../../../packages/backend/migration/1580276619901-v12-10.js)。
### 3. データ移行 (UPDATE バックフィル)
**なぜ危険か**: migration 内の `UPDATE` は本番の全行を触る可能性がある。大量行では長時間ロック・トランザクション肥大を招く。
- 既定値を入れるだけなら `UPDATE ... WHERE col IS NULL` で冪等に書く。複数回流れても安全な形にする
- 巨大テーブルの全行更新は避けるのが基本。どうしても必要なら CONCURRENTLY 同様にバッチ分割や別運用を検討し、PR で相談する
- `down` で元値に戻せないデータ移行 (情報が失われる変換) は、`down` に戻せない旨をコメントで明示し、最低限スキーマだけは巻き戻す
```js
// up: nullable 追加 → バックフィル → NOT NULL 化
await queryRunner.query(`ALTER TABLE "user_profile" ADD "github" boolean`);
await queryRunner.query(`UPDATE "user_profile" SET "github" = FALSE WHERE "github" IS NULL`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "github" SET NOT NULL`);
```
### 4. JSONB / 配列列のデフォルト
**なぜ危険か**: 既定値リテラルの書式を誤ると `migration:generate` の出力とズレてスタイル不一致になる。実績ある書式に揃える。
```js
await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`); // オブジェクト
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD "logs" jsonb NOT NULL DEFAULT '[]'`); // 配列(JSON)
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`); // PG 配列型
```
参照: [migration/1565634203341-room.js](../../../../../packages/backend/migration/1565634203341-room.js), [migration/1704959805077-bubble-game-record.js](../../../../../packages/backend/migration/1704959805077-bubble-game-record.js), [migration/1557476068003-PinnedUsers.js](../../../../../packages/backend/migration/1557476068003-PinnedUsers.js)。`down` はいずれも `DROP COLUMN`
### 5. 安全な DROP と COMMENT
- **DROP の冪等性**: 状況により対象が無いことがある DROP は `IF EXISTS` を付ける (`DROP INDEX IF EXISTS "..."`)。ただし `migration:generate` は通常 `IF EXISTS` を付けない素の DDL を出すので、手で足すのは「条件付きで存在する」と分かっている時だけにする (無闇に付けると本来検出すべき不整合を隠す)
- **COMMENT ON COLUMN**: Misskey は denormalize した列に `'[Denormalized]'` コメントを付ける慣習がある。エンティティの `@Column({ comment: '[Denormalized]' })` に対応して `migration:generate``COMMENT ON COLUMN` を出す。`up` で付与したら `down` でも対称に書く
```js
await queryRunner.query(`COMMENT ON COLUMN "note"."renoteChannelId" IS '[Denormalized]'`);
```
参照: [migration/1761569941833-add-channel-muting.js](../../../../../packages/backend/migration/1761569941833-add-channel-muting.js)
### 6. 列リネーム
`migration:generate` はエンティティのプロパティ名変更を **「DROP 旧列 + ADD 新列」** と解釈しがちで、これだと **データが消える**。意図がリネームなら生成 SQL を捨て、手書きで `ALTER TABLE "t" RENAME COLUMN "old" TO "new"` (down は逆) に直す。生成結果を鵜呑みにしないこと。

View file

@ -0,0 +1,291 @@
# 新規 REST API endpoint を追加する
`packages/backend/src/server/api/endpoints/<category>/<name>.ts` に新規エンドポイントを追加するための手順。**配線フェーズの `endpoint-list.ts` 登録を忘れると 404** になるので、まずそこを念頭に置く。
## 最重要事実 (見落とすと CI / 本番が壊れる)
1. **エンドポイントは glob 自動収集されない**。[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) への 1 行追加が必須 → [knowledge/endpoint-list.md](../knowledge/endpoint-list.md)
2. **`meta` / `paramDef` / `res` を変えたら misskey-js 再生成が必須**。`pnpm build-misskey-js-with-types` を忘れると CI の `check-misskey-js-autogen` で必ず落ちる
3. **`meta.errors` の各 `id` は UUID v4 で、リポジトリ内で一意**。`crypto.randomUUID()` で生成し、`grep -r "id: '<UUID>'" packages/backend/src/server/api/endpoints/` で衝突確認
## ワークフロー全体図
```
1. 設計 : エンドポイントの種類を決める (read/write × 認証要否 × 権限)
2. 実装 : meta / paramDef / クラス本体を書く (SPDX ヘッダー付き)
3. 配線 : endpoint-list.ts に登録 (★ 忘れると 404)
4. 検証 : e2e テスト + lint + misskey-js 再生成
5. 仕上げ : CHANGELOG エントリ (shipping-misskey-change で確認)
```
---
## 1. 設計フェーズ — どのテンプレートをベースにするか
まず作るエンドポイントの性質を確定させる。**既存実装をテンプレートとしてコピペ起点にするのが最短路**。
| 性質 | ベースにする既存実装 |
|---|---|
| 認証不要・パラメータなし・小さなレスポンス | [endpoints/ping.ts](../../../../../packages/backend/src/server/api/endpoints/ping.ts) |
| 認証必須・DI で Repository / Service を注入・errors あり | [endpoints/notes/create.ts](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts) |
| ページネーション (sinceId/untilId/limit) | [endpoints/notes/timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/timeline.ts) |
| ロールポリシー (動的) ベースのアクセス制御 | [endpoints/notes/global-timeline.ts](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts) — `RoleService.getUserPolicies()` を使う |
| ファイル添付 (`requireFile: true`) | [endpoints/drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) |
| moderator / admin 専用 | [endpoints/admin/suspend-user.ts](../../../../../packages/backend/src/server/api/endpoints/admin/suspend-user.ts) (moderator), [endpoints/admin/roles/create.ts](../../../../../packages/backend/src/server/api/endpoints/admin/roles/create.ts) (admin) |
`<category>` は機能領域 (例: `notes`, `users`, `admin/announcements`)。ディレクトリは既存に倣う。
---
## 2. 実装フェーズ
### 2.1 SPDX ヘッダー (必須)
新規 `.ts` ファイル冒頭に必ず付ける (欠落すると CI の `spdx` ジョブで失敗):
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
**注:** `packages/misskey-js/src/autogen/` 配下にも diff が出るが、**misskey-js は MIT ライセンス** で別管理 (`packages/misskey-js/package.json:license` = MIT) なので SPDX ヘッダーは付けない / 不要。
### 2.2 最小テンプレート (認証不要 read 系)
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
export const meta = {
tags: ['<tag>'],
requireCredential: false,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
// ...
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
) {
super(meta, paramDef, async (ps, me) => {
// 実装。me は MiLocalUser | null (requireCredential: false のため null チェック必須)
});
}
}
```
### 2.3 DI / errors / limit を含むテンプレート
```ts
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
requireCredential: true, // 認証必須 → kind 必須 (例外: secure: true な内部 API は kind 不要)
kind: 'write:notes', // OAuth scope (一覧は packages/misskey-js/src/consts.ts の `permissions`)
prohibitMoved: false, // 移行済アカウントを拒否するか
limit: {
duration: 1000 * 60 * 60, // 1 時間
max: 300,
},
errors: {
noSuchNote: { // ← キーは camelCase
message: 'No such note.', // ← 英語ハードコード (バックエンドに i18n 機構なし)
code: 'NO_SUCH_NOTE', // ← code は SCREAMING_SNAKE_CASE
id: '17a0e0fa-3f3e-4f3e-9f3e-3f3e3f3e3f3e', // ← crypto.randomUUID() で生成し衝突確認
},
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Note', // packed entity を参照する場合
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['noteId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
) {
super(meta, paramDef, async (ps, me) => {
// requireCredential: true なので me は MiLocalUser (null になり得ない)
const note = await this.notesRepository.findOneBy({ id: ps.noteId });
if (note == null) throw new ApiError(meta.errors.noSuchNote);
// 実装
});
}
}
```
DI / module 登録の詳細は [knowledge/nestjs-di.md](../knowledge/nestjs-di.md) を参照。
### 2.4 `exec` 関数のフルシグネチャ
`super(meta, paramDef, cb)``cb` が受け取る引数は 7 つある ([endpoint-base.ts](../../../../../packages/backend/src/server/api/endpoint-base.ts) の `Executor` 型):
```ts
async (ps, me, token, file, cleanup, ip, headers) => { ... }
```
| 引数 | 型 | 用途 |
|---|---|---|
| `ps` | `SchemaType<typeof paramDef>` | AJV 検証済の入力 |
| `me` | `MiLocalUser` (requireCredential: true) / `MiLocalUser \| null` (false) | ローカルユーザー。`requireCredential: false` のとき必ず null チェック |
| `token` | `MiAccessToken \| null` | OAuth トークン (アプリ識別が要るとき) |
| `file` | `{ name, path } \| undefined` | `requireFile: true` のときのみ確実に渡る。エンドポイント基底クラスが既に null チェック済 |
| `cleanup` | `() => any \| undefined` | アップロードされた一時ファイルを削除するコールバック。**基底クラスが自動で呼ぶのは AJV バリデーション失敗時だけ**。正常終了や endpoint 内例外時は **呼ばれない** ので、`try { ... } finally { cleanup!(); }` で必ず呼ぶ責務がある ([drive/files/create.ts](../../../../../packages/backend/src/server/api/endpoints/drive/files/create.ts) の `finally { cleanup!(); }` が手本) |
| `ip` | `string \| null \| undefined` | クライアント IP |
| `headers` | `Record<string, string> \| null \| undefined` | リクエストヘッダ |
ほとんどのエンドポイントは `(ps, me)` だけで十分。`token` / `ip` / `headers` まで使うのは admin / debug / auth 系のごく一部。
### 2.5 meta / paramDef の規約
頻出 5 件 (`tags` / `requireCredential` / `kind` / `limit` / `errors`) の使い方や全フィールド一覧、`requiredRolePolicy` / `secure` / `cacheSec` / `allowGet` 等、それと `paramDef` の AJV 実用パターンは → [knowledge/api-meta-paramdef.md](../knowledge/api-meta-paramdef.md)。
### 2.6 エラー throw のバランス
**クライアントに返すべき業務エラー** は必ず `meta.errors` に列挙して `throw new ApiError(meta.errors.<key>)` する。これを守らないと misskey-js 側の型に出ず、レスポンスも 500 になる。第 2 引数で追加情報を渡せる:
```ts
throw new ApiError(meta.errors.invalidParam, { reason: 'too short' });
```
一方で **想定外の例外 (DB 不整合 / 下層 service の bug / 防御的アサーション)**`throw new Error('...')` のままで構わない。すべての例外を `ApiError` で包むと、未知のバグが client error として隠蔽されてしまう。`endpoints/notes/create.ts``catch` 節末尾の `throw err;` がこの二段構えの典型。
---
## 3. 配線フェーズ — endpoint-list.ts に登録 ★必須
[endpoint-list.ts](../../../../../packages/backend/src/server/api/endpoint-list.ts) の **同カテゴリ内** に 1 行追加する:
```ts
export * as '<category>/<name>' from './endpoints/<category>/<name>.js';
```
詳細・落とし穴は [knowledge/endpoint-list.md](../knowledge/endpoint-list.md) を参照。**ここへの登録漏れ = 404**。
---
## 4. 検証フェーズ
### 4.1 e2e テスト
[packages/backend/test/e2e/](../../../../../packages/backend/test/e2e/) の構造は **機能カテゴリごとのファイル分け** (`note.ts` / `users.ts` / `timelines.ts` / `drive.ts` / `clips.ts` / `oauth.ts` 等)。
- 既存のカテゴリファイルがあるなら、そこに `describe('<人間可読ラベル>', () => { test('正常系', ...) })` で追加
- どのファイルにも合わないなら `test/e2e/endpoints.ts` に追加
- `describe` 名は **人間可読 OK**
最小例 (詳細なヘルパー一覧は → [knowledge/backend-testing.md](../knowledge/backend-testing.md)):
```ts
import { describe, test } from 'vitest';
import * as assert from 'node:assert';
import { api, signup } from '../utils.js';
describe('<人間可読ラベル>', () => {
test('正常系', async () => {
const alice = await signup({ username: 'alice' });
const res = await api('<category>/<name>', { /* params */ }, alice);
assert.strictEqual(res.status, 200);
});
});
```
実行 (前提: `.config/test.yml` — [knowledge/backend-testing.md](../knowledge/backend-testing.md) §前提 参照):
```bash
pnpm --filter backend test:e2e
```
### 4.2 lint / typecheck
```bash
# 個別ファイルを高速にチェック
pnpm exec eslint --fix packages/backend/src/server/api/endpoints/<category>/<name>.ts
pnpm --filter backend typecheck # tsgo --noEmit (backend のみ)
# 一括 (PR 提出前)
pnpm --filter backend lint
```
### 4.3 misskey-js 再生成 (★必須)
`meta` / `paramDef` / `res` を変えたら必ず:
```bash
pnpm build-misskey-js-with-types
```
PR に `packages/misskey-js/src/autogen/` 配下の差分が含まれていないと CI の `check-misskey-js-autogen` で必ず落ちる (最頻ミス)。詳細手順は [shipping-misskey-change/references/tasks/regenerate-misskey-js.md](../../../shipping-misskey-change/references/tasks/regenerate-misskey-js.md)。
---
## 5. 仕上げフェーズ — CHANGELOG
ユーザー影響がある (新機能 / 既存挙動変更) なら `CHANGELOG.md``## Unreleased``### Server` に 1 行追加する。詳細は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) に従う。
---
## 落とし穴サマリ (PR で頻発するミス)
詳細な症状 → 原因 → 修正 のフォーマット → **[knowledge/api-meta-paramdef.md](../knowledge/api-meta-paramdef.md) §落とし穴**
- **404 になる**`endpoint-list.ts` 登録漏れ
- **CI `check-misskey-js-autogen` で落ちる**`pnpm build-misskey-js-with-types` 忘れ
- **CI `spdx` で落ちる** → SPDX ヘッダー欠落
- **クライアントが 500 と error 型不在を受け取る**`meta.errors` 列挙なしに `throw new ApiError(...)` した
- **`me.id` で TypeError** → `requireCredential: false` で null チェックを忘れた
- **UUID 重複** → 衝突確認グレップを忘れた
- **一時ファイルが残る**`requireFile: true``cleanup!()``finally` で呼び忘れた
- **`requiredRolePolicy` で匿名アクセスが 500 になる** → `ApiCallService``user!.id` を非null前提で参照するため `requireCredential: true` 必須
---
## 参照ファイル
### コードベース
- [endpoints.ts (meta/paramDef 型定義)](../../../../../packages/backend/src/server/api/endpoints.ts)
- [endpoint-base.ts (Endpoint 基底クラス)](../../../../../packages/backend/src/server/api/endpoint-base.ts)
- [endpoint-list.ts (★ ここに登録)](../../../../../packages/backend/src/server/api/endpoint-list.ts)
- [error.ts (ApiError)](../../../../../packages/backend/src/server/api/error.ts)
- [endpoints/ping.ts (最小例)](../../../../../packages/backend/src/server/api/endpoints/ping.ts)
- [endpoints/notes/create.ts (DI + errors の典型)](../../../../../packages/backend/src/server/api/endpoints/notes/create.ts)
- [endpoints/notes/global-timeline.ts (policies 動的チェック)](../../../../../packages/backend/src/server/api/endpoints/notes/global-timeline.ts)
- [test/e2e/endpoints.ts (テスト例)](../../../../../packages/backend/test/e2e/endpoints.ts)
- [test/utils.ts (api/signup/post 等のヘルパー)](../../../../../packages/backend/test/utils.ts)
- [scripts/generate_api_json.js (misskey-js 生成元)](../../../../../packages/backend/scripts/generate_api_json.js)

View file

@ -0,0 +1,180 @@
# DB migration を作成する
`packages/backend/migration/` に新規 TypeORM マイグレーションを追加するための手順。
## 大前提 (絶対 NG)
- **既にマージ済み (develop / master) のマイグレーションファイルを編集しない** ([AGENTS.md](../../../../../AGENTS.md))。本番履歴の改変は深刻なデータ不整合を引き起こす。スキーマ変更は **常に新しいタイムスタンプで新規ファイル** を作る
- ファイル名のタイムスタンプ部分を後から書き換えない (順序が壊れる)
- マージ済 migration の `up()` / `down()` 本文も触らない (たとえ "明らかなバグ" であっても、新しい migration で打ち消すこと)
---
## どの方式を使うか決める
| 状況 | 方式 |
|---|---|
| エンティティ (`packages/backend/src/models/*.ts`) を `@Column` / `@Index` / `@Entity` 等で先に変更し、差分から自動生成したい | `typeorm migration:generate` (本ファイルの "A. 差分から自動生成") |
| 手書き SQL / データ移行 / `CREATE INDEX CONCURRENTLY` など、エンティティ差分では表現できない変更 | `typeorm migration:create` で空雛形を作る (本ファイルの "B. 空雛形を作る") |
迷ったら **まずエンティティを変更 → `migration:generate`** が原則。既存 migration (`packages/backend/migration/*.js`) のほぼすべてが `queryRunner.query(\`SQL...\`)` の raw SQL なので、CLI 出力でも手書きでもスタイルは揃う。
---
## 共通: クラス命名規則
- ファイル名: `packages/backend/migration/{unixMs}-{descriptive-name}.js` (拡張子 `.js`)
- ファイル名の `descriptive-name` 部分は既存履歴で混在 (PascalCase / camelCase / kebab-case)、変更を表す単一英語名なら良い
- **クラス名は PascalCase + 13 桁タイムスタンプ** (例: `class BirthdayIndex1767169026317`)
- **`name` プロパティもクラス名と同一文字列** にする (`name = 'BirthdayIndex1767169026317'`)
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PascalCaseName1234567890123 {
name = 'PascalCaseName1234567890123'
async up(queryRunner) {
// 前進マイグレーション
}
async down(queryRunner) {
// up を完全に巻き戻す
}
}
```
---
## A. エンティティ差分から自動生成
```bash
# リポジトリルートから実行してよい。--filter backend exec が cwd を packages/backend に移すので、
# 出力パス migration/<PascalName> と -d ormconfig.js は packages/backend/ 基準で解決される
pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>
```
**CONTRIBUTING.md との違い**: CONTRIBUTING.md は `pnpm dlx typeorm ...` を案内しているが、`dlx` はパッケージを一時ダウンロードするため、バージョンが backend の依存関係と揃わない可能性がある。`pnpm --filter backend exec typeorm` はワークスペースにインストール済みの typeorm を使うため **こちらを推奨**
**`-o --esm` について**: `-o` (`--outputJs`) は「TS ではなく JS を出力する」オプション、`--esm` は「ESM 形式 (`export class ...`) で出力する」オプション。Misskey の既存 migration はすべて ESM JS であるため **両方が必須**`--esm` を省略すると CommonJS 形式の JS が生成されスタイルが揃わない。
### 事前準備 (一括スクリプト)
`migration:generate` には backend ビルド + ローカル DB が必要。一括で揃えるスクリプトを同梱している (node 製。pure Windows でも動く)。リポジトリルートから:
```bash
node .claude/skills/working-on-backend/scripts/prepare-generate.mjs
```
スクリプトがやること:
- `pnpm build-pre``built/meta.json` を生成 (`loadConfig()` が要求)
- `pnpm --filter backend compile-config``built/.config.json` を生成 (`ormconfig.js``loadConfig()` が要求するのはこれ。ソースの `.config/default.yml` はその入力なので、無ければ `.config/example.yml` から作っておく)
- `pnpm --filter backend build` → エンティティを `built/` に反映 (CLI は `built/` を読む)
- `docker compose -f compose.local-db.yml up -d --wait db` → ローカル DB (postgres) を起動。`--wait` は Docker Compose v2.1.1 (2021-11) 以降が必要 (v2 の `docker compose` 前提。EOL の `docker-compose` v1 は対象外)
`migration:create` (空雛形) しか使わないなら DB もビルドも不要なので、このスクリプトは不要。
---
## B. 空雛形を作る (手書き SQL / データ移行用)
```bash
pnpm --filter backend exec typeorm migration:create -o --esm migration/<PascalName>
```
ローカル DB の起動とビルドは不要。空の `up` / `down` だけが生成される。
**注意:** `-o --esm`**必ず付ける**。これが無いと `<UnixMs>-<PascalName>.ts` (CommonJS / TS 出力) が生成されるが、Misskey の `ormconfig.js``migration/*.js` だけを読み、既存の他 migration も全て `export class ... { async up(queryRunner) {...} }` の ESM JS 形式なので、後で手作業で変換が必要になる。`-o --esm` を付ければそのまま `.js` ESM で出る。
ただし `migration:create` の雛形は **`name = '...'` プロパティを出力しない**ので、後段の SPDX 付与に加えて `name = '<PascalName><ms>'` を手で足し、`up`/`down` を埋める必要がある。雛形冒頭の `@typedef` / `@implements MigrationInterface` JSDoc は既存ファイルに無いので消して house style に揃える。
### B の補助: 引数だけで全部を済ませたい場合
引数で `<PascalCaseName>` を渡すだけで「空雛形生成 + SPDX 付与 + check-migrations 実行」までやる薄いラッパー (旧 `.claude/commands/migrate-new.md` 由来) は廃止された。同等の流れを手で踏みたい場合、上記の `typeorm migration:create` + SPDX 付与 + `name` プロパティ追加 + `check-migrations` の順で実行する。
---
## SPDX ヘッダー付与
CLI 出力には SPDX ヘッダーが含まれない。**必ず冒頭に追加する** (CI の `spdx` ジョブが失敗するため)。
```js
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
---
## up / down の整合確認
- `up()` の各ステートメントに対し、`down()` で完全に巻き戻せること
- 列追加 (`ADD COLUMN`) ↔ 列削除 (`DROP COLUMN`)、テーブル作成 ↔ テーブル削除、FK 追加 ↔ FK 削除、インデックス作成 ↔ インデックス削除 を必ずペアで書く
- `down()` を空のまま残さない。本番ロールバック時に詰む
**単純な逆 SQL では戻らない難ケース** (enum 値の追加・変更 / NOT NULL 列追加 / データ移行 UPDATE / JSONB・配列デフォルト / 列リネーム / 安全な DROP・COMMENT) は [knowledge/typeorm-patterns.md §migration 難ケース](../knowledge/typeorm-patterns.md) を必ず参照。特に **enum 変更****列リネーム**`migration:generate` の出力をそのまま使うと巻き戻せない / データが消えるので要注意。
### インデックス追加時 (CREATE INDEX CONCURRENTLY)
大規模テーブルへの `CREATE INDEX` は本番で長時間ロックする恐れがある。`CONCURRENTLY` で発行するときは migration class に `transaction = false` 等の対応が必要。詳細は [knowledge/typeorm-patterns.md §CONCURRENTLY](../knowledge/typeorm-patterns.md) を参照。
参照実装: [packages/backend/migration/1745378064470-composite-note-index.js](../../../../../packages/backend/migration/1745378064470-composite-note-index.js)。
---
## 検証
ルートから実行:
```bash
# 未反映の差分が無いか (新規 migration が生成すべき DDL を取り逃していないか)
pnpm --filter backend check-migrations
# ローカル DB に適用
pnpm migrate
# ロールバック (down が壊れていないか)
pnpm revert
# 再適用 (順方向にもう一度通す)
pnpm migrate
```
`check-migrations` の実体は [scripts/check_migrations_clean.js](../../../../../packages/backend/scripts/check_migrations_clean.js)。TypeORM の `dataSource.driver.createSchemaBuilder().log()` で pending DDL を取得し、`upQueries` / `downQueries` のいずれかが残っていれば非ゼロ終了する。**順序検査ではなく**「エンティティと migration が同期しているか」の検査。
---
## 既存ファイル参照テンプレ
新規ファイルを書くときは、変更パターンが近い既存ファイルを **必ずひとつ開いて並べて書く**。スタイルが激しくズレた PR は差し戻されやすい。
| パターン | 参照ファイル |
|---|---|
| インデックス追加 + 関数定義 | [migration/1767169026317-birthday-index.js](../../../../../packages/backend/migration/1767169026317-birthday-index.js) |
| 列追加のみ | [migration/1766652173085-add-category-to-avatar-decorations.js](../../../../../packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js) |
| テーブル新規作成 + FK | [migration/1761569941833-add-channel-muting.js](../../../../../packages/backend/migration/1761569941833-add-channel-muting.js) |
---
## CHANGELOG (ユーザー影響がある場合)
スキーマ変更がユーザーに見える挙動を生む場合のみ、`CHANGELOG.md` に追記する。内部リファクタや純粋なインデックス追加は不要。詳細は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で確認。
---
## 提出前セルフレビューチェックリスト
完了前に以下を上から確認する (各項目を TodoWrite 化してよい):
- [ ] **新規タイムスタンプ**で作成し、既にマージ済みの migration ファイルは一切編集していない (大前提)
- [ ] ファイル冒頭に **SPDX ヘッダー**がある
- [ ] `export class <PascalName><ms>``name = '<PascalName><ms>'`**文字列が完全一致** している (PascalCase + 13 桁タイムスタンプ)
- [ ] `up()` の各文に対応する巻き戻しが `down()` にあり、**`down()` が空でない** (難ケースは [knowledge/typeorm-patterns.md](../knowledge/typeorm-patterns.md) を確認済み)
- [ ] `pnpm --filter backend check-migrations`**0 件 (pending DDL なし)** で通る
- [ ] (可能なら) `pnpm migrate``pnpm revert``pnpm migrate` が通る
- [ ] ユーザーに見える変更なら CHANGELOG 追記 → [shipping-misskey-change](../../../shipping-misskey-change/SKILL.md)

View file

@ -0,0 +1,66 @@
/*
* typeorm migration:generate の前準備をまとめて実行する (冪等クロスプラットフォーム)
* リポジトリルートから実行: node .claude/skills/working-on-backend/scripts/prepare-generate.mjs
*
* generate はエンティティのビルド出力 (built/)コンパイル済み設定 (built/.config.json)
* 稼働中の DB を必要とする手で 5 段並べると取りこぼすのでここに集約する
* migration:create (空雛形) しか使わないなら DB もビルドも不要なのでこのスクリプトは不要
*
* Node で書いているのは pure Windows (bash の無い環境) でも動かすためnode はこのリポジトリの
* ランタイムなので必ず存在しbuild-pre.mjs / compile_config.js と同じ流儀に揃う
*/
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
// このファイルの 4 つ上が repo root
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
process.chdir(root);
function step(msg) { console.log(`\n==> ${msg}`); }
function run(cmd) { console.log(`$ ${cmd}`); execSync(cmd, { stdio: 'inherit' }); }
function fail(msg) { console.error(`ERROR: ${msg}`); process.exit(1); }
step('1/5 設定ファイルの確認');
if (!existsSync('.config/default.yml')) {
fail([
'.config/default.yml が存在しません。',
' .config/example.yml を .config/default.yml にコピーしてから再実行してください:',
' Unix系: cp .config/example.yml .config/default.yml',
' PowerShell: Copy-Item .config/example.yml .config/default.yml',
' コピー後、db.user / pass / db を .config/docker.env と一致させてください',
' (example.yml の既定値は docker.env の例と一致するので、独自 DB を使わなければそのままで可)。',
].join('\n'));
}
// compose.local-db.yml の db サービスは .config/docker.env を env_file に要求する
if (!existsSync('.config/docker.env')) {
fail([
'.config/docker.env が存在しません (compose.local-db.yml の db が要求)。',
' 例 (.config/default.yml の db.user / db.pass / db.db と一致させる):',
' POSTGRES_USER=example-misskey-user',
' POSTGRES_PASSWORD=example-misskey-pass',
' POSTGRES_DB=misskey',
].join('\n'));
}
console.log('OK: .config/default.yml と .config/docker.env あり');
step('2/5 built/meta.json の生成 (build-pre)');
run('pnpm build-pre');
step('3/5 設定のコンパイル (compile-config -> built/.config.json)');
run('pnpm --filter backend compile-config');
step('4/5 backend のビルド (エンティティを built/ へ反映)');
run('pnpm --filter backend build');
step('5/5 ローカル DB の起動 (postgres のみ・healthcheck 完了まで待機)');
// migration:generate が必要とするのは postgres だけ。db サービスに絞れば meilisearch.env 等が無くても動く。
// --wait は compose の pg_isready healthcheck 完了まで待つ。直後の migration:generate が
// DB 未起動で失敗しないために必須。--wait は Docker Compose v2.1.1 (2021-11) で導入されており、
// このリポジトリが前提とする v2 の `docker compose` なら標準で使える (EOL の `docker-compose` v1 は対象外)。
run('docker compose -f compose.local-db.yml up -d --wait db');
console.log('\n準備完了。次を実行できます:');
console.log(' pnpm --filter backend exec typeorm migration:generate -d ormconfig.js -o --esm migration/<PascalName>');

View file

@ -0,0 +1,36 @@
---
name: working-on-frontend
description: Use whenever editing or adding code under `packages/frontend/`, or editing `locales/ja-JP.yml` for frontend-facing UI text — including Vue 3 SFCs (`Mk*` components), i18n keys (`i18n.ts.<key>` / `i18n.tsx.<key>()`), SCSS Modules, theme/CSS variables, `os.*` UI helpers, and Storybook stories. Covers SPDX (HTML comment form), `<script setup lang="ts">` conventions, type-only defineProps, `ja-JP.yml`-only locale editing (other locale yml files are Crowdin-managed and must not be edited), and accessibility. Must be consulted before any frontend or UI-locale change to avoid CI failures, lost translations, and reviewer pushback. This is NOT waived by having already invoked brainstorming, writing-plans, or any other upstream skill — invoke this at implementation time regardless of what preceded it.
---
# working-on-frontend
`packages/frontend/` (Misskey Web クライアント) を編集するとき、最初に参照するスキル。Vue 3 SFC / SCSS Modules / i18n / `os.*` / Storybook / アクセシビリティの **手順****背景知識** をまとめている。
SKILL.md 本体は references への索引だけ。具体的な手順や規約は該当ファイルを Read すること (progressive disclosure)。
**他スキル実行後も免除されない。** `brainstorming` / `writing-plans` / その他アップストリームスキルを先に呼んでいても、`packages/frontend/` に触れる実装フェーズに入る時点でこのスキルを呼ぶこと。
## 作業別ワークフロー (tasks)
タスク単位の完結したチェックリスト。新しい何かを足すときに開く。
- 新規 / 既存 `Mk*` Vue コンポーネントを追加・改修する → [references/tasks/adding-mk-component.md](references/tasks/adding-mk-component.md)
- i18n キーを追加・改修する (`locales/ja-JP.yml` 編集) → [references/tasks/adding-i18n-key.md](references/tasks/adding-i18n-key.md)
## 共通知識 (knowledge)
タスクに紐付かない参照リファレンス。SFC を **編集する** 場面 (新規追加でなくても) で踏みうる規約。
- `<script setup>` / type-only `defineProps` / `defineEmits` / generic SFC / v-model 連動など SFC 規約 → [references/knowledge/component-conventions.md](references/knowledge/component-conventions.md)
- `i18n.ts.<key>` / `i18n.tsx.<key>(...)` の使い分け / HTML タグ埋め込み / 動的キー切替 / 既存キーのリネーム手順 → [references/knowledge/i18n-usage.md](references/knowledge/i18n-usage.md)
- SCSS Modules / `--MI_THEME-*` `--MI-*` CSS 変数 / グローバル utility class (`_button` 等) → [references/knowledge/scss-modules.md](references/knowledge/scss-modules.md)
- `os.alert` / `os.confirm` / `os.popup` 等 UI ヘルパー (ブラウザ標準 `alert()` 直呼びは禁止) → [references/knowledge/os-api.md](references/knowledge/os-api.md)
- `*.stories.impl.ts` 併設規則 + 複数 story / argTypes / layout / action パターン → [references/knowledge/storybook.md](references/knowledge/storybook.md)
- frontend Vitest / Cypress E2E の書き方と前提 → [references/knowledge/frontend-testing.md](references/knowledge/frontend-testing.md)
## 必ず最後に通る場所
frontend の変更を commit / PR にする前に、必ず [shipping-misskey-change](../shipping-misskey-change/SKILL.md) の最終チェックリストに従う。`pnpm lint` / SPDX / `ja-JP.yml` のみ編集確認 / CHANGELOG をまとめて確認する。
`.vue` を追加・変更したなら、その出口で [vue-component-reviewer](../../agents/vue-component-reviewer.md) agent (この skill の規約を review-mode から機械チェックする専門 reviewer) を Task で起動すると、SPDX 形式・命名・i18n・SCSS 変数・a11y・Storybook 併設の逸脱を取りこぼしにくい。

View file

@ -0,0 +1,357 @@
# Vue SFC 規約・テンプレート集 + a11y チェックリスト
Misskey の Vue 3 SFC 規約と、新規 `Mk*` コンポーネント / 既存コンポーネント編集時のテンプレート / アクセシビリティ要件をまとめたページ。
## 目次
- [SFC スタイルの基本](#sfc-スタイルの基本)
- [`<script>` / `<style>` 規約](#script--style-規約)
- [テンプレート集](#テンプレート集)
- [simple (`<slot>` + 単純 props)](#simple-slot--単純-props)
- [generic + 2 ブロック script](#generic--2-ブロック-script)
- [`defineModel` で v-model 連動](#definemodel-で-v-model-連動)
- [emit + 名前付き slot で外部から動作を差し込む](#emit--名前付き-slot-で外部から動作を差し込む)
- [a11y チェックリスト](#a11y-チェックリスト)
## SFC スタイルの基本
Composition API + `<script setup lang="ts">` を基本とする (Options API は新規導入しない)。型宣言や module スコープのユーティリティを置きたい時は、setup ブロックと **併用** する形で追加の `<script lang="ts">` ブロックを置いて構わない (例: [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) は `SupportedTypes` 型を別ブロックで宣言してから setup を書いている)。SCSS は **CSS Modules** で書き、`<style lang="scss" module>` を使う。
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<!-- ... -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// ...
</script>
<style lang="scss" module>
.root {
/* ... */
}
</style>
```
## `<script>` / `<style>` 規約
| 項目 | 規約 | 新規不可 |
|---|---|---|
| `<script>` 開始タグ | `<script lang="ts" setup>` または `<script setup lang="ts">` (順序不問) | `<script>` (lang 無し) / Options API (`export default { data() {...} }`) |
| Props 定義 | `defineProps<{ ... }>()` (type-only) | runtime object 形式 `defineProps({ name: { type: String } })` |
| Emits 定義 | `defineEmits<{ (ev: 'click'): void }>()` (type-only) | runtime array 形式 `defineEmits(['click'])` |
| 型ジェネリック | `<script setup lang="ts" generic="T extends ...">` 属性で渡す。複雑な型宣言が必要なら **2 ブロック構成** ([generic パターン](#generic--2-ブロック-script)) | — |
| `<style>` 開始タグ | `<style lang="scss" module>`、参照は `:class="$style.foo"` | `<style scoped>` (module なし) は新規不可 (legacy 混在) |
| CSS 値 | `var(--MI_THEME-...)` (テーマ) / `var(--MI-...)` (UI 共通定数) を使う | `#fff` / `rgb(...)` / `rgba(...)` のハードコード ([scss-modules.md](scss-modules.md)) |
| グローバル class | `_button` / `_panel` / `_selectable` / `_buttonPrimary` 等の global utility class を活用 | — |
| アイコン | Tabler icons クラス `<i class="ti ti-info-circle">` | インライン SVG / 別アイコンセット |
## テンプレート集
### simple (`<slot>` + 単純 props)
下記は `<slot>` + props + `withDefaults` の典型パターンを示す**合成例** (特定ファイルの写しではない)。実在する単純コンポーネントの例は [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) 等を参照。
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, { [$style.warn]: variant === 'warn' }]" class="_selectable">
<i v-if="variant === 'warn'" class="ti ti-alert-triangle" :class="$style.icon"></i>
<i v-else class="ti ti-info-circle" :class="$style.icon"></i>
<div><slot></slot></div>
</div>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
variant?: 'info' | 'warn';
}>(), {
variant: 'info',
});
</script>
<style lang="scss" module>
.root {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 14px;
font-size: 90%;
background: var(--MI_THEME-infoBg);
color: var(--MI_THEME-infoFg);
border-radius: var(--MI-radius);
&.warn {
background: var(--MI_THEME-infoWarnBg);
color: var(--MI_THEME-infoWarnFg);
}
}
.icon {
margin-right: 4px;
}
</style>
```
ポイント:
- デフォルト値が必要なら `withDefaults(defineProps<{...}>(), { ... })` を使う (type-only のまま既定値を渡せる)
- `_selectable` は本文選択を許可する global utility class ([scss-modules.md](scss-modules.md) 参照)
- `<i class="ti ti-...">` は Tabler icons。`v-if` 切り替えで variant 別アイコンを出すのは多用パターン
### generic + 2 ブロック script
参考: [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue)
型ジェネリックを取りつつ、その型計算や `type` エイリアス宣言を setup ブロックの中に書きたくない場合は、**型宣言用 `<script lang="ts">` と setup 用 `<script lang="ts" setup>` を 2 つ並べる** 構成にできる。
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<button
v-for="item in items"
:key="String(item.value)"
class="_button"
:class="[$style.item, { [$style.active]: item.value === modelValue }]"
@click="select(item.value)"
>
{{ item.label }}
</button>
</div>
</template>
<script lang="ts">
// module scope: 型 / 定数 / 純関数のみ。setup の中から見える。
export type ChoiceItem<T> = {
value: T;
label: string;
};
</script>
<script lang="ts" setup generic="T extends string | number">
const props = defineProps<{
modelValue: T;
items: ChoiceItem<T>[];
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: T): void;
}>();
function select(value: T) {
emit('update:modelValue', value);
}
</script>
```
ポイント:
- `generic="T extends string | number"` の制約を付けることで、`v-model` で渡された型が `string` / `number` 系に限定される
- 2 ブロック構成にする理由は **setup ブロック内では `export type` が書けない** から
- `MkSelect.vue` のような複雑な型エクスポートをするコンポーネントで多用される
### `defineModel` で v-model 連動
参考: [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue), [MkRadios.vue](../../../../../packages/frontend/src/components/MkRadios.vue)
`defineModel` を使うと `props.modelValue` + `emit('update:modelValue', v)` の 2 行が 1 行に圧縮できる。
```vue
<template>
<label :class="[$style.root, { [$style.disabled]: disabled }]">
<input
v-model="checked"
type="checkbox"
:class="$style.input"
:disabled="disabled"
>
<span :class="$style.label"><slot></slot></span>
</label>
</template>
<script lang="ts" setup>
const checked = defineModel<boolean>({ required: true });
const props = defineProps<{
disabled?: boolean;
}>();
</script>
```
ポイント:
- `defineModel<boolean>()`**自動で `props.modelValue` と `emit('update:modelValue', v)` を生成** する。返り値は `Ref` なので `checked.value = ...` で書き換えると emit される
- `defineModel('foo')` のように引数を渡すと `v-model:foo` (`props.foo` + `emit('update:foo', v)`) の連動が作れる
- 新規ファイルの v-model 連動は原則として `defineModel` を使う (`props.modelValue` + `emit` の手書きは既存コードに残るのみ)
### emit + 名前付き slot で外部から動作を差し込む
下記は emit + 名前付き slot の典型パターンを示す**合成例** (特定ファイルの写しではない)。クリック時の処理を呼び出し元に委ねるパターン (確認 UI など)。なお [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) 自体は `(ev: 'click', payload: PointerEvent)` のみを emit する単機能ボタンで、この合成例とは構造が異なる。
```vue
<template>
<div :class="$style.root" class="_panel">
<div :class="$style.header">
<slot name="header">{{ i18n.ts.confirm }}</slot>
</div>
<div :class="$style.body">
<slot></slot>
</div>
<div :class="$style.footer">
<button class="_button" :class="$style.cancel" @click="emit('cancel')">
{{ i18n.ts.cancel }}
</button>
<button class="_button _buttonPrimary" :class="$style.ok" @click="emit('ok')">
{{ i18n.ts.ok }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
const emit = defineEmits<{
(ev: 'ok'): void;
(ev: 'cancel'): void;
}>();
</script>
```
ポイント:
- 名前付き slot (`<slot name="header">`) と無名 slot (`<slot></slot>`) は両方使ってよい
- `_panel` / `_button` / `_buttonPrimary` は global utility class なので、自前で同じスタイルを書かない
- `emit('ok')` 等の単純 emit は中継するだけにし、`os.confirm` などの実際の確認 UI 起動は呼び出し元の責務にする (テスト・差し替えしやすくするため)
## a11y チェックリスト
Misskey の PR レビューで頻繁に出る a11y 指摘をまとめた。新規 / 既存コンポーネントを編集する時は以下を満たす。
### クリック可能要素
#### 第一選択: `<button class="_button">`
```vue
<button class="_button" :class="$style.action" :disabled="disabled" @click="onClick">
{{ i18n.ts.save }}
</button>
```
- `_button` global class はボタンの装飾を除去するリセット (背景/枠線なし + `cursor: pointer` + disabled cursor)。focus ring や ripple は**付かない** — ripple 付きのボタンが要るなら `MkButton.vue` コンポーネントを使う
- `<button>` はデフォルトで `tabindex` / Enter / Space / `aria-disabled` の挙動とブラウザ標準のフォーカスリングを持つので、追加の ARIA を書かなくてよい
- form の中で意図せず submit させたくない場合は `type="button"` を明示する (省略時は `type="submit"` 扱い)
#### やむを得ず `<div @click>` を使う場合
装飾やレイアウト都合で `<button>` が使えないときは、**4 点セット** を必ず揃える。
```vue
<div
role="button"
tabindex="0"
:aria-disabled="disabled"
:class="$style.fakeButton"
@click="onClick"
@keydown.enter="onClick"
@keydown.space.prevent="onClick"
>
<slot></slot>
</div>
```
| 属性 / ハンドラ | なぜ必要か |
|---|---|
| `role="button"` | スクリーンリーダーにボタンとして読ませる |
| `tabindex="0"` | キーボードでフォーカス可能にする |
| `@keydown.enter` | Enter で発火 (本物の `<button>` の挙動を再現) |
| `@keydown.space.prevent` | Space で発火 + ページスクロール防止 |
| `:aria-disabled` | disabled スタイルだけでなく状態も伝える |
`@keydown.enter` を忘れて click だけ付けるのが最頻出ミス。
#### `<a>` をボタン代わりに使うのは原則禁止
URL に飛ばない `<a href="#" @click.prevent>` は a11y / SEO 両面で良くない。リンクなら `<MkA>` ([MkA.vue](../../../../../packages/frontend/src/components/global/MkA.vue))、アクションなら `<button>` を使う。
### フォーム要素
#### `<label>` 接続
```vue
<!-- ✅ for / id で結ぶ -->
<label :for="id">{{ i18n.ts.username }}</label>
<input :id="id" v-model="username" type="text">
<!-- ✅ ラップする (id 不要) -->
<label>
{{ i18n.ts.username }}
<input v-model="username" type="text">
</label>
```
label を slot で受け取る共通コンポーネント ([MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue), [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue)) を使うとこの規約は自然に守れる。
#### `aria-label` で代替
slot や label を見せたくない (アイコンのみのボタンなど) 場合は `aria-label`:
```vue
<button class="_button" :aria-label="i18n.ts.close" @click="emit('close')">
<i class="ti ti-x"></i>
</button>
```
`aria-label` の値も i18n 経由にする (英語直書きは禁止)。
**実情:** 現状コードベースでは `aria-label` の使用例自体が乏しい (アイコンの hover ヒントには `:title="i18n.ts..."` が使われるが、`title` は tooltip でありスクリーンリーダー向けラベルの代替にはならない)。このため aria-label は確立した慣習というより a11y 上の推奨ベストプラクティスとして書いている。新規でアイコンのみのボタンを足すなら付けるのが望ましい。
### `:disabled``aria-disabled` の整合
- 本物の `<button :disabled>` ならブラウザが click を抑止するが、`<div role="button">` は止めてくれない。`aria-disabled` を付けるだけでなく、**ハンドラ側でも早期 return** する:
```ts
function onClick() {
if (props.disabled) return; // ← これが無いと disabled でも発火する
// ...
}
```
### キーボード操作
- Tab で全ての操作可能要素にたどり着けること (`tabindex="-1"` を不用意に付けない)
- モーダル / popup を開いたら focus trap を考える ([MkModal.vue](../../../../../packages/frontend/src/components/MkModal.vue) のような既存コンポーネントは内部で対応している)
- リスト中の項目は矢印キー操作も考慮する。Space / Enter で開く・確定する UI は `MkSelect.vue``@keydown.space.enter`(メニューを開く) パターンを参考にする
### 既存実装の参考
| パターン | 既存コンポーネント |
|---|---|
| 標準的なボタン | [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) |
| カスタム UI でも a11y を満たす | [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue) |
| input + label slot | [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) |
| キーボード操作対応の選択 UI | [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue) |
### ありがちな PR レビュー指摘
- `<div @click>` に role / tabindex / keydown が無い
- アイコンだけのボタンに `aria-label` が無い (Tabler icon 自体には意味情報が無い)
- `disabled` スタイルだけ付けて `aria-disabled` / ハンドラ抑止が無い
- フォーカスリング (`:focus-visible` / `outline`) を `outline: none` で消したまま放置

View file

@ -0,0 +1,60 @@
# Frontend テスト (Vitest / Cypress)
Misskey frontend のテスト構成。
## Vitest (unit)
```bash
pnpm --filter frontend test # 1 回実行
pnpm --filter frontend test-and-coverage # カバレッジ付き
```
### 配置
- 主な配置: `packages/frontend/test/*.test.ts` (例: `i18n.test.ts`, `theme.test.ts`, `is-birthday.test.ts`)
- ビルドツール周りなど対象コードと隣接させた方が分かりやすいテストは、コードと同じディレクトリに `*.test.ts` として置く (例: [packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts](../../../../../packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts))
- 共有コンポーネント (`MkX.vue`) のユニットテストは現状少なく、`*.spec.ts` / `__tests__/` 形式は採用していない (Storybook + Cypress でカバー)
## Cypress E2E
Cypress は **起動済みのテストサーバー** に対して走るため、unit より前提が多い。[.github/workflows/test-frontend.yml](../../../../../.github/workflows/test-frontend.yml) の `e2e` ジョブと同じ手順をローカルで踏む:
```bash
# 1. テスト用 DB / Redis を起動 (テスト用ポート。開発用の compose.local-db.yml ではない)
docker compose -f packages/backend/test/compose.yml up -d
# 2. テスト設定を配置 (未作成なら。例示なので、cpコマンドは環境にあったコマンドに適宜読み替えること)
cp .github/misskey/test.yml .config/test.yml
# 3. 全体ビルド
pnpm build
# 4. テストサーバー起動 + Cypress 実行 (いずれもルートから)
pnpm e2e # 内部で pnpm start:test を起動し http://localhost:61812 を待って Cypress run
pnpm cy:open # 対話的に開く (サーバーは別途 pnpm start:test で起動しておく)
```
- 設定: ルート [cypress.config.ts](../../../../../cypress.config.ts)
- テスト本体は [cypress/](../../../../../cypress/) 配下
新規 frontend 機能の E2E は Cypress に書くのが基本。ただし対象は主要 UI フロー (login / post / drive etc) に限定し、細かい単位テストは Vitest または Storybook で代替する慣習。
## Storybook (視覚確認 + Chromatic 視覚回帰)
詳細は → [storybook.md](storybook.md)。
```bash
pnpm --filter frontend storybook-dev # http://localhost:6006
pnpm --filter frontend build-storybook # 静的ビルド
```
各コンポーネント横に `*.stories.impl.ts` を併設する慣習 (例: `MkButton.stories.impl.ts`)。Chromatic (`pnpm --filter frontend chromatic`) で視覚回帰チェック。
## ローカル DB / Redis
frontend のテスト種別で DB / Redis の要否が違う:
- **Vitest (unit)** — DB 不要。ロジック / コンポーネント単体のテストで backend に繋がない (CI の `vitest` ジョブにも `services:` は無い)
- **Cypress (E2E)** — テストサーバー (`pnpm start:test`) 経由で backend に繋ぐため DB / Redis が必要。**テスト用ポートの [packages/backend/test/compose.yml](../../../../../packages/backend/test/compose.yml)** を使う (上記 Cypress E2E の手順を参照)
開発用の `compose.local-db.yml` (db `5432` / redis `6379`) は **テストには使わない**。テスト用の `packages/backend/test/compose.yml` (`54312` / `56312`) とはポートが異なり、混同すると接続できない。

View file

@ -0,0 +1,412 @@
# i18n 使い分け / Crowdin 安全策 / トラブルシュート
`i18n.ts` / `i18n.tsx` の使い分け、Crowdin との同期メカニズム、頻発する型エラー / 実行時警告の対処を 1 箇所にまとめたページ。
## 目次
- [基本: ts と tsx の使い分け](#基本-ts-と-tsx-の使い分け)
- [実装パターン](#実装パターン)
- [Crowdin 安全策 (既存キーのリネーム / 復旧)](#crowdin-安全策-既存キーのリネーム--復旧)
- [トラブルシュート](#トラブルシュート)
- [制約と補足](#制約と補足)
## 基本: ts と tsx の使い分け
文言は **必ず** [i18n.ts](../../../../../packages/frontend/src/i18n.ts) 経由で参照する。引数の有無で **使う変数名そのものが変わる**。間違えると、非パラメータキーを `i18n.tsx` で呼ぶ場合は型エラーになるが、パラメータキーを `i18n.ts` で参照する場合は型エラーにならず `{name}` 等が未展開のまま画面に出る (後述のトラブルシュート参照)。
- 引数なし → `i18n.ts.<key>` (プロパティアクセス)
```ts
os.toast(i18n.ts.removed);
```
- 引数あり → `i18n.tsx.<key>(...)` (関数呼び出し)
```ts
os.alert({ type: 'info', text: i18n.tsx.unfollowConfirm({ name: user.username }) });
```
YAML 側に `{name}` 形式のプレースホルダが含まれているキーは **`i18n.tsx`** からしか呼べない。誤って `i18n.ts.unfollowConfirm` と書くと値がフォーマット前の関数になってそのまま表示される。
- **既存キーの再利用が第一**。新キー追加が必要に見えても、まず `locales/ja-JP.yml` を grep して `deleteAreYouSure({ x })` のような汎用キー (`x` プレースホルダ) が転用可能でないか確認する。新キー追加は [tasks/adding-i18n-key.md](../tasks/adding-i18n-key.md)。他言語ファイルは Crowdin の自動配信先なので絶対に手で触らない
```vue
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{ name: string }>();
async function onDelete() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.driveFileDeleteConfirm({ name: props.name }), // 引数あり
});
if (canceled) return;
os.toast(i18n.ts.removed); // 引数なし
}
</script>
```
| 用途 | 書き方 |
|---|---|
| 単純文字列 | `i18n.ts.save` |
| ネスト | `i18n.ts._settings.general` |
| パラメータ付き (1 個) | `i18n.tsx.unfollowConfirm({ name })` |
| パラメータ付き (複数) | `i18n.tsx.monthAndDay({ month, day })` |
| Vue テンプレート内 | `{{ i18n.ts.save }}` / `{{ i18n.tsx.unfollowConfirm({ name }) }}` |
## 実装パターン
### HTML タグ埋め込み
ja-JP.yml の値に `<b>` / `<br>` / `<strong>` を含めて、表示側で v-html や `<Mfm>` で描画するパターンが多用されている。
```yaml
# locales/ja-JP.yml
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつです。"
# locales/ja-JP.yml (改行 + br)
driveAboutTip: "ドライブでは、過去に...<br>\nートに添付する際に再利用したり...<br>\n<b>ファイルを削除すると...</b><br>\n..."
```
参照側:
```vue
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: 'Misskey' })" />
```
注意:
- HTML を含むキー値は **必ずダブルクォート** で囲む (YAML パース失敗回避)
- `v-html` 越しの XSS リスクが無いことを必ず確認する。パラメータ側にユーザー入力をそのまま渡すと事故る。安全な静的文字列か、別途エスケープ済の値だけにする
### リアクティブ参照 + 動的キー切替
時間経過などで翻訳キー自体を切り替えたい場合の慣習。`computed` でラップし、ブラケット記法で翻訳キーを動的に選ぶ。
出典: [packages/frontend/src/components/MkPoll.vue](../../../../../packages/frontend/src/components/MkPoll.vue) の `_poll` 動的キー
```ts
const timer = computed(() => i18n.tsx._poll[
remaining.value >= 86400 ? 'remainingDays' :
remaining.value >= 3600 ? 'remainingHours' :
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
]({
s: Math.floor(remaining.value % 60),
m: Math.floor(remaining.value / 60) % 60,
h: Math.floor(remaining.value / 3600) % 24,
d: Math.floor(remaining.value / 86400),
}));
```
対応する yml (各キーで実際に使うプレースホルダは違って良い):
```yaml
_poll:
remainingDays: "終了まであと{d}日{h}時間" # {d} {h}
remainingHours: "終了まであと{h}時間{m}分" # {h} {m}
remainingMinutes: "終了まであと{m}分{s}秒" # {m} {s}
remainingSeconds: "終了まであと{s}秒" # {s}
```
ポイント:
- 各キーで使うプレースホルダは **バラバラで構わない**
- **呼び出し側で候補キー全体に必要な全パラメータの superset を 1 つの引数オブジェクトで渡す**。各キーの内部実装は受け取ったオブジェクトから自分が必要なものだけ拾う
### 識別子として無効なキー名 (ブラケット記法)
キー名が数字始まりや予約語の場合、ドット記法ではアクセスできずブラケット記法を使う。
出典: [packages/frontend/src/components/MkSignin.totp.vue](../../../../../packages/frontend/src/components/MkSignin.totp.vue)
```vue
<div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
```
新規キー追加時は **lowerCamelCase を守れば不要**
### ネスト + パラメータ複合
```vue
{{ i18n.tsx._uploader.maxFileSizeIsX({ x: maxSize + 'MB' }) }}
{{ i18n.tsx._auth.shareAccess({ name: appName }) }}
```
### `tsx` の引数に `ts` を埋め込む
別の翻訳済み文字列をパラメータとして渡せる。
出典: [packages/frontend/src/components/MkSignupDialog.rules.vue](../../../../../packages/frontend/src/components/MkSignupDialog.rules.vue)
```ts
i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules })
```
### 三項演算子で ts / tsx を切り替え
パラメータ有無で出し分け。
```vue
{{ name ? i18n.tsx._auth.shareAccess({ name }) : i18n.ts._auth.shareAccessAsk }}
```
## Crowdin 安全策 (既存キーのリネーム / 復旧)
ja-JP.yml 以外の locales/*.yml は **Crowdin の自動配信先**。手動編集や source 側の不用意な操作で他言語の翻訳資産が失われる。
### 同期メカニズム
[crowdin.yml](../../../../../crowdin.yml):
```yaml
files:
- source: /locales/ja-JP.yml
translation: /locales/%locale%.yml
update_option: update_as_unapproved
```
- `ja-JP.yml` = **source**。これだけが翻訳元
- `en-US.yml` / `fr-FR.yml` ほか `ja-JP.yml` 以外の全 locale = **translation**。Crowdin が自動 PR で更新する
- 翻訳済みキーの **source 文字列が変わると** `update_as_unapproved` 設定により翻訳が "unapproved" 状態に戻る (= レビュー再要求)
- **キー名自体が変わる** と Crowdin は別キー扱いし、旧キーの翻訳は孤立 → 同期で削除される
根拠: [locales/README.md](../../../../../locales/README.md) "DO NOT edit locale files except `ja-JP.yml`."
### 既存キーをリネームしたい時 (3 段階)
単純な「旧キー削除 → 新キー追加」を 1 PR で行うと、すべての言語の旧キー翻訳が失われる。以下のように分割する。
#### Step 1: 新キー追加 (PR A)
旧キーを残したまま、新キー (同等の意味の日本語) を ja-JP.yml に追加する。
```yaml
# 旧キー (まだ残す)
_settings:
theme: "テーマ"
# 新キー (追加)
appearance: "外観"
```
参照箇所も新キーに移行 (frontend の全 grep + 置換)。
#### Step 2: マージ → Crowdin 翻訳が来るのを待つ
Crowdin の自動 PR で他言語にも `appearance` が追加され、翻訳が入る。`update_option: update_as_unapproved` のため、初回は unapproved 状態。プロジェクト管理者が approve するまで本番には載らない (フォールバックで日本語が出る)。
通常は数日〜数週間。急ぐ場合は Crowdin プロジェクト管理者に依頼。
#### Step 3: 旧キー削除 (PR B)
新キーの翻訳が十分埋まった後、別 PR で旧キー (`theme`) を ja-JP.yml から削除。次の Crowdin 同期で他言語からも消える。
### 単純リネームをやってしまったら
```bash
# git diff で他言語 yml が変更されていないか必ず確認 (出力が空なら OK)
git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'
```
`grep -v 'ja-JP.yml'` を diff 本文に当てる書き方は、ja-JP.yml 単体の変更でも追加行 (`+`) が素通りして必ず非空になるため使わない。**ファイル名にだけ grep を当てる** こと。
- **他言語 yml が変更されていたら即 revert**:
```bash
git restore --source=develop -- locales/en-US.yml locales/<lang>.yml
```
- ja-JP.yml だけで旧キー削除 + 新キー追加してしまった場合は、PR を分割するか、上記 3 段階に組み直す。**マージ前なら間に合う**
### ja-JP.yml 以外を触ってしまったら
```bash
# 最も安全な復旧: develop 側の中身に戻す
git restore --source=develop -- locales/en-US.yml
# あるいは特定 path だけステージから外し作業ツリーごと戻す
git checkout HEAD -- locales/zh-CN.yml
```
PR 化前なら何度でもやり直せる。**マージしてしまうと Crowdin 側との整合性が崩れて手動回復が必要** になるので、PR レビュー段階で必ず `locales/*.yml` (ja-JP 以外) の diff がゼロであることを確認する。
### CHANGELOG 記載の判定
| 変更内容 | CHANGELOG 記載 |
|---|---|
| 新規画面追加と一緒に新キー追加 | 必要 (`### Client` に Feat/Enhance) |
| 既存文言の改善 (誤字脱字以外) | 必要 (`### Client` に Enhance) |
| 誤字脱字・微妙な言い回し修正 | 不要 |
| キーのリネーム (UI 変化なし) | 不要 |
| キー削除 (画面から消える) | 必要 (`### Client` に Feat / 機能削除) |
書き方は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) を参照。
## トラブルシュート
i18n 周辺で踏みやすい失敗とその対処。エラー文字列で grep してたどり着けるよう整理。
### 型エラー: `Property '<key>' does not exist on type 'Locale'`
**症状**:
```
packages/frontend/src/components/MkXxx.vue
> i18n.ts.newKey
Property 'newKey' does not exist on type 'Locale'.
```
**原因**: ja-JP.yml にキーは追加したが、`packages/i18n` の型生成 (`autogen/locale.ts`) が再生成されていない。
**対処**:
- `pnpm dev` を起動中なら、`packages/i18n` の watch (`nodemon ... tsx ./build.ts --watch`) が自動再生成するので、yml 保存後に typecheck をやり直す
- 一回だけ手動再生成したいなら: `pnpm --filter i18n generate` (実体は `tsx scripts/generateLocaleInterface.ts`)
- 検出経路: `pnpm --filter frontend lint`
実装根拠: [packages/i18n/scripts/generateLocaleInterface.ts](../../../../../packages/i18n/scripts/generateLocaleInterface.ts) (パラメータ抽出の正規表現 `/\{(\w+)\}/g`)。
### 型エラー: ts/tsx の取り違え
**症状 A** (パラメータ無しキーを tsx で呼ぶ):
```
i18n.tsx.save({...})
> Property 'save' does not exist on type 'Tsx<Locale>'.
```
**症状 B** (パラメータ付きキーを ts で参照、関数化されたまま使う):
```vue
{{ i18n.ts.unfollowConfirm }}
<!-- 画面に "{name}のフォローを解除しますか?" が {name} 未置換のまま出る -->
```
**原因**: `Tsx<T>` 型 ([packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts)) は `ParameterizedString<P>` を持つキーだけを関数として公開する。
**対処**: パラメータ有無は yml の `{...}` 記法で決まる。
| yml の値 | ts | tsx |
|---|---|---|
| `"保存"` | `i18n.ts.save` ✅ | (キー存在せず) ❌ |
| `"{name}のフォローを解除しますか?"` | `i18n.ts.unfollowConfirm``{name}` 未置換の文字列のまま ❌ | `i18n.tsx.unfollowConfirm({ name })` ✅ |
### 実行時警告: `Unexpected locale key: <key>`
**症状**: 開発モードのコンソールに出る。
**原因**: dev mode の Proxy が ja-JP.yml に存在しないキーへのアクセスを検知 ([packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts) の dev 用 Proxy)。
**対処**: ja-JP.yml に該当キーを追加するか、参照側のタイポを直す。
### 実行時警告: `Missing locale parameters: <param> at <key>`
**症状**: dev mode コンソール。
**原因**:
- yml 側 `{name}` に対し、呼び出し側で `{ user: ... }` のように **キー名が違う**
- あるいは引数オブジェクトに値が含まれていない
実装根拠: [packages/frontend-shared/js/i18n.ts](../../../../../packages/frontend-shared/js/i18n.ts) (`Object.hasOwn(arg, expressions[i])` チェック)。
**対処**: yml と呼び出し側でパラメータ名を一致させる。yml 側のキー名を変更したら、呼び出し側 (frontend 全体) を grep で揃える。
### YAML パース失敗
**症状**: `pnpm --filter i18n generate` 実行時に `YAMLException: ...`、または `pnpm dev` の watch ログにエラー。
**原因**: 値に YAML の特殊文字 (`<` `>` `:` `'` `&` `*` `|` `>` `#`) を含むのに **クォートしていない**
**対処**: 値全体を `"..."` (ダブルクォート) で囲む。
```yaml
# OK: HTML タグを含む
poweredByMisskeyDescription: "{name}は、...プラットフォーム<b>Misskey</b>のサーバーのひとつです。"
# OK: コロン・シングルクォート・角括弧を含む URL 説明
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL、S3: 'https://<bucket>.s3.amazonaws.com'、GCS等: 'https://storage.googleapis.com/<bucket>'。"
# OK: 改行をリテラルで埋め込む
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの...<br>\nートに添付する際に..."
```
YAML の block scalar (`|` / `>`) も使えるが、HTML タグ + プレースホルダ混在では **ダブルクォート + `\n` エスケープ** の方が安定する。
### キー名衝突: `_lang_` を上書きしてしまう
**症状**: 各言語ファイルの先頭にある `_lang_` (例: ja-JP は `"日本語"`) を別用途で使おうとして上書き。
**原因**: `_lang_`**言語自身の表記** に予約されている ([packages/i18n/src/autogen/locale.ts](../../../../../packages/i18n/src/autogen/locale.ts) の先頭キー)。
**対処**: 新規キーは別名にする。
### frontend で diff を当てても変わらない
**症状**: ja-JP.yml を変更したが画面に反映されない。
**原因**:
- `pnpm dev` ではなく `pnpm --filter frontend watch` だけ起動していて、`packages/i18n` の watch が走っていない
- もしくは frontend へ配信される生成物 (`built/_frontend_dist_/locales/*.json`) がブラウザ側でキャッシュされている
**対処**: ルートの `pnpm dev` を起動する (frontend + backend + i18n watch が全部立ち上がる)。それでも反映しないならブラウザのキャッシュをクリア、または `pnpm --filter i18n build` を手動実行。
## 制約と補足
### ICU MessageFormat 非対応
[packages/i18n/scripts/generateLocaleInterface.ts](../../../../../packages/i18n/scripts/generateLocaleInterface.ts) の正規表現は `/\{(\w+)\}/g`。つまり受け付けるのは **`{paramName}` 形式の単純置換のみ**。
```yaml
# NG: ICU plural — そのまま画面に文字列として出るだけ
items: "{count, plural, one {1個} other {{count}個}}"
# NG: ICU select
gender: "{gender, select, male {彼} female {彼女} other {その人}}"
```
代替戦略:
#### 1. 件数別にキーを分ける
```yaml
# OK
withNFiles: "{n}個のファイル"
withOneFile: "1個のファイル"
```
```ts
const text = files.length === 1
? i18n.ts.withOneFile
: i18n.tsx.withNFiles({ n: files.length });
```
#### 2. 切替パターン (動的キー)
時間経過のような連続的な分岐は MkPoll のパターン ([上記「リアクティブ参照」](#リアクティブ参照--動的キー切替)) を採用。
### 予約キー `_lang_`
各 yml ファイルの **トップレベル先頭** に置かれ、その言語自身の表記名を持つ。
```yaml
# locales/ja-JP.yml (トップレベル先頭)
_lang_: "日本語"
```
UI の言語切替プルダウンなどで参照される。**新規キーには使わない**。
### Storybook での挙動
Storybook 環境はバンドラが別物なので、本番の i18n パッケージをそのままは使わない。代わりに [packages/frontend/.storybook/preload-locale.ts](../../../../../packages/frontend/.storybook/preload-locale.ts) がビルド時に **ja-JP の locale だけを JSON にダンプして同居 `locale.ts` を生成** する。
つまり Storybook では:
- **ja-JP の文字列だけが見える** (他言語の検証はできない)
- ja-JP.yml にキーを追加した直後に Storybook を起動しても、`preload-locale.ts` 実行前なら反映されない。Storybook を再起動するか、`packages/i18n` を一度 build する
- stories からの呼び方は通常通り: `i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })`
### backend での i18n 直接参照は基本無し
i18n は frontend (および一部の SSR されるエラーページ) でのみ使われる。`packages/backend` 配下から `import { i18n }` するパターンは原則無く、API エラー文言は別ルート (`ApiError` の i18n 化されていないメッセージ + frontend 側で翻訳) で扱う。
### 改行の扱い
ダブルクォート値の中で `\n` は実際の改行になる。block scalar (`|`) でも可だが、HTML タグやプレースホルダ混在では扱いづらい。慣習はダブルクォート + `\n`
Vue 側で表示時に `white-space: pre-wrap` などを当てる必要あり。

View file

@ -0,0 +1,96 @@
# `os.*` UI ヘルパー
[`packages/frontend/src/os.ts`](../../../../../packages/frontend/src/os.ts) で公開されている UI 操作 API の一覧。**ブラウザ標準の `window.alert()` / `window.confirm()` / `window.prompt()` を直接呼ばない**。これらは Misskey のテーマ / アクセシビリティ / モーダルレイヤと整合しないため。
## 主要 API
| 関数 | 用途 |
|---|---|
| `os.alert({ type?, title?, text? })` | 単方向アラート (全フィールド任意) |
| `os.confirm({ type, title?, text? })` | yes/no 確認 (`type` 必須、`{ canceled }` を返す) |
| `os.toast(message)` | 一時通知 |
| `os.popup(component, props, handlers)` | 任意コンポーネントの非同期ポップアップ |
| `os.popupMenu(items, anchor?)` | コンテキストメニュー |
| `os.contextMenu(items, ev)` | 右クリックメニュー |
| `os.form(title, fields)` | フォームダイアログ |
| `os.apiWithDialog(endpoint, data)` | API 呼出し + エラー時ダイアログ表示 |
| `os.success()` / `os.waiting()` | 成功 / ローディング表示 |
## 使用例
### `os.alert` (単方向通知)
```ts
await os.alert({
type: 'info',
text: i18n.ts.savedSuccessfully,
});
```
`type``'info'` / `'warning'` / `'error'` / `'question'` / `'success'` / `'waiting'`
### `os.confirm` (yes/no 確認)
```ts
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._notes.deleteConfirm,
});
if (canceled) return;
// 削除処理
```
`canceled === true` のとき何もしない、というパターンが頻出。
### `os.toast` (一時通知)
```ts
os.toast(i18n.ts.deleted);
```
成功通知などの軽い fire-and-forget なフィードバック。
### `os.popup` (任意コンポーネント)
```ts
const { dispose } = os.popup(MkUserSelectDialog, {
includeSelf: false,
}, {
ok: (user) => {
// ...
dispose();
},
cancel: () => {
dispose();
},
});
```
カスタムダイアログを開く場合は、コンポーネント (props / emits) を `os.popup` で起動する。`dispose()` で閉じる。
### `os.apiWithDialog` (API + 自動エラーダイアログ)
```ts
const result = await os.apiWithDialog('notes/create', {
text: 'hello',
});
// 成功時: result は API レスポンス
// 失敗時: 自動でエラーダイアログを表示。ただし promise 自体は reject されるので、await するなら try/catch が必要
```
通常の `misskeyApi(...)` だと自前でエラーダイアログ表示が必要だが、`apiWithDialog` は失敗時に自動で `os.alert({ type: 'error', ... })` を表示してくれる。ただし返す promise は元の `misskeyApi(...)` と同一で **reject される** ([os.ts](../../../../../packages/frontend/src/os.ts) で `return promise`)。`await` する場合は依然 try/catch が要る (ダイアログ表示後に後続処理を止めたいだけなら catch して握りつぶす)。
## なぜブラウザ標準 UI を使わないか
- `window.alert()` は Misskey のテーマ (ダークモード / カスタムテーマ) に追従しない
- `window.confirm()` はキーボード操作・focus trap・i18n のいずれも Misskey の規約と整合しない
- `window.prompt()` の入力 UI も同じ
- ブラウザ依存の表示揺れ (Firefox / Safari / Chrome で見た目が違う)
- vue-component-reviewer から指摘される
代わりに `os.alert` / `os.confirm` / `os.form` / `os.popup` を使う。
## 参照ファイル
- [packages/frontend/src/os.ts](../../../../../packages/frontend/src/os.ts) — 全 API の実装
- 既存のダイアログ系コンポーネント: `MkDialog.vue` (alert / confirm はこれを再利用)、`MkFormDialog.vue`

View file

@ -0,0 +1,135 @@
# SCSS Modules / CSS 変数 / utility class
Misskey の SCSS 規約。`<style lang="scss" module>` の書き方、`--MI_THEME-*` / `--MI-*` CSS 変数の使い分け、グローバル utility class の一覧をまとめる。
## CSS 変数の使い分け
Misskey のテーマシステムは 2 系統の CSS 変数で構成される。新規のスタイルは **必ず変数経由** にする。直接の `#fff` / `rgb()` / `rgba()` ハードコードは vue-component-reviewer から Major 指摘される。
### `--MI_THEME-*` (テーマ依存)
ユーザーが選んだテーマ (light / dark / 個別テーマ) で変わる色。`packages/frontend-shared/themes/_dark.json5` などで定義。
| 変数 | 用途 |
|---|---|
| `--MI_THEME-bg` | ページ背景 |
| `--MI_THEME-panel` | カード / パネル背景 |
| `--MI_THEME-panelHighlight` | 強調表示パネル |
| `--MI_THEME-fg` | 本文文字色 |
| `--MI_THEME-fgHighlighted` | 強調文字色 |
| `--MI_THEME-fgOnPanel` | パネル上の文字 |
| `--MI_THEME-fgOnAccent` | accent 色背景上の文字 (≒白系) |
| `--MI_THEME-accent` | プライマリアクセント (リンク、active state) |
| `--MI_THEME-accentedBg` | accent 系の薄背景 |
| `--MI_THEME-divider` | 罫線 |
| `--MI_THEME-error` | エラー色 |
| `--MI_THEME-warn` / `--MI_THEME-infoWarnBg` / `--MI_THEME-infoWarnFg` | 警告系 |
| `--MI_THEME-infoBg` / `--MI_THEME-infoFg` | 情報系 |
| `--MI_THEME-buttonBg` / `--MI_THEME-buttonHoverBg` | ボタン背景 |
| `--MI_THEME-inputBorder` / `--MI_THEME-inputBorderHover` | フォーム枠 |
| `--MI_THEME-focus` | フォーカスリング色 |
| `--MI_THEME-link` | リンク色 |
| `--MI_THEME-mention` / `--MI_THEME-hashtag` | メンション / ハッシュタグ |
全部の一覧が必要なら `packages/frontend-shared/themes/_light.json5` を読むのが早い (JSON5 で全キーが揃っている)。
### `--MI-*` (UI 共通定数、テーマ非依存)
| 変数 | 用途 |
|---|---|
| `--MI-radius` | 標準角丸 (`12px`) |
| `--MI-margin` | 標準余白 (大、`16px` / モバイルでは `10px`) |
| `--MI-marginHalf` | 標準余白の半分 |
| `--MI-modalBgFilter` | モーダル背景 (backdrop) のフィルタ |
`var(--MI-radius)` を使うとアプリ全体で角丸の大きさが揃う。`border-radius: 12px;` のように直書きすると、後から角丸を変える要件が来たときに全件直すことになる。
### ハードコードの例外
色は基本ハードコード禁止だが、以下のケースは正当化される:
- `transparent` / `currentColor` / `none` などの CSS キーワード
- alpha だけ動的に変えたい → `color-mix(in srgb, var(--MI_THEME-fg) 50%, transparent)` のように合成する
- アイコンサイズ等、CSS 変数化されていない数値定数 (`font-size: 14px;` 等は OK)
## グローバル utility class
`packages/frontend/src/style.scss` に定義されたグローバル class。`<style module>` 内のクラスと **併用** する (`:class="[$style.root, '_button']"` ではなく、HTML の `class="_button"` 属性で直接書く)。
下表は **よく使う代表例** で網羅ではない (class は随時増減するため、この一覧は腐りやすい)。手元の class が実在するか / 実装を確認したいときは正本の [packages/frontend/src/style.scss](../../../../../packages/frontend/src/style.scss) を直接見る (`grep -nE '^\._' packages/frontend/src/style.scss` で定義済み class を列挙できる)。
| class | 意味 |
|---|---|
| `_button` | クリック可能な無装飾ベース (`appearance:none` + `cursor:pointer` + disabled cursor のリセットのみ。focus ring や ripple は**含まない** — ripple が要るなら `MkButton.vue` を使う)。`<button>` または `<a>` に付ける |
| `_buttonPrimary` | `_button` + accent 色背景 (確定アクション) |
| `_buttonGradate` | `_button` + グラデーション背景 |
| `_panel` | カード / パネル枠 (背景 + 角丸 + `overflow:clip`。shadow は含まない) |
| `_selectable` | テキスト選択許可 (Misskey はデフォルトで本文以外の選択を抑止しているため) |
| `_selectableAtomic` | 子要素まとめて 1 単位で選択 |
| `_noSelect` | テキスト選択禁止 |
| `_nowrap` | `white-space: nowrap;` |
| `_help` | accent 色 + `cursor: help` (ヘルプアイコン用) |
| `_textButton` | accent 色のテキストボタン (hover で下線) |
| `_link` | テキストリンク強調 |
| `_gaps` | 縦並び flex (`display: flex; flex-direction: column; gap: var(--MI-margin);`) |
| `_gaps_m` / `_gaps_s` | 同じく縦並び flex で gap 固定 (`21px` / `10px`) |
| `_margin` | 標準 margin (= `--MI-margin`) |
| `_shadow` | 標準シャドウ (`box-shadow`) |
| `_popup` | popup / dropdown 用 (背景 + 角丸 + `contain`。shadow は含まない) |
| `_acrylic` | 半透明 + backdrop blur (アクリル風) |
使い方:
```vue
<template>
<button class="_button _buttonPrimary" :class="$style.action" @click="onClick">
{{ i18n.ts.save }}
</button>
</template>
<style lang="scss" module>
.action {
padding: 8px 24px;
/* 背景色や focus ring は _buttonPrimary が持つので書かない */
}
</style>
```
## `<style lang="scss" module>` の特殊記法
### `:global(...)` で module スコープから出る
`<style lang="scss" module>` 内に書いたクラス名はビルド時にハッシュ化されて他コンポーネントから参照できなくなる。これを意図的に外したい (子コンポーネント側の特定クラスや外部ライブラリのクラスにスタイルを当てたい) 場合のみ `:global(...)` を使う:
```scss
.root {
:global(.someThirdPartyClass) {
color: var(--MI_THEME-fg);
}
}
```
通常はほぼ使わない。
### `:deep(...)` で子コンポーネント内部を狙う
```scss
.root :deep(.child-internal-class) {
color: var(--MI_THEME-accent);
}
```
これも頻用しない (子コンポーネントを直接修正する方が望ましい)。
## 命名
- module class は **camelCase** が慣習 (`root` / `inputCore` / `headerText`)
- BEM 風の `block__element--modifier` は使わない (CSS Modules でハッシュ化されるので名前衝突を心配する必要が無い)
- 状態 modifier は `&.active` / `&.disabled` のようにネストする
## ありがちなレビュー指摘
- `#fff` / `#000` / `rgba(0, 0, 0, 0.5)` のハードコード → `var(--MI_THEME-fg)` / `var(--MI_THEME-bg)` / `color-mix(...)` 等に置き換える
- `<style scoped>` で書いている (module ではない) → `<style lang="scss" module>` に直し、`:class="$style.foo"` で参照する
- 自前で `border-radius: 8px; padding: 14px;` を書いている → `_panel` global class 使えば不要
- 自前で button styling を書いている → `_button` global class を base に乗せる

View file

@ -0,0 +1,191 @@
# Storybook (`*.stories.impl.ts`) 規約
共有 `Mk*` コンポーネントには `Mk<Name>.stories.impl.ts`**同階層** に併設するのが慣習。
## 配置と命名
- **ファイル名は `.stories.impl.ts` 固定** (`.stories.ts``packages/frontend/.storybook/generate.tsx` による生成物で手編集・コミット不可)
- 同階層に置く (`components/MkButton.stories.impl.ts``components/global/MkAvatar.stories.impl.ts` 等)
- 先頭に TS コメント形式の SPDX ヘッダーが必要
## 基本: 単一 story (Default のみ)
シンプルなコンポーネントならこれで十分。(以下の `MkColoredTag` は説明用の**架空のコンポーネント名**。実在しない。実物のパターンは `MkButton.stories.impl.ts` を参照。)
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable import/no-default-export */
import type { StoryObj } from '@storybook/vue3';
import MkColoredTag from './MkColoredTag.vue';
export const Default = {
render(args) {
return {
components: { MkColoredTag },
setup() {
return { args };
},
template: '<MkColoredTag v-bind="args">タグ</MkColoredTag>',
};
},
args: {
variant: 'info',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkColoredTag>;
```
ポイント:
- 上 2 つの `eslint-disable` は Storybook のお作法で必須 (render の関数が return type を明示しないため / `default export` ではないため)
- `satisfies StoryObj<typeof MkColoredTag>` が無いと `args` の型補完が効かなくなる
## 複数 story (variant 別)
参考: [MkButton.stories.impl.ts](../../../../../packages/frontend/src/components/MkButton.stories.impl.ts)
variant / size / 状態などのバリエーションがあるなら、`Default` を base にして spread で派生させると簡潔。
```ts
export const Default = {
render(args) {
return {
components: { MkColoredTag },
setup() {
return { args };
},
template: '<MkColoredTag v-bind="args">タグ</MkColoredTag>',
};
},
args: {
variant: 'info',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkColoredTag>;
export const Warn = {
...Default,
args: { ...Default.args, variant: 'warn' },
} satisfies StoryObj<typeof MkColoredTag>;
export const Danger = {
...Default,
args: { ...Default.args, variant: 'danger' },
} satisfies StoryObj<typeof MkColoredTag>;
export const Disabled = {
...Default,
args: { ...Default.args, disabled: true },
} satisfies StoryObj<typeof MkColoredTag>;
```
## イベントを可視化する (`action()`)
クリック等の emit を Storybook の Actions panel で見たい場合、`storybook/actions``action()` を使う。
```ts
import { action } from 'storybook/actions';
// ...
export const Default = {
render(args) {
return {
components: { MkColoredTag },
setup() {
return { args };
},
computed: {
props() {
return { ...this.args };
},
events() {
return {
click: action('click'),
close: action('close'),
};
},
},
template: '<MkColoredTag v-bind="props" v-on="events">タグ</MkColoredTag>',
};
},
args: {},
parameters: { layout: 'centered' },
} satisfies StoryObj<typeof MkColoredTag>;
```
`MkButton.stories.impl.ts` がこのパターン。
## `argTypes` で controls を細かく制御
string union を radio に / number を range に変えるとレビューが楽になる。(標準の Storybook 機能。現状リポジトリ内の `.stories.impl.ts` では実際には使われていないので必須ではない。)
```ts
export const Default = {
render(args) { /* ... */ },
args: { variant: 'info' },
argTypes: {
variant: {
control: 'inline-radio',
options: ['info', 'warn', 'danger'],
},
disabled: {
control: 'boolean',
},
},
parameters: { layout: 'centered' },
} satisfies StoryObj<typeof MkColoredTag>;
```
## `parameters.layout` の使い分け
| 値 | 使い所 |
|---|---|
| `'centered'` | 単体表示 (ボタン、タグ、アイコン等の小さい部品) |
| `'fullscreen'` | ページ単位、もしくはパネル全体を見せたい時 |
| `'padded'` (デフォルト) | 周囲に余白が欲しい中サイズ部品 |
`layout` を変えるだけで Storybook 上の見え方が大きく変わる。レイアウト依存のコンポーネント (sticky header 等) なら `'fullscreen'` を選ぶ。
## slot の中身を可変にする
`args` に slot 用文字列フィールドを足し、template で `{{ args.label }}` のように展開する。
```ts
export const Default = {
render(args) {
return {
components: { MkColoredTag },
setup() {
return { args };
},
template: '<MkColoredTag v-bind="args">{{ args.label }}</MkColoredTag>',
};
},
args: {
label: 'タグ',
variant: 'info',
},
parameters: { layout: 'centered' },
} satisfies StoryObj<typeof MkColoredTag>;
```
ただし `label` を component の props にしてしまうのは禁物 (slot で受け取る方針なら slot のままにする)。Storybook 上だけで使う表示用文字列として扱う。
## 確認方法
```bash
pnpm --filter frontend storybook-dev # http://localhost:6006
pnpm --filter frontend build-storybook # 静的ビルド
```
新規コンポーネントの stories が Sidebar に出ない場合、多くは [generate.tsx](../../../../../packages/frontend/.storybook/generate.tsx) の生成対象 **allowlist** に入っていないため。`src/{components,pages,...}/**/*.vue` の全体 glob はコメントアウトされており、対象は `globSync('src/components/global/Mk*.vue')` / `globSync('src/components/Mk[B-E]*.vue')` などの**明示列挙**になっている。`.stories.impl.ts` を併設しただけでは自動では出ないことがあるので、対象外なら generate.tsx に 1 行追加する。加えて、ファイル名 (`.stories.impl.ts`) と SPDX ヘッダー以降に構文エラーが無いかも確認する。
Chromatic (`pnpm --filter frontend chromatic`) で視覚回帰チェックも行われる。

View file

@ -0,0 +1,124 @@
# i18n キーを追加・改修する
UI 文言の追加・変更を行う際の手順。**手動編集して良いのは `locales/ja-JP.yml` のみ**。
## 大前提 (絶対 NG)
- **`locales/<lang>.yml` (ja-JP.yml 以外) の編集は禁止**。これらは Crowdin の自動配信先で、手動編集すると次の同期で上書き喪失する ([locales/README.md](../../../../../locales/README.md), [crowdin.yml](../../../../../crowdin.yml))
- 文字列リテラルを SFC に直書きしない (`<span>こんにちは</span>` 等)。必ず `i18n.ts.<key>` を経由する
- 既存キーの破壊的リネームは Crowdin 翻訳資産を失わせる。**追加 → 移行 → 旧キー削除** の 3 段階に分割する。詳細手順と誤編集の復旧は [knowledge/i18n-usage.md §Crowdin 安全策](../knowledge/i18n-usage.md)
## ステップ 1: ja-JP.yml にキーを追加
[locales/ja-JP.yml](../../../../../locales/ja-JP.yml) を編集する。YAML の階層構造を維持し、関連するセクションに配置する:
```yaml
# トップレベル単純キー
save: "保存"
# ネストしたカテゴリ (アンダースコア接頭辞は内部カテゴリ)
_settings:
general: "全般"
appearance: "外観"
# パラメータ付き (単純なプレースホルダ置換)
# 受け付けるのは {name} 形式のみ。ICU MessageFormat (plural/select) は非対応
greeting: "こんにちは、{name}さん"
```
### 命名のお作法
- 単純キー: lowerCamelCase (例: `saveChanges`, `confirmDelete`)
- カテゴリ: アンダースコア接頭辞 (例: `_settings`, `_abuseUserReport`)
- 既存セクション内に追加する場合は **周辺の既存配置・意味グループに合わせる** (例えば `_settings` は機能ブロック順に並んでおりアルファベット順ではない)。新セクション全体を末尾に追加するのは可
- **HTML タグ (`<b>` `<br>` `<strong>` 等) や `:` `'` `&` を含む値は必ずダブルクォートで囲む** (未クォートだと YAML パース失敗)
**詳細:** ICU 非対応の代替戦略・予約キー `_lang_`・Storybook での挙動は → [knowledge/i18n-usage.md §制約と補足](../knowledge/i18n-usage.md)
## ステップ 2: 型定義の自動再生成
`packages/i18n/build.ts``ja-JP.yml` を解析し、TypeScript インターフェースを [packages/i18n/src/autogen/locale.ts](../../../../../packages/i18n/src/autogen/locale.ts) に出力する。
### 自動 (推奨)
`pnpm dev` 実行中なら、`packages/i18n` の watch スクリプト (`nodemon ... tsx ./build.ts --watch`) が yml の変更を検知して自動再生成する。
### 手動
```bash
pnpm --filter i18n generate
```
実体は `tsx scripts/generateLocaleInterface.ts`
### 失敗パターン
これを実行せずに frontend 側で `i18n.ts.<newKey>` を参照すると、`Locale` インターフェースに追加されていないため typecheck で `Property '<newKey>' does not exist on type 'Locale'` というエラーになる (`pnpm --filter frontend lint` で発覚)。型エラー・実行時警告 (`Unexpected locale key`, `Missing locale parameters`) と対処は → [knowledge/i18n-usage.md §トラブルシュート](../knowledge/i18n-usage.md)。
## ステップ 3: frontend での参照
```ts
import { i18n } from '@/i18n.js';
```
| 用途 | 書き方 |
|---|---|
| 単純文字列 | `i18n.ts.save` |
| ネスト | `i18n.ts._settings.general` |
| パラメータ付き | `i18n.tsx.greeting({ name: userName })` |
| Vue テンプレート内 | `{{ i18n.ts.save }}` / `{{ i18n.tsx.greeting({ name }) }}` |
`i18n.ts` は型付き文字列、`i18n.tsx``{name}` プレースホルダを埋め込む関数 (パラメータ付きキーのみ存在。ICU MessageFormat ではなく単純な文字列置換)。
**詳細:** HTML タグ埋め込み・computed によるリアクティブ参照・動的キー切替・ブラケット記法 (`i18n.ts['2fa']`) などの実装パターンは → [knowledge/i18n-usage.md §実装パターン](../knowledge/i18n-usage.md)
## ステップ 4: 検証
```bash
# i18n の型再生成 → typecheck + eslint (lint は generate を呼ばないので順番が必須)
pnpm --filter i18n generate
pnpm --filter i18n lint
# frontend で新キー参照箇所の型チェック
pnpm --filter frontend lint
# 他言語 yml に diff が出ていないことを確認 (出力が空であれば OK)
git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'
```
**注意:** `grep -v 'ja-JP.yml'`**diff 本文** に当てると ja-JP.yml 単体の変更でも `+追加行` が素通りして必ず非空になる。`--name-only` でファイル名だけに絞ってから完全一致で除外するのが正しい。
ユーザー影響のある UI 変更を伴う場合は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で CHANGELOG エントリの判定をする。
## 例: 「ノートを削除しますか?」確認ダイアログを追加する
1. `locales/ja-JP.yml`:
```yaml
_notes:
deleteConfirm: "このノートを削除しますか?"
```
2. `pnpm --filter i18n generate` (または `pnpm dev` で watch 中)
3. SFC:
```vue
<script setup lang="ts">
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
async function onDelete() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._notes.deleteConfirm,
});
if (canceled) return;
// 削除処理
}
</script>
```
## 参照ファイル
- [locales/README.md (★ 編集ポリシー根拠)](../../../../../locales/README.md)
- [locales/ja-JP.yml](../../../../../locales/ja-JP.yml)
- [packages/i18n/build.ts](../../../../../packages/i18n/build.ts)
- [packages/i18n/src/autogen/locale.ts (生成物)](../../../../../packages/i18n/src/autogen/locale.ts)
- [packages/frontend/src/i18n.ts](../../../../../packages/frontend/src/i18n.ts)

View file

@ -0,0 +1,196 @@
# 新規 / 既存 `Mk*` Vue コンポーネントを追加・改修する
`packages/frontend/src/components/` 配下に新規の共有 Vue 3 SFC を追加する、または既存コンポーネントを大きく改修する時の手順。同じ規約をレビュー側からチェックする agent が [.claude/agents/vue-component-reviewer.md](../../../../agents/vue-component-reviewer.md)。
## 大前提 (事故直結 / Critical)
1. **SPDX ヘッダー**`.vue` は HTML コメント形式 `<!-- ... -->``.stories.impl.ts` は TS コメント形式 `/* ... */`。欠落すると CI (`spdx` ジョブ) が落ちる
2. **`Mk` プレフィックス必須** — 共有コンポーネントは `MkButton.vue` / `global/MkAvatar.vue` のように `Mk` で始める。ページ固有 UI は `Mk` を付けず `pages/` 側に置く
3. **`locales/ja-JP.yml` のみ編集可** — i18n キー追加時に他言語 (`en-US.yml` 等) を手で触ってはいけない。Crowdin の自動配信で上書きされて失われる。詳細は [tasks/adding-i18n-key.md](adding-i18n-key.md) を参照
4. **文字列リテラルの直書き禁止** — テンプレート / JS どちらでも、ユーザーに見せる文言は必ず `i18n.ts.<key>``i18n.tsx.<key>(...)` 経由 → [knowledge/i18n-usage.md](../knowledge/i18n-usage.md)
5. **ブラウザ標準 UI を直接呼ばない**`alert()` / `confirm()` / `window.prompt()` は禁止、必ず `os.alert` / `os.confirm` / `os.popup` 経由 → [knowledge/os-api.md](../knowledge/os-api.md)
## ファイル配置
| 配置先 | 用途 | 命名 |
|---|---|---|
| `packages/frontend/src/components/Mk<Name>.vue` | 通常の共有 UI コンポーネント | `Mk<Name>.vue` |
| `packages/frontend/src/components/global/Mk<Name>.vue` | `components/index.ts` で Vue グローバルコンポーネント登録 (`app.component`) され、import 無しで全テンプレートから使える基本部品 (`MkA` / `MkAvatar` / `MkAcct` 等) | `Mk<Name>.vue` (サブディレクトリ内でも `Mk` prefix 必須) |
| `packages/frontend/src/components/grid/Mk<Name>.vue` | テーブル/グリッド系の部品セット | 同上 |
| `packages/frontend/src/pages/<Name>.vue` | 単一ページ専用の UI (再利用しない) | `Mk` prefix **不要** |
迷ったら「他の `Mk*.vue` から import される可能性があるか?」で判定する。Yes なら `components/`、No なら `pages/`
ストーリーが必要 (= ほぼ常に必要) なら、同階層に `Mk<Name>.stories.impl.ts` も作る → [knowledge/storybook.md](../knowledge/storybook.md)。
## SPDX ヘッダー
### `.vue` ファイル (HTML コメント)
```html
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
`/* ... */` (TS / JS 形式) は **使わない**。既存の `.vue` ファイルがすべて HTML コメント形式を採用しており、SFC 先頭として自然な形式に統一するため。
### `.stories.impl.ts` ファイル (TS コメント)
```ts
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
## 最小テンプレート
シンプルな表示コンポーネントの最小形を示す**合成例** (特定ファイルの写しではない)。実在する単純コンポーネントの例は [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) 等を参照:
```vue
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, $style[`variant_${variant}`]]">
<slot></slot>
<button
v-if="closable"
class="_button"
:class="$style.close"
:aria-label="i18n.ts.close"
@click="emit('close')"
>
<i class="ti ti-x"></i>
</button>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
variant?: 'info' | 'warn' | 'danger';
closable?: boolean;
}>(), {
variant: 'info',
});
const emit = defineEmits<{
(ev: 'close'): void;
}>();
</script>
<style lang="scss" module>
.root {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-radius: var(--MI-radius);
}
.variant_info {
background: var(--MI_THEME-infoBg);
color: var(--MI_THEME-infoFg);
}
.variant_warn {
background: var(--MI_THEME-infoWarnBg);
color: var(--MI_THEME-infoWarnFg);
}
.variant_danger {
background: var(--MI_THEME-error);
color: var(--MI_THEME-fgOnAccent);
}
.close {
margin-left: auto;
}
</style>
```
より複雑なケース (型ジェネリック / 2 ブロック script / `v-model` 連動 / 名前付き slot) は → [knowledge/component-conventions.md §テンプレート集](../knowledge/component-conventions.md)。
## `<script>` / `<style>` 規約サマリ
| 項目 | 規約 | 新規不可 |
|---|---|---|
| `<script>` 開始タグ | `<script lang="ts" setup>` または `<script setup lang="ts">` | `<script>` (lang 無し) / Options API |
| Props 定義 | `defineProps<{ ... }>()` (type-only) | runtime object 形式 |
| Emits 定義 | `defineEmits<{ (ev: 'click'): void }>()` (type-only) | runtime array 形式 |
| `<style>` 開始タグ | `<style lang="scss" module>`、参照は `:class="$style.foo"` | `<style scoped>` (module なし) |
| CSS 値 | `var(--MI_THEME-...)` / `var(--MI-...)` | `#fff` / `rgb(...)` のハードコード |
| グローバル class | `_button` / `_panel` / `_selectable` 等を活用 | — |
| アイコン | Tabler icons クラス `<i class="ti ti-info-circle">` | インライン SVG / 別アイコンセット |
詳細・テンプレート集は → [knowledge/component-conventions.md](../knowledge/component-conventions.md) / [knowledge/scss-modules.md](../knowledge/scss-modules.md)。
## i18n の使い分け
引数なし → `i18n.ts.<key>` / 引数あり → `i18n.tsx.<key>(...)`。詳細は → [knowledge/i18n-usage.md](../knowledge/i18n-usage.md)。
新キー追加が必要なら → [tasks/adding-i18n-key.md](adding-i18n-key.md)。
## `os.*` ヘルパー
`os.alert` / `os.confirm` / `os.popup` / `os.toast` / `os.popupMenu` 等。詳細は → [knowledge/os-api.md](../knowledge/os-api.md)。
## アクセシビリティ最低ライン
1. **クリック可能要素は `<button class="_button">` を第一選択**。やむを得ず `<div @click>` なら `role="button"` + `tabindex="0"` + `@keydown.enter` / `@keydown.space.prevent` の 4 点セット必須
2. **フォーム要素 (`<input>` / `<select>` / `<textarea>`) は `<label>` 接続もしくは `aria-label`**
3. **`:disabled` バインドと `aria-disabled` を一致**させる。ハンドラ側でも早期 return
4. **キーボードのみで完結**できるか確認 (Tab で focus 移動できる / Enter で確定できる)
5. ARIA 属性は最小限
詳細チェックリストと既存例 (`MkButton.vue` / `MkSwitch.vue`) は → [knowledge/component-conventions.md §a11y](../knowledge/component-conventions.md)。
## Storybook 併設
共有 `Mk*` コンポーネントには `Mk<Name>.stories.impl.ts`**同階層** に併設する (サブディレクトリ含む)。詳細は → [knowledge/storybook.md](../knowledge/storybook.md)。
## 検証フロー
```bash
# 型チェック (vue-tsc)
pnpm --filter frontend typecheck
# ESLint (規約全体)
pnpm --filter frontend eslint
# 単一ファイルに ESLint --fix
pnpm exec eslint --fix packages/frontend/src/components/Mk<Name>.vue
# Storybook で目視確認
pnpm --filter frontend storybook-dev # localhost:6006
# Vitest unit test (component spec があれば)
pnpm --filter frontend test
```
## CHANGELOG エントリ
ユーザーから見える変更 (新規コンポーネントが新しい UI として露出する、既存 UI の挙動を変える) なら、`CHANGELOG.md` に追記する。判定方法と書式は [shipping-misskey-change スキル](../../../shipping-misskey-change/SKILL.md) で確認。
## 既存コンポーネントとの整合性
- 似た用途の既存 `Mk*` を 1-2 個読んで、props 命名 (`primary` / `danger` / `small` 等の形容詞、`onClose` ではなく `emit('close')` 等) を揃える
- グローバル utility class (`_button` / `_panel` / `_selectable` / `_gaps_m`) を使えば独自スタイルを書かずに済む → [knowledge/scss-modules.md](../knowledge/scss-modules.md)
- 大きな機能なら Storybook で各バリエーション (variant / size / disabled / loading) を網羅する
## 参照コード
- [MkInfo.vue](../../../../../packages/frontend/src/components/MkInfo.vue) — simple SFC 例
- [MkButton.vue](../../../../../packages/frontend/src/components/MkButton.vue) — 汎用ボタン (a11y / `_button` global class)
- [MkInput.vue](../../../../../packages/frontend/src/components/MkInput.vue) — generic + 2 ブロック script 例
- [MkSelect.vue](../../../../../packages/frontend/src/components/MkSelect.vue) — `defineModel` + 名前付き slot 例
- [MkSwitch.vue](../../../../../packages/frontend/src/components/MkSwitch.vue) — a11y 込みカスタム UI
- [MkButton.stories.impl.ts](../../../../../packages/frontend/src/components/MkButton.stories.impl.ts) — 複数 story Storybook 雛形
- [packages/frontend/src/os.ts](../../../../../packages/frontend/src/os.ts) — UI 操作 API 一覧
- [packages/frontend/src/i18n.ts](../../../../../packages/frontend/src/i18n.ts) — `i18n.ts` / `i18n.tsx` 実装

View file

@ -2,36 +2,61 @@
このファイルは GitHub Copilot の repository-wide instructions として使われる。Copilot code review では `AGENTS.md` が読まれない環境があるため、レビューや軽微な実装判断に必要な規約はこのファイル単体で満たすこと。
このリポジトリは Misskey の pnpm workspace モノレポ。主要な実装は `packages/backend` (NestJS / TypeORM) と `packages/frontend` (Vue 3) にある。より詳しいガイドはリポジトリルートの `AGENTS.md` を参照してよいが、このファイルの要件を省略してそちらへの参照だけで済ませないこと。
リポジトリは Misskey の pnpm workspace モノレポ。主要な実装は `packages/backend` (NestJS / TypeORM) と `packages/frontend` (Vue 3) にある。より詳しいガイドはリポジトリルートの `AGENTS.md` を参照してよいが、このファイルの要件を省略してそちらへの参照だけで済ませないこと。
## Always follow
## 絶対にやってはいけない事
- AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリに新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.scss` ファイルを追加する場合は、必ず次の SPDX ヘッダーを付ける。詳細な対象判定は `AGENTS.md``.github/workflows/check-spdx-license-id.yml` を参照すること
違反すると CI 失敗 / 本番事故 になる
```text
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
### コード・データ関連
- AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリに新規 `.vue` / `.html` ファイルを追加する場合は、必ず次の SPDX ヘッダーを付ける
- **SPDX ヘッダー必須**: AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリに新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.scss` / `.vue` / `.html` ファイルを追加する場合は冒頭に必ず付ける。詳細な対象判定は `.github/workflows/check-spdx-license-id.yml` を参照。
```text
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
```text
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
`packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない。サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う。
新規 `.vue` / `.html` ファイルは HTML コメント形式で:
- `locales/` 配下の YAML は `ja-JP.yml` のみ手動編集してよい。他言語は Crowdin の自動配信先なので手動編集しないこと。
- `packages/backend/migration/{timestamp}-*.js` のうち、既にマージ済みの migration は絶対に編集しない。スキーマ変更が必要な場合は新しい timestamp で migration を追加し、`up()``down()` の両方を実装すること。
- ユーザー影響のある変更は `CHANGELOG.md``## Unreleased` 配下の `### General` / `### Client` / `### Server` のいずれかに 1 行追加する。内部リファクタのみなら不要。
- API 変更時は `pnpm build-misskey-js-with-types` の実行が必要になる。
```text
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
## Validation
`packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない (サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う)。
- **`locales/ja-JP.yml` 以外の locale YAML を編集しない**。他言語ファイル (`en-US.yml` など `ja-JP.yml` 以外すべて) は Crowdin の自動配信先で、手動編集すると次の同期で上書き喪失する。
- **マージ済 migration を編集しない**`packages/backend/migration/{timestamp}-*.js` のうち既に `develop` / `master` に入ったものは絶対に変更しない。スキーマ変更が必要なら新しい timestamp で新規ファイルを追加し、`up()``down()` の両方を実装する。
- **secrets / 認証情報をリポジトリにコミットしない** (`.config/*.yml` の本番値、`.env` ファイル、API token、private key 等)。
### Git / リポジトリ操作
- `git push --force` / `--force-with-lease``main` / `develop` / `master` にしない
- `git commit --no-verify` で hook をスキップしない
- マージ済 / プッシュ済コミットを `git commit --amend` で書き換えない
- 他人のブランチを `git reset --hard` / `git branch -D` で破壊しない
- `git config` をユーザーに無断で書き換えない (特に `user.name` / `user.email` / `commit.gpgsign`)
### Issue / PR / 外部送信
- ユーザーの明示指示なしに PR を merge / close / force-push しない
- ユーザーの明示指示なしに external service (GitHub comments / Slack / メール 等) へ送信しない
## 変更を出す前の最低チェック
1. `pnpm lint` が通る (typecheck + eslint, 全パッケージ)
2. backend で `meta` / `paramDef` / `res` を変更した → `pnpm build-misskey-js-with-types` を実行し `packages/misskey-js/src/autogen/` の差分も commit に含めた
3. entity / migration を変更した → `pnpm --filter backend check-migrations` が pending DDL 0 件で通る / 新規 migration は `up()``down()` 両方実装済
4. 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルを追加した → SPDX ヘッダーを付けた
5. ユーザー影響のある変更 → `CHANGELOG.md``## Unreleased` 配下の該当サブセクション (`### General` / `### Client` / `### Server`) に `- <Feat|Enhance|Fix>: <概要>` を 1 行追記
6. `locales/` を編集した場合、`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空 (ja-JP.yml 以外に差分が無い) ことを確認
## Validation コマンド
- 全体ビルド: `pnpm build`
- 全体 lint / typecheck: `pnpm lint`
@ -40,15 +65,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- Backend federation test: `pnpm --filter backend test:fed`
- Frontend test: `pnpm --filter frontend test`
- Migration 差分検査: `pnpm --filter backend check-migrations`
- `misskey-js` 再生成 (API 変更後必須): `pnpm build-misskey-js-with-types`
> **backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要。** 未作成の場合は `ncp .github/misskey/test.yml .config/test.yml` (または `cp .github/misskey/test.yml .config/test.yml`) を実行してから走らせる。各テストスクリプトが内部で `cross-env NODE_ENV=test pnpm compile-config` を呼ぶため、コピー済みであれば追加の compile-config は不要。
**注意:** backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要。未作成の場合は `ncp .github/misskey/test.yml .config/test.yml` (または `cp .github/misskey/test.yml .config/test.yml`) を実行してから走らせる。各テストスクリプトが内部で `cross-env NODE_ENV=test pnpm compile-config` を呼ぶため、コピー済みであれば追加の compile-config は不要。
変更範囲に応じて最も近いコマンドから優先して検証し、必要なら全体コマンドに広げること。
## Editing hints
- Backend の API / migration / TypeORM 変更は `packages/backend` を見る
- Frontend の Vue コンポーネントやページ変更は `packages/frontend` を見る
- `AGENTS.md` 内の相対リンクはリポジトリルート起点で解決する想定
- Backend の API / migration / TypeORM 変更は `packages/backend` を見る
- Frontend の Vue コンポーネントやページ変更は `packages/frontend` を見る
- `AGENTS.md` 内の相対リンクはリポジトリルート起点で解決する想定
> `AGENTS.md` はより詳細な正典だが、Copilot code review ではこのファイルが主な入口になる。両方が読まれる環境では `AGENTS.md` を補助情報として使ってよい。
**補足:** `AGENTS.md` はより詳細な正典 (Codex / Claude Code が読み込む)。Copilot code review ではこのファイルが主な入口になる。両方が読まれる環境では `AGENTS.md` を補助情報として使ってよい。

179
AGENTS.md
View file

@ -1,139 +1,104 @@
# Misskey AI Agent Guide
このファイルは Misskey リポジトリで動く AI コーディングエージェント (Claude Code / OpenAI Codex / GitHub Copilot 等) が共通で参照する **最低限のルールと索引**。次の 3 経路から参照・読み込みされる:
このファイルは Misskey リポジトリで動く AI コーディングエージェント (Claude Code / OpenAI Codex / GitHub Copilot 等) が共通で参照する **絶対禁止事項と最低限のチェック** を集めた索引。次の 3 経路から参照・読み込みされる:
- **Claude Code**: ルート `CLAUDE.md` から `@AGENTS.md` で取り込まれる
- **OpenAI Codex**: ルート `AGENTS.md` を直接読み込む
- **GitHub Copilot**: `.github/copilot-instructions.md` (本ファイルを参照しつつ、Copilot code review 向けに必須規約を再掲するファイル) 経由で参照する
- **Claude Code**: ルート `CLAUDE.md` から `@AGENTS.md` で取り込まれる。詳細手順・規約は `.claude/skills/` (description で自動索引)
- **OpenAI Codex**: ルート `AGENTS.md` を直接読み込む (skill エントリは `.agents/skills/`、実体は `.claude/skills/` を指す)
- **GitHub Copilot**: `.github/copilot-instructions.md` (本ファイルの規約を Copilot code review 向けに再掲) 経由で参照する
人間 contributor 向けの一般規約 (Issue / PR の出し方、ActivityPub 拡張など) は [CONTRIBUTING.md](CONTRIBUTING.md) を参照。本ファイルは AI が **コードを書く・直す** 際に踏み外してはいけない事項に絞っている。
人間 contributor 向けの一般規約 (Issue / PR の出し方、ActivityPub 拡張など) は [CONTRIBUTING.md](CONTRIBUTING.md) を参照。本ファイルは AI が **コードを書く・直す・出す** 際に踏み外してはいけない事項に絞る。
---
## 事故直結ルール (必ず守る)
## 絶対にやってはいけない事
違反すると CI 失敗または本番事故になる。順守すること。
違反すると CI 失敗 / 本番事故 / 共有環境破壊 になる。順守すること。
### 1. SPDX ヘッダー必須
### コード・データ関連
AGPL-3.0-only 管轄かつ SPDX CI 対象ディレクトリに新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイルを追加する場合、冒頭に以下を必ず付ける。欠落すると CI (`spdx` ジョブ) が失敗する。CI の対象判定は [.github/workflows/check-spdx-license-id.yml](.github/workflows/check-spdx-license-id.yml) の `directories` 配列を参照 (`*.config.{ts,js,cjs,mjs}``*eslint*` は除外)。
1. **SPDX ヘッダー欠落のまま AGPL 管轄ディレクトリへ新規ファイルを追加しない**
- 対象: 新規 `.ts` / `.js` / `.cjs` / `.mjs` / `.vue` / `.scss` / `.html` ファイル
- CI の対象判定は [.github/workflows/check-spdx-license-id.yml](.github/workflows/check-spdx-license-id.yml) の `directories` 配列を参照 (`*.config.{ts,js,cjs,mjs}``*eslint*` は除外)
- 欠落すると CI (`spdx` ジョブ) が失敗する
- `packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない (サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う)
`packages/misskey-js` は MIT ライセンスのサブパッケージなので、この AGPL ヘッダーを一律に付けない。サブパッケージ固有の `package.json` / `LICENSE` / 既存ファイルのヘッダーに従う。
`.ts` / `.js` / `.cjs` / `.mjs` / `.scss`:
`.ts` / `.js` / `.cjs` / `.mjs` / `.scss`:
```text
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
```text
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
```
`.vue` / `.html` (HTML コメント形式):
`.vue` / `.html` (HTML コメント形式):
```text
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
```text
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
```
2. **`locales/ja-JP.yml` 以外の locale YAML を手動編集しない**
- 他言語ファイル (`en-US.yml` など `ja-JP.yml` 以外すべて) は Crowdin の自動配信先。手動編集すると次の同期で上書き喪失する
- 根拠: [locales/README.md](locales/README.md) と [crowdin.yml](crowdin.yml) (`ja-JP.yml``locales/%locale%.yml` の同期設定)
### 2. locales/*.yml は `ja-JP.yml` のみ編集可
3. **マージ済 migration ファイルを編集しない**
- 対象: `packages/backend/migration/{unixMs}-{name}.js` のうち、既に `develop` / `master` にマージされたもの
- 本番環境で履歴改変が起きると深刻なデータ不整合を引き起こす
- スキーマ変更が必要な場合は **新しいタイムスタンプで新規ファイル** を作成する (`node -e "console.log(Date.now())"` でタイムスタンプ取得)
- 新規 migration は `up()``down()` の両方を実装し、`pnpm --filter backend check-migrations` を通すこと (TypeORM schema builder で pending DDL を検出)
`locales/` 配下の YAML は **`ja-JP.yml` のみ手動編集してよい**。他言語ファイル (`en-US.yml` 等) は Crowdin の自動配信先で、手動編集すると上書きで失われる。根拠: `locales/README.md` (ja-JP.yml 以外を手動編集しない運用) と `crowdin.yml` (`ja-JP.yml``locales/%locale%.yml` の同期設定)。
### Git / リポジトリ操作
### 3. マージ済み migration を絶対に編集しない
4. **`git push --force` / `--force-with-lease``main` / `develop` / `master` にしない** (他人の作業を消す可能性)
5. **`git commit --no-verify` で hook をスキップしない** (lint / format / SPDX チェックを潰す)
6. **マージ済 / プッシュ済コミットを `git commit --amend` で書き換えない** (履歴の整合性が壊れる)
7. **他人のブランチを `git reset --hard` / `git branch -D` で破壊しない**
8. **`git config` をユーザーに無断で書き換えない** (特に `user.name` / `user.email` / `commit.gpgsign`)
`packages/backend/migration/{unixMs}-{PascalName}.js` のうち、既に `develop` / `master` にマージ済みのファイルは **絶対に変更しない**。本番環境で履歴改変が起きると深刻なデータ不整合を引き起こす。
### Issue / PR / 外部送信
スキーマ変更が必要な場合は **新しいタイムスタンプで新規ファイル** を作成する:
9. **ユーザーの明示指示なしに PR を merge / close / force-push しない**
10. **ユーザーの明示指示なしに external service (GitHub comments / Slack / メール 等) へ送信しない**
11. **secrets / 認証情報をリポジトリにコミットしない** (`.config/*.yml` の本番値、`.env` ファイル、API token、private key 等)
- ファイル名: `node -e "console.log(Date.now())"` で UNIX ms を取得し、`{ms}-<descriptive-name>.js` として置く。命名スタイルは既存履歴で混在しており (`1716129964060-ChannelIdDenormalizedForMiPoll.js` のような PascalCase、`1721666053703-fixDriveUrl.js` のような camelCase、`1672704136584-remove-latestStatus.js` のような kebab-case)、変更を表す単一の英語名であれば良い。クラス名側は PascalCase + 13 桁タイムスタンプ (`class FixDriveUrl1721666053703 { ... }`) を必ず守ること。
- `up()``down()` の両方を必ず実装する (`down``up` の完全な巻き戻し)。
- `pnpm --filter backend check-migrations` を通す。これは **TypeORM schema builder で pending DDL を検出する** 検査 ([packages/backend/scripts/check_migrations_clean.js](packages/backend/scripts/check_migrations_clean.js))。エンティティの `@Column` / `@Entity` 変更が migration に取り込まれていないとここで検出される。タイムスタンプの順序自体を直接検査するわけではない (順序が壊れた場合の失敗は別経路で出る)。
### スキル呼び出し
エンティティ差分から TypeORM CLI で自動生成したい / `CREATE INDEX CONCURRENTLY` 等のオプションを使いたい場合は [.claude/skills/create-migration/SKILL.md](.claude/skills/create-migration/SKILL.md) を参照。手書き / CLI どちらの方式でも上記 3 点 (履歴改変禁止 / `up`+`down` / `check-migrations`) が満たせれば良い。
上流スキルの実行・事前知識・memory の内容に関わらず免除されない。
12. **`working-on-backend` スキルを参照せずに `packages/backend/` 配下のファイルを編集・追加しない**
13. **`working-on-frontend` スキルを参照せずに `packages/frontend/` 配下のファイルを編集・追加しない**
14. **`shipping-misskey-change` スキルを参照せずに commit / PR 作成 / 作業をユーザーに返さない**
---
## 必須コマンド
## 変更を出す前の最低チェック
各エージェントは [shipping-misskey-change スキル](.claude/skills/shipping-misskey-change/SKILL.md) を参照すること。スキルが利用できない環境でも、以下のチェックは必ず実施すること:
1. **lint**: `pnpm lint` が通る (typecheck + eslint, 全パッケージ)
2. **backend API 変更時**: `pnpm build-misskey-js-with-types` を実行し `packages/misskey-js/src/autogen/` の差分も commit に含めた
3. **entity / migration 変更時**: `pnpm --filter backend check-migrations` が pending DDL 0 件で通る / 新規 migration は `up()``down()` 両方実装済
4. **新規ファイル**: SPDX ヘッダーを付けた (`.vue` / `.html` は HTML コメント形式、それ以外は TS コメント形式)
5. **ユーザー影響のある変更**: `CHANGELOG.md``## Unreleased` 配下の該当サブセクション (`### General` / `### Client` / `### Server`) に `- <Feat|Enhance|Fix>: <概要>` を 1 行追記
6. **locale safety**: `locales/` を編集した場合、`git diff --name-only develop -- 'locales/*.yml' | grep -v '^locales/ja-JP\.yml$'` が空 (ja-JP.yml 以外に差分が無い) ことを確認
### Validation commands
各チェックで使う pnpm コマンド一覧。状況に応じて最も近いコマンドから検証する。
| 用途 | コマンド |
| --- | --- |
| 全体ビルド | `pnpm build` |
| 開発サーバー (backend + frontend watch) | `pnpm dev` |
| Lint (typecheck + eslint, 全パッケージ) | `pnpm lint` (= `pnpm --no-bail -r lint`。最初の失敗で止まらず全パッケージの結果を収集する) |
| Backend unit test (Vitest) | `pnpm --filter backend test` |
| 全体 lint (typecheck + eslint) | `pnpm lint` |
| Backend unit test | `pnpm --filter backend test` |
| Backend e2e test | `pnpm --filter backend test:e2e` |
| Backend federation test | `pnpm --filter backend test:fed` |
| Frontend test (Vitest) | `pnpm --filter frontend test` |
| Cypress E2E (要 `start:test`) | `pnpm e2e` |
| Storybook dev (frontend) | `pnpm --filter frontend storybook-dev` |
| Migration 適用 | `pnpm migrate` |
| Migration ロールバック | `pnpm revert` |
| Migration の pending DDL 検査 (エンティティ差分の取り込み漏れ検出) | `pnpm --filter backend check-migrations` |
| Frontend unit test | `pnpm --filter frontend test` |
| Migration 差分検査 (pending DDL) | `pnpm --filter backend check-migrations` |
| `misskey-js` 再生成 (API 変更後必須) | `pnpm build-misskey-js-with-types` |
| 全体ビルド | `pnpm build` |
| 開発サーバー (backend + frontend watch) | `pnpm dev` |
> Backend の TypeScript 型チェックは `pnpm --filter backend typecheck` (tsgo)。
> 個別ファイルへの ESLint --fix は `pnpm exec eslint --fix <path>`
> **backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要** (未作成だとテスト自体が起動しない)。コピー手順と詳細は [.claude/docs/testing.md §Backend 全般の前提](.claude/docs/testing.md#backend-全般の前提-configtestyml) を参照。
---
## CHANGELOG
ユーザー影響のある変更 (機能追加・修正・改善) は `CHANGELOG.md` の冒頭 `## Unreleased` セクションに 1 行追加する。リファクタリング等の内部変更は不要。
### セクション構造
`## Unreleased` 配下に **3 つのサブセクション** が用意されている:
- `### General` — 共通 / 横断的な変更
- `### Client``packages/frontend`
- `### Server``packages/backend`
### エントリ書式
該当サブセクションに `- <Prefix>: <概要>` の形式で追加。Prefix は先頭大文字。
```text
- Enhance: ノートの詳細表示での公開範囲の表示を改善
- Fix: 通知が約10秒遅延する問題を修正
- Feat: 新機能の追加
```
### 触ってはいけない範囲
- `## Unreleased` **以外** のセクション (過去リリース) は変更しない。
- `## Unreleased` の見出しと 3 つの空サブセクション骨格自体は維持する (リリーススクリプトが期待する構造)。
> 参考: コミットメッセージ側は `enhance(frontend): ...` / `fix(backend): ...` の小文字 + スコープ形式 ([CONTRIBUTING.md](CONTRIBUTING.md) 参照)。CHANGELOG とは書式が異なる点に注意。
---
## オンデマンド参照 (必要時に Read すること)
以下は AI が **作業対象に応じて必要なときだけ** 開く詳細ドキュメント。常時コンテキストには載せない。
| 何をしたい時 | 参照先 |
| --- | --- |
| パッケージ構成・依存関係を把握したい | [.claude/docs/architecture.md](.claude/docs/architecture.md) |
| `packages/backend` を編集する (NestJS / TypeORM / migration / API endpoint) | [.claude/docs/backend.md](.claude/docs/backend.md) |
| `packages/frontend` を編集する (Vue 3 / Mk* / i18n / SCSS module / `os.ts`) | [.claude/docs/frontend.md](.claude/docs/frontend.md) |
| テストを書く・走らせる (Vitest / Cypress / Storybook) | [.claude/docs/testing.md](.claude/docs/testing.md) |
| 有効化済 Claude Code プラグインの用途を確認 | [.claude/docs/plugins.md](.claude/docs/plugins.md) |
---
## ツール固有の補助ファイル
`.claude/` 配下は Claude Code 固有の skills / agents / slash commands を集約している (Codex / Copilot は読み飛ばしてよい):
- `.claude/skills/` — 繰り返しタスク用の skill 定義 (例: `add-api-endpoint`, `create-migration`)
- `.claude/agents/` — 専門レビューエージェント (例: `misskey-api-reviewer`, `vue-component-reviewer`)
- `.claude/commands/` — Claude Code のスラッシュコマンド (例: `/check-misskey-js`, `/changelog-add`)
- `.claude/docs/` — オンデマンド参照ドキュメント (上記の表で示したもの。Codex / Copilot からも内容自体は読める)
- `.claude/settings.json` — Claude Code の有効プラグイン (`enabledPlugins`) のみを記載した共有設定。hook は意図的に登録しない (各 contributor が `.claude/settings.local.json` で opt-in する方針)
- `.claude/settings.local.json` — 個人ローカル設定 (`.gitignore` 済)
サードパーティ由来 (everything-claude-code 由来の MIT ライセンスファイル等) の出典は [.claude/THIRD_PARTY_LICENSES.md](.claude/THIRD_PARTY_LICENSES.md) を参照。
**注意:** backend テスト (`test` / `test:e2e` / `test:fed`) 実行前に `.config/test.yml` が必要 (`ncp .github/misskey/test.yml .config/test.yml` または `cp .github/misskey/test.yml .config/test.yml` で作成)。