Repository: misskey-dev/misskey
Pull request #15953: fix(backend): enumerate achievement types in some response schema
Author: login: zyoshoka
Labels: name: packages/backend
description: Server side specific issue/PR
name: packages/misskey-js
name: packages/backend:test
Created: 2025-05-05T05:18:23Z | Merged: 2025-05-06T10:52:30Z

<!-- ℹ お読みください / README
PRありがとうございます！ PRを作成する前に、コントリビューションガイドをご確認ください:
Thank you for your PR! Before creating a PR, please check the contribution guide:
https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
-->

## What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->
実績タイプを API レスポンススキーマに含めるようにします。

## Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
API ドキュメント改善

## Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->

## Checklist
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
- [ ] Test working in a local environment
- [ ] (If needed) Add story of storybook
- [ ] (If needed) Update CHANGELOG.md
- [ ] (If possible) Add tests



Diff:
diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts
index 4fc1193f32a..8d2de89efd6 100644
--- a/packages/backend/src/core/AchievementService.ts
+++ b/packages/backend/src/core/AchievementService.ts
@@ -9,87 +9,7 @@ import type { MiUser } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import { NotificationService } from '@/core/NotificationService.js';
-
-export const ACHIEVEMENT_TYPES = [
-	'notes1',
-	'notes10',
-	'notes100',
-	'notes500',
-	'notes1000',
-	'notes5000',
-	'notes10000',
-	'notes20000',
-	'notes30000',
-	'notes40000',
-	'notes50000',
-	'notes60000',
-	'notes70000',
-	'notes80000',
-	'notes90000',
-	'notes100000',
-	'login3',
-	'login7',
-	'login15',
-	'login30',
-	'login60',
-	'login100',
-	'login200',
-	'login300',
-	'login400',
-	'login500',
-	'login600',
-	'login700',
-	'login800',
-	'login900',
-	'login1000',
-	'passedSinceAccountCreated1',
-	'passedSinceAccountCreated2',
-	'passedSinceAccountCreated3',
-	'loggedInOnBirthday',
-	'loggedInOnNewYearsDay',
-	'noteClipped1',
-	'noteFavorited1',
-	'myNoteFavorited1',
-	'profileFilled',
-	'markedAsCat',
-	'following1',
-	'following10',
-	'following50',
-	'following100',
-	'following300',
-	'followers1',
-	'followers10',
-	'followers50',
-	'followers100',
-	'followers300',
-	'followers500',
-	'followers1000',
-	'collectAchievements30',
-	'viewAchievements3min',
-	'iLoveMisskey',
-	'foundTreasure',
-	'client30min',
-	'client60min',
-	'noteDeletedWithin1min',
-	'postedAtLateNight',
-	'postedAt0min0sec',
-	'selfQuote',
-	'htl20npm',
-	'viewInstanceChart',
-	'outputHelloWorldOnScratchpad',
-	'open3windows',
-	'driveFolderCircularReference',
-	'reactWithoutRead',
-	'clickedClickHere',
-	'justPlainLucky',
-	'setNameToSyuilo',
-	'cookieClicked',
-	'brainDiver',
-	'smashTestNotificationButton',
-	'tutorialCompleted',
-	'bubbleGameExplodingHead',
-	'bubbleGameDoubleExplodingHead',
-] as const;
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
 
 @Injectable()
 export class AchievementService {
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index 27aa3d89de7..e4eb10efca5 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -67,6 +67,7 @@ import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessage
 import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
 import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
 import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
+import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
 
 export const refs = {
 	UserLite: packedUserLiteSchema,
@@ -78,6 +79,8 @@ export const refs = {
 	User: packedUserSchema,
 
 	UserList: packedUserListSchema,
+	Achievement: packedAchievementSchema,
+	AchievementName: packedAchievementNameSchema,
 	Ad: packedAdSchema,
 	Announcement: packedAnnouncementSchema,
 	App: packedAppSchema,
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 55445552961..c4c1fa5ec94 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -274,7 +274,7 @@ export class MiUserProfile {
 		default: [],
 	})
 	public achievements: {
-		name: string;
+		name: typeof ACHIEVEMENT_TYPES[number];
 		unlockedAt: number;
 	}[];
 
@@ -295,3 +295,84 @@ export class MiUserProfile {
 		}
 	}
 }
+
+export const ACHIEVEMENT_TYPES = [
+	'notes1',
+	'notes10',
+	'notes100',
+	'notes500',
+	'notes1000',
+	'notes5000',
+	'notes10000',
+	'notes20000',
+	'notes30000',
+	'notes40000',
+	'notes50000',
+	'notes60000',
+	'notes70000',
+	'notes80000',
+	'notes90000',
+	'notes100000',
+	'login3',
+	'login7',
+	'login15',
+	'login30',
+	'login60',
+	'login100',
+	'login200',
+	'login300',
+	'login400',
+	'login500',
+	'login600',
+	'login700',
+	'login800',
+	'login900',
+	'login1000',
+	'passedSinceAccountCreated1',
+	'passedSinceAccountCreated2',
+	'passedSinceAccountCreated3',
+	'loggedInOnBirthday',
+	'loggedInOnNewYearsDay',
+	'noteClipped1',
+	'noteFavorited1',
+	'myNoteFavorited1',
+	'profileFilled',
+	'markedAsCat',
+	'following1',
+	'following10',
+	'following50',
+	'following100',
+	'following300',
+	'followers1',
+	'followers10',
+	'followers50',
+	'followers100',
+	'followers300',
+	'followers500',
+	'followers1000',
+	'collectAchievements30',
+	'viewAchievements3min',
+	'iLoveMisskey',
+	'foundTreasure',
+	'client30min',
+	'client60min',
+	'noteDeletedWithin1min',
+	'postedAtLateNight',
+	'postedAt0min0sec',
+	'selfQuote',
+	'htl20npm',
+	'viewInstanceChart',
+	'outputHelloWorldOnScratchpad',
+	'open3windows',
+	'driveFolderCircularReference',
+	'reactWithoutRead',
+	'clickedClickHere',
+	'justPlainLucky',
+	'setNameToSyuilo',
+	'cookieClicked',
+	'brainDiver',
+	'smashTestNotificationButton',
+	'tutorialCompleted',
+	'bubbleGameExplodingHead',
+	'bubbleGameDoubleExplodingHead',
+] as const;
diff --git a/packages/backend/src/models/json-schema/achievement.ts b/packages/backend/src/models/json-schema/achievement.ts
new file mode 100644
index 00000000000..39a621a5706
--- /dev/null
+++ b/packages/backend/src/models/json-schema/achievement.ts
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
+
+export const packedAchievementNameSchema = {
+	type: 'string',
+	enum: ACHIEVEMENT_TYPES,
+	optional: false,
+} as const;
+
+export const packedAchievementSchema = {
+	type: 'object',
+	properties: {
+		name: {
+			ref: 'AchievementName',
+		},
+		unlockedAt: {
+			type: 'number',
+			optional: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 7f23d2d6a17..6de120c8d70 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -3,7 +3,6 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
 import { notificationTypes, userExportableEntities } from '@/types.js';
 
 const baseSchema = {
@@ -312,9 +311,7 @@ export const packedNotificationSchema = {
 				enum: ['achievementEarned'],
 			},
 			achievement: {
-				type: 'string',
-				optional: false, nullable: false,
-				enum: ACHIEVEMENT_TYPES,
+				ref: 'AchievementName',
 			},
 		},
 	}, {
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index e4752967023..2b5f706ff94 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -630,18 +630,7 @@ export const packedMeDetailedOnlySchema = {
 			type: 'array',
 			nullable: false, optional: false,
 			items: {
-				type: 'object',
-				nullable: false, optional: false,
-				properties: {
-					name: {
-						type: 'string',
-						nullable: false, optional: false,
-					},
-					unlockedAt: {
-						type: 'number',
-						nullable: false, optional: false,
-					},
-				},
+				ref: 'Achievement',
 			},
 		},
 		loggedInDays: {
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index e70905ef1b7..0e42647ef70 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -5,7 +5,8 @@
 
 import { Injectable } from '@nestjs/common';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
+import { AchievementService } from '@/core/AchievementService.js';
+import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
 
 export const meta = {
 	requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/users/achievements.ts b/packages/backend/src/server/api/endpoints/users/achievements.ts
index f7139b36841..bae216e3472 100644
--- a/packages/backend/src/server/api/endpoints/users/achievements.ts
+++ b/packages/backend/src/server/api/endpoints/users/achievements.ts
@@ -14,15 +14,7 @@ export const meta = {
 	res: {
 		type: 'array',
 		items: {
-			type: 'object',
-			properties: {
-				name: {
-					type: 'string',
-				},
-				unlockedAt: {
-					type: 'number',
-				},
-			},
+			ref: 'Achievement',
 		},
 	},
 } as const;
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
index ce3f931bb04..ca6a639be8d 100644
--- a/packages/backend/test/unit/entities/UserEntityService.ts
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
 			const me = await createUser({}, {
 				birthday: '2000-01-01',
 				achievements: achievements,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 7069d323170..79813d3f823 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -30,6 +30,12 @@ declare namespace acct {
 }
 export { acct }
 
+// @public (undocumented)
+type Achievement = components['schemas']['Achievement'];
+
+// @public (undocumented)
+type AchievementName = components['schemas']['AchievementName'];
+
 // @public (undocumented)
 type Ad = components['schemas']['Ad'];
 
@@ -2084,6 +2090,8 @@ declare namespace entities {
         UserDetailed,
         User,
         UserList,
+        Achievement,
+        AchievementName,
         Ad,
         Announcement,
         App,
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 15c3ee7e551..354daf800b7 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -8,6 +8,8 @@ export type MeDetailed = components['schemas']['MeDetailed'];
 export type UserDetailed = components['schemas']['UserDetailed'];
 export type User = components['schemas']['User'];
 export type UserList = components['schemas']['UserList'];
+export type Achievement = components['schemas']['Achievement'];
+export type AchievementName = components['schemas']['AchievementName'];
 export type Ad = components['schemas']['Ad'];
 export type Announcement = components['schemas']['Announcement'];
 export type App = components['schemas']['App'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index c83e1f1fbe8..c54cc571d26 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4303,10 +4303,7 @@ export type components = {
         }]>;
       };
       emailNotificationTypes: string[];
-      achievements: {
-          name: string;
-          unlockedAt: number;
-        }[];
+      achievements: components['schemas']['Achievement'][];
       loggedInDays: number;
       policies: components['schemas']['RolePolicies'];
       /** @default false */
@@ -4344,6 +4341,12 @@ export type components = {
       userIds?: string[];
       isPublic: boolean;
     };
+    Achievement: {
+      name: components['schemas']['AchievementName'];
+      unlockedAt: number;
+    };
+    /** @enum {string} */
+    AchievementName: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
     Ad: {
       /**
        * Format: id
@@ -4619,16 +4622,15 @@ export type components = {
       /** @enum {string} */
       type: 'chatRoomInvitationReceived';
       invitation: components['schemas']['ChatRoomInvitation'];
-    } | ({
+    } | {
       /** Format: id */
       id: string;
       /** Format: date-time */
       createdAt: string;
       /** @enum {string} */
       type: 'achievementEarned';
-      /** @enum {string} */
-      achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
-    }) | ({
+      achievement: components['schemas']['AchievementName'];
+    } | ({
       /** Format: id */
       id: string;
       /** Format: date-time */
@@ -28531,10 +28533,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': {
-              name: string;
-              unlockedAt: number;
-            }[];
+          'application/json': components['schemas']['Achievement'][];
         };
       };
       /** @description Client error */


Reviews:
user: login: KisaragiEffective
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T05:20:34Z
user: login: zyoshoka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T05:21:50Z
user: login: KisaragiEffective
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T05:22:41Z
user: login: zyoshoka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T05:42:09Z
user: login: KisaragiEffective
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T05:42:47Z
user: login: zyoshoka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T05:50:34Z

Review comments:
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: KisaragiEffective
body: そもそも実績はいらなさそうな気がします
created_at: 2025-05-05T05:20:11Z
author_association: COLLABORATOR
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: zyoshoka
body: その後で整合性チェックしてそうな気がしますが、どうでしょう
created_at: 2025-05-05T05:21:50Z
author_association: MEMBER
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: KisaragiEffective
body: `service.pack`しても与えたachievementsはそのままになると思います。
created_at: 2025-05-05T05:22:41Z
author_association: COLLABORATOR
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: zyoshoka
body: それは確かにそうなんですが、そのままで返ってくることをテストしていると思えばあった方がいいと思います。
created_at: 2025-05-05T05:42:09Z
author_association: MEMBER
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: KisaragiEffective
body: 自分が言いたかったのはこのテストは実績依存ではないはずなので、値を空配列にしても良いのではないか？ということです。
created_at: 2025-05-05T05:42:47Z
author_association: COLLABORATOR
diff_hunk: @@ -232,7 +232,7 @@ describe('UserEntityService', () => {
 		});
 
 		test('MeDetailed', async() => {
-			const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }];
+			const achievements = [{ name: 'iLoveMisskey' as const, unlockedAt: new Date().getTime() }];
path: packages/backend/test/unit/entities/UserEntityService.ts
user: login: zyoshoka
body: なるほどです。とはいえ、その理屈で言えば同様に `birthday` を `null` にしてテストしても良いのではという話になってきそうです。自分としては意味のある値が入っている方が一目で分かりやすいかなと思っています。
created_at: 2025-05-05T05:50:34Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-05T05:19:28Z
body: ## [Codecov](https://app.codecov.io/gh/misskey-dev/misskey/pull/15953?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 48.22%. Comparing base [(`6bbda4d`)](https://app.codecov.io/gh/misskey-dev/misskey/commit/6bbda4d67c42b94e5918fef755d9fae5d1cf13d5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev) to head [(`9e2ab18`)](https://app.codecov.io/gh/misskey-dev/misskey/commit/9e2ab18de2b08196de89a1668f281dfbb42140f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).
> Report is 11 commits behind head on develop.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop   #15953      +/-   ##
===========================================
+ Coverage    46.38%   48.22%   +1.83%     
===========================================
  Files         1696     1701       +5     
  Lines       172316   178568    +6252     
  Branches      5321     5353      +32     
===========================================
+ Hits         79929    86111    +6182     
- Misses       91910    91999      +89     
+ Partials       477      458      -19     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/misskey-dev/misskey/pull/15953?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=misskey-dev).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-05T05:20:25Z
body: このPRによるapi.jsonの差分
<details>
<summary>差分はこちら</summary>

```diff
--- base
+++ head
@@ -1,7 +1,7 @@
 {
   "openapi": "3.1.0",
   "info": {
-    "version": "2025.5.0-alpha.2",
+    "version": "2025.5.0-alpha.1",
     "title": "Misskey API"
   },
   "externalDocs": {
@@ -77555,19 +77555,7 @@
                 "schema": {
                   "type": "array",
                   "items": {
-                    "type": "object",
-                    "properties": {
-                      "name": {
-                        "type": "string"
-                      },
-                      "unlockedAt": {
-                        "type": "number"
-                      }
-                    },
-                    "required": [
-                      "name",
-                      "unlockedAt"
-                    ]
+                    "$ref": "#/components/schemas/Achievement"
                   }
                 }
               }
@@ -84794,19 +84782,7 @@
           "achievements": {
             "type": "array",
             "items": {
-              "type": "object",
-              "properties": {
-                "name": {
-                  "type": "string"
-                },
-                "unlockedAt": {
-                  "type": "number"
-                }
-              },
-              "required": [
-                "name",
-                "unlockedAt"
-              ]
+              "$ref": "#/components/schemas/Achievement"
             }
           },
           "loggedInDays": {
@@ -84994,6 +84970,104 @@
           "isPublic"
         ]
       },
+      "Achievement": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "$ref": "#/components/schemas/AchievementName"
+          },
+          "unlockedAt": {
+            "type": "number"
+          }
+        },
+        "required": [
+          "name",
+          "unlockedAt"
+        ]
+      },
+      "AchievementName": {
+        "type": "string",
+        "enum": [
+          "notes1",
+          "notes10",
+          "notes100",
+          "notes500",
+          "notes1000",
+          "notes5000",
+          "notes10000",
+          "notes20000",
+          "notes30000",
+          "notes40000",
+          "notes50000",
+          "notes60000",
+          "notes70000",
+          "notes80000",
+          "notes90000",
+          "notes100000",
+          "login3",
+          "login7",
+          "login15",
+          "login30",
+          "login60",
+          "login100",
+          "login200",
+          "login300",
+          "login400",
+          "login500",
+          "login600",
+          "login700",
+          "login800",
+          "login900",
+          "login1000",
+          "passedSinceAccountCreated1",
+          "passedSinceAccountCreated2",
+          "passedSinceAccountCreated3",
+          "loggedInOnBirthday",
+          "loggedInOnNewYearsDay",
+          "noteClipped1",
+          "noteFavorited1",
+          "myNoteFavorited1",
+          "profileFilled",
+          "markedAsCat",
+          "following1",
+          "following10",
+          "following50",
+          "following100",
+          "following300",
+          "followers1",
+          "followers10",
+          "followers50",
+          "followers100",
+          "followers300",
+          "followers500",
+          "followers1000",
+          "collectAchievements30",
+          "viewAchievements3min",
+          "iLoveMisskey",
+          "foundTreasure",
+          "client30min",
+          "client60min",
+          "noteDeletedWithin1min",
+          "postedAtLateNight",
+          "postedAt0min0sec",
+          "selfQuote",
+          "htl20npm",
+          "viewInstanceChart",
+          "outputHelloWorldOnScratchpad",
+          "open3windows",
+          "driveFolderCircularReference",
+          "reactWithoutRead",
+          "clickedClickHere",
+          "justPlainLucky",
+          "setNameToSyuilo",
+          "cookieClicked",
+          "brainDiver",
+          "smashTestNotificationButton",
+          "tutorialCompleted",
+          "bubbleGameExplodingHead",
+          "bubbleGameDoubleExplodingHead"
+        ]
+      },
       "Ad": {
         "type": "object",
         "properties": {
@@ -85979,87 +86053,7 @@
                 ]
               },
               "achievement": {
-                "type": "string",
-                "enum": [
-                  "notes1",
-                  "notes10",
-                  "notes100",
-                  "notes500",
-                  "notes1000",
-                  "notes5000",
-                  "notes10000",
-                  "notes20000",
-                  "notes30000",
-                  "notes40000",
-                  "notes50000",
-                  "notes60000",
-                  "notes70000",
-                  "notes80000",
-                  "notes90000",
-                  "notes100000",
-                  "login3",
-                  "login7",
-                  "login15",
-                  "login30",
-                  "login60",
-                  "login100",
-                  "login200",
-                  "login300",
-                  "login400",
-                  "login500",
-                  "login600",
-                  "login700",
-                  "login800",
-                  "login900",
-                  "login1000",
-                  "passedSinceAccountCreated1",
-                  "passedSinceAccountCreated2",
-                  "passedSinceAccountCreated3",
-                  "loggedInOnBirthday",
-                  "loggedInOnNewYearsDay",
-                  "noteClipped1",
-                  "noteFavorited1",
-                  "myNoteFavorited1",
-                  "profileFilled",
-                  "markedAsCat",
-                  "following1",
-                  "following10",
-                  "following50",
-                  "following100",
-                  "following300",
-                  "followers1",
-                  "followers10",
-                  "followers50",
-                  "followers100",
-                  "followers300",
-                  "followers500",
-                  "followers1000",
-                  "collectAchievements30",
-                  "viewAchievements3min",
-                  "iLoveMisskey",
-                  "foundTreasure",
-                  "client30min",
-                  "client60min",
-                  "noteDeletedWithin1min",
-                  "postedAtLateNight",
-                  "postedAt0min0sec",
-                  "selfQuote",
-                  "htl20npm",
-                  "viewInstanceChart",
-                  "outputHelloWorldOnScratchpad",
-                  "open3windows",
-                  "driveFolderCircularReference",
-                  "reactWithoutRead",
-                  "clickedClickHere",
-                  "justPlainLucky",
-                  "setNameToSyuilo",
-                  "cookieClicked",
-                  "brainDiver",
-                  "smashTestNotificationButton",
-                  "tutorialCompleted",
-                  "bubbleGameExplodingHead",
-                  "bubbleGameDoubleExplodingHead"
-                ]
+                "$ref": "#/components/schemas/AchievementName"
               }
             },
             "required": [
```
[Get diff files from Workflow Page](https://github.com/misskey-dev/misskey/actions/runs/14829856633)

<!-- thollander/actions-comment-pull-request "show_diff" -->
author_association: CONTRIBUTOR
user: login: zyoshoka
created_at: 2025-05-05T05:21:10Z
body: `models/json-schema` 下に置いて `ref` したほうがいい気がしてきたので draft
author_association: MEMBER
user: login: syuilo
created_at: 2025-05-06T10:52:33Z
body: 🙏🏻 
author_association: MEMBER