@amityco/social-plus-vise 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const INTERNAL_PACKAGE_NAMES = {
4
+ "@amityco/social-plus-vise": "the Vise source tree",
5
+ "social-plus-blocks": "the social.plus blocks product tree",
6
+ };
7
+ export async function detectInternalWorkspace(repoPath) {
8
+ const root = path.resolve(repoPath);
9
+ const markers = [];
10
+ const rootName = await packageName(root);
11
+ if (rootName && INTERNAL_PACKAGE_NAMES[rootName]) {
12
+ markers.push(`package.json name "${rootName}" — this is ${INTERNAL_PACKAGE_NAMES[rootName]}`);
13
+ }
14
+ const workspaceFile = path.join(root, "pnpm-workspace.yaml");
15
+ if (await readFile(workspaceFile, "utf8").then(() => true, () => false)) {
16
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
17
+ for (const entry of entries) {
18
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
19
+ continue;
20
+ }
21
+ const memberName = await packageName(path.join(root, entry.name));
22
+ if (memberName && INTERNAL_PACKAGE_NAMES[memberName]) {
23
+ markers.push(`workspace member ${entry.name}/ is "${memberName}" — this is ${INTERNAL_PACKAGE_NAMES[memberName]}`);
24
+ }
25
+ }
26
+ }
27
+ return { isInternalWorkspace: markers.length > 0, markers };
28
+ }
29
+ export function internalWorkspaceWarning(detection) {
30
+ return {
31
+ detectedMarkers: detection.markers,
32
+ reason: "This target looks like the social.plus Vise / social.plus blocks development workspace, not a customer project.",
33
+ guidance: "Customer-integration commands write sp-vise/ state into a customer repo. For SDK surface facts while packaging blocks, use `vise sdk-facts` (projectless and read-only). To run the customer flow against a workspace fixture, point Vise at the fixture directory itself.",
34
+ };
35
+ }
36
+ async function packageName(directory) {
37
+ try {
38
+ const parsed = JSON.parse(await readFile(path.join(directory, "package.json"), "utf8"));
39
+ return typeof parsed.name === "string" ? parsed.name : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
@@ -70,6 +70,119 @@ const CAPABILITIES = {
70
70
  ],
71
71
  models: [{ name: "Post", kind: "model", owner: "Amity" }],
72
72
  },
73
+ community: {
74
+ required: [
75
+ { name: "CommunityRepository", kind: "type" },
76
+ { name: "getCommunities", kind: "member", owner: "CommunityRepository" },
77
+ { name: "joinCommunity", kind: "member", owner: "CommunityRepository" },
78
+ ],
79
+ optional: [
80
+ { name: "getCommunity", kind: "member", owner: "CommunityRepository" },
81
+ { name: "createCommunity", kind: "member", owner: "CommunityRepository" },
82
+ { name: "leaveCommunity", kind: "member", owner: "CommunityRepository" },
83
+ { name: "updateCommunity", kind: "member", owner: "CommunityRepository" },
84
+ { name: "deleteCommunity", kind: "member", owner: "CommunityRepository" },
85
+ { name: "searchCommunities", kind: "member", owner: "CommunityRepository" },
86
+ { name: "getRecommendedCommunities", kind: "member", owner: "CommunityRepository" },
87
+ { name: "getTrendingCommunities", kind: "member", owner: "CommunityRepository" },
88
+ { name: "onCommunityCreated", kind: "member", owner: "CommunityRepository" },
89
+ { name: "onCommunityUpdated", kind: "member", owner: "CommunityRepository" },
90
+ { name: "onCommunityDeleted", kind: "member", owner: "CommunityRepository" },
91
+ ],
92
+ models: [{ name: "Community", kind: "model", owner: "Amity" }],
93
+ },
94
+ chat: {
95
+ required: [
96
+ { name: "ChannelRepository", kind: "type" },
97
+ { name: "MessageRepository", kind: "type" },
98
+ { name: "getChannels", kind: "member", owner: "ChannelRepository" },
99
+ { name: "getMessages", kind: "member", owner: "MessageRepository" },
100
+ { name: "createMessage", kind: "member", owner: "MessageRepository" },
101
+ ],
102
+ optional: [
103
+ { name: "getChannel", kind: "member", owner: "ChannelRepository" },
104
+ { name: "createChannel", kind: "member", owner: "ChannelRepository" },
105
+ { name: "joinChannel", kind: "member", owner: "ChannelRepository" },
106
+ { name: "leaveChannel", kind: "member", owner: "ChannelRepository" },
107
+ { name: "archiveChannel", kind: "member", owner: "ChannelRepository" },
108
+ { name: "unarchiveChannel", kind: "member", owner: "ChannelRepository" },
109
+ { name: "muteChannel", kind: "member", owner: "ChannelRepository" },
110
+ { name: "searchChannels", kind: "member", owner: "ChannelRepository" },
111
+ { name: "getSubChannels", kind: "member", owner: "SubChannelRepository" },
112
+ { name: "editMessage", kind: "member", owner: "MessageRepository" },
113
+ { name: "deleteMessage", kind: "member", owner: "MessageRepository" },
114
+ { name: "flagMessage", kind: "member", owner: "MessageRepository" },
115
+ { name: "unflagMessage", kind: "member", owner: "MessageRepository" },
116
+ { name: "isMessageFlaggedByMe", kind: "member", owner: "MessageRepository" },
117
+ { name: "searchMessage", kind: "member", owner: "MessageRepository" },
118
+ { name: "markAsDelivered", kind: "member", owner: "MessageRepository" },
119
+ { name: "onMessageCreatedMqtt", kind: "member", owner: "MessageRepository" },
120
+ { name: "onMessageUpdated", kind: "member", owner: "MessageRepository" },
121
+ { name: "onMessageDeleted", kind: "member", owner: "MessageRepository" },
122
+ ],
123
+ models: [
124
+ { name: "Channel", kind: "model", owner: "Amity" },
125
+ { name: "Message", kind: "model", owner: "Amity" },
126
+ { name: "SubChannel", kind: "model", owner: "Amity" },
127
+ ],
128
+ },
129
+ livestream: {
130
+ required: [
131
+ { name: "RoomRepository", kind: "type" },
132
+ { name: "getRoom", kind: "member", owner: "RoomRepository" },
133
+ { name: "getRooms", kind: "member", owner: "RoomRepository" },
134
+ ],
135
+ optional: [
136
+ { name: "createRoom", kind: "member", owner: "RoomRepository" },
137
+ { name: "updateRoom", kind: "member", owner: "RoomRepository" },
138
+ { name: "deleteRoom", kind: "member", owner: "RoomRepository" },
139
+ { name: "leaveRoom", kind: "member", owner: "RoomRepository" },
140
+ { name: "stopRoom", kind: "member", owner: "RoomRepository" },
141
+ { name: "removeParticipant", kind: "member", owner: "RoomRepository" },
142
+ { name: "getBroadcasterData", kind: "member", owner: "RoomRepository" },
143
+ { name: "getRecordedUrl", kind: "member", owner: "RoomRepository" },
144
+ { name: "getPlayer", kind: "member", owner: "LiveStreamPlayer" },
145
+ { name: "getRoomOnlineUsers", kind: "member", owner: "RoomPresenceRepository" },
146
+ { name: "getRoomUserCount", kind: "member", owner: "RoomPresenceRepository" },
147
+ ],
148
+ models: [{ name: "Room", kind: "model", owner: "Amity" }],
149
+ },
150
+ "notification-tray": {
151
+ required: [
152
+ { name: "notificationTray", kind: "type" },
153
+ { name: "getNotificationTrayItems", kind: "member", owner: "notificationTray" },
154
+ ],
155
+ optional: [
156
+ { name: "getNotificationTraySeen", kind: "member", owner: "notificationTray" },
157
+ { name: "markItemsSeen", kind: "member", owner: "notificationTray" },
158
+ { name: "markTraySeen", kind: "member", owner: "notificationTray" },
159
+ { name: "onNotificationTraySeenUpdated", kind: "member", owner: "notificationTray" },
160
+ ],
161
+ models: [
162
+ { name: "NotificationTrayItem", kind: "model", owner: "Amity" },
163
+ { name: "NotificationTraySeen", kind: "model", owner: "Amity" },
164
+ ],
165
+ },
166
+ events: {
167
+ required: [
168
+ { name: "EventRepository", kind: "type" },
169
+ { name: "getEvents", kind: "member", owner: "EventRepository" },
170
+ { name: "getRSVPs", kind: "member", owner: "EventRepository" },
171
+ ],
172
+ optional: [
173
+ { name: "getEvent", kind: "member", owner: "EventRepository" },
174
+ { name: "getMyEvents", kind: "member", owner: "EventRepository" },
175
+ { name: "createEvent", kind: "member", owner: "EventRepository" },
176
+ { name: "updateEvent", kind: "member", owner: "EventRepository" },
177
+ { name: "deleteEvent", kind: "member", owner: "EventRepository" },
178
+ { name: "onEventCreated", kind: "member", owner: "EventRepository" },
179
+ { name: "onEventUpdated", kind: "member", owner: "EventRepository" },
180
+ { name: "onEventDeleted", kind: "member", owner: "EventRepository" },
181
+ { name: "onRSVPCreated", kind: "member", owner: "EventRepository" },
182
+ { name: "onRSVPUpdated", kind: "member", owner: "EventRepository" },
183
+ ],
184
+ models: [{ name: "Event", kind: "model", owner: "Amity" }],
185
+ },
73
186
  users: {
74
187
  required: [
75
188
  { name: "UserRepository", kind: "type" },
@@ -88,6 +201,113 @@ const CAPABILITIES = {
88
201
  models: [{ name: "User", kind: "model", owner: "Amity" }],
89
202
  },
90
203
  };
204
+ const FLUTTER_CAPABILITIES = {
205
+ comments: {
206
+ required: [
207
+ { name: "CommentRepository", kind: "type" },
208
+ { name: "getComments", kind: "member", owner: "CommentRepository" },
209
+ { name: "createComment", kind: "member", owner: "CommentRepository" },
210
+ ],
211
+ optional: [
212
+ { name: "getComment", kind: "member", owner: "CommentRepository" },
213
+ { name: "updateComment", kind: "member", owner: "CommentRepository" },
214
+ ],
215
+ models: [{ name: "AmityComment", kind: "model" }],
216
+ },
217
+ reactions: {
218
+ required: [
219
+ { name: "AmityReactionRepository", kind: "type" },
220
+ { name: "addReaction", kind: "member", owner: "AmityReactionRepository" },
221
+ { name: "removeReaction", kind: "member", owner: "AmityReactionRepository" },
222
+ ],
223
+ optional: [
224
+ { name: "getReactions", kind: "member", owner: "AmityReactionRepository" },
225
+ ],
226
+ models: [{ name: "AmityReaction", kind: "model" }],
227
+ },
228
+ posts: {
229
+ required: [
230
+ { name: "PostRepository", kind: "type" },
231
+ { name: "getPosts", kind: "member", owner: "PostRepository" },
232
+ { name: "createPost", kind: "member", owner: "PostRepository" },
233
+ ],
234
+ optional: [
235
+ { name: "getPost", kind: "member", owner: "PostRepository" },
236
+ { name: "deletePost", kind: "member", owner: "PostRepository" },
237
+ { name: "reviewPost", kind: "member", owner: "PostRepository" },
238
+ { name: "getPinnedPosts", kind: "member", owner: "PostRepository" },
239
+ { name: "isFlaggedByMe", kind: "member", owner: "PostRepository" },
240
+ ],
241
+ models: [{ name: "AmityPost", kind: "model" }],
242
+ },
243
+ users: {
244
+ required: [
245
+ { name: "UserRepository", kind: "type" },
246
+ { name: "getUser", kind: "member", owner: "UserRepository" },
247
+ ],
248
+ optional: [
249
+ { name: "getUsers", kind: "member", owner: "UserRepository" },
250
+ { name: "searchUserByDisplayName", kind: "member", owner: "UserRepository" },
251
+ { name: "updateUser", kind: "member", owner: "UserRepository" },
252
+ { name: "getBlockedUsers", kind: "member", owner: "UserRepository" },
253
+ { name: "report", kind: "member", owner: "UserRepository" },
254
+ { name: "follow", kind: "member", owner: "AmityUserRelationshipRepository" },
255
+ { name: "getFollowInfo", kind: "member", owner: "AmityUserRelationshipRepository" },
256
+ ],
257
+ models: [{ name: "AmityUser", kind: "model" }],
258
+ },
259
+ community: {
260
+ required: [
261
+ { name: "AmityCommunityRepository", kind: "type" },
262
+ { name: "getCommunities", kind: "member", owner: "AmityCommunityRepository" },
263
+ { name: "joinCommunity", kind: "member", owner: "AmityCommunityRepository" },
264
+ ],
265
+ optional: [
266
+ { name: "getCommunity", kind: "member", owner: "AmityCommunityRepository" },
267
+ { name: "createCommunity", kind: "member", owner: "AmityCommunityRepository" },
268
+ { name: "leaveCommunity", kind: "member", owner: "AmityCommunityRepository" },
269
+ { name: "updateCommunity", kind: "member", owner: "AmityCommunityRepository" },
270
+ { name: "deleteCommunity", kind: "member", owner: "AmityCommunityRepository" },
271
+ { name: "getRecommendedCommunities", kind: "member", owner: "AmityCommunityRepository" },
272
+ { name: "getTrendingCommunities", kind: "member", owner: "AmityCommunityRepository" },
273
+ ],
274
+ models: [{ name: "AmityCommunity", kind: "model" }],
275
+ },
276
+ chat: {
277
+ required: [
278
+ { name: "AmityChannelRepository", kind: "type" },
279
+ { name: "MessageRepository", kind: "type" },
280
+ { name: "getChannels", kind: "member", owner: "AmityChannelRepository" },
281
+ { name: "getMessages", kind: "member", owner: "MessageRepository" },
282
+ { name: "createMessage", kind: "member", owner: "MessageRepository" },
283
+ ],
284
+ optional: [
285
+ { name: "getChannel", kind: "member", owner: "AmityChannelRepository" },
286
+ { name: "createChannel", kind: "member", owner: "AmityChannelRepository" },
287
+ { name: "joinChannel", kind: "member", owner: "AmityChannelRepository" },
288
+ { name: "leaveChannel", kind: "member", owner: "AmityChannelRepository" },
289
+ { name: "archiveChannel", kind: "member", owner: "AmityChannelRepository" },
290
+ { name: "unarchiveChannel", kind: "member", owner: "AmityChannelRepository" },
291
+ { name: "muteChannel", kind: "member", owner: "AmityChannelRepository" },
292
+ { name: "searchChannels", kind: "member", owner: "AmityChannelRepository" },
293
+ { name: "getMessage", kind: "member", owner: "MessageRepository" },
294
+ { name: "deleteMessage", kind: "member", owner: "MessageRepository" },
295
+ { name: "flagMessage", kind: "member", owner: "MessageRepository" },
296
+ { name: "updateMessage", kind: "member", owner: "MessageRepository" },
297
+ { name: "searchMessage", kind: "member", owner: "MessageRepository" },
298
+ { name: "getSubChannels", kind: "member", owner: "AmitySubChannelRepository" },
299
+ ],
300
+ models: [
301
+ { name: "AmityChannel", kind: "model" },
302
+ { name: "AmityMessage", kind: "model" },
303
+ { name: "AmitySubChannel", kind: "model" },
304
+ ],
305
+ },
306
+ };
307
+ const CAPABILITY_ANCHORS = {
308
+ typescript: CAPABILITIES,
309
+ flutter: FLUTTER_CAPABILITIES,
310
+ };
91
311
  export const getSdkFactsTool = {
92
312
  name: "get_sdk_facts",
93
313
  description: "Read bundled social.plus SDK surface facts for block packaging. Projectless and read-only; answers symbol existence, not semantic correctness.",
@@ -181,7 +401,12 @@ export async function getSdkFacts(options) {
181
401
  "Remote SP_SDK_SURFACE_URL resolution is recorded in the manifest for a future online snapshot source; this MVP uses local override/env/bundled directories only.",
182
402
  ...(platform !== "typescript"
183
403
  ? [
184
- `Capability anchors are authored TypeScript-first at this MVP, so capabilities is empty for ${platform}. This means capability mapping is not yet available on ${platform} — it does NOT mean the SDK lacks the feature. The symbol and model facts above are extracted from the ${platform} surface and remain authoritative.`,
404
+ (() => {
405
+ const anchored = Object.keys(CAPABILITY_ANCHORS[platform] ?? {});
406
+ return anchored.length > 0
407
+ ? `Capability anchors on ${platform} cover: ${anchored.join(", ")}. Families not listed are not yet anchored on ${platform} — capability mapping is unavailable for them, which does NOT mean the SDK lacks the feature. The symbol and model facts above are extracted from the ${platform} surface and remain authoritative.`
408
+ : `Capability anchors are authored TypeScript-first at this MVP, so capabilities is empty for ${platform}. This means capability mapping is not yet available on ${platform} — it does NOT mean the SDK lacks the feature. The symbol and model facts above are extracted from the ${platform} surface and remain authoritative.`;
409
+ })(),
185
410
  ]
186
411
  : []),
187
412
  ],
@@ -264,20 +489,21 @@ async function readJson(filePath) {
264
489
  }
265
490
  function requestedCapabilityIds(platform, capability) {
266
491
  const available = Object.keys(CAPABILITIES);
492
+ const anchored = Object.keys(CAPABILITY_ANCHORS[platform] ?? {});
267
493
  if (!capability) {
268
- return platform === "typescript" ? available : [];
494
+ return anchored;
269
495
  }
270
496
  const normalized = capability.trim().toLowerCase();
271
497
  if (!available.includes(normalized)) {
272
498
  throw new Error(`Unsupported SDK capability: ${capability}. Supported capabilities: ${available.join(", ")}.`);
273
499
  }
274
- if (platform !== "typescript") {
275
- return [];
276
- }
277
- return [normalized];
500
+ return anchored.includes(normalized) ? [normalized] : [];
278
501
  }
279
502
  function buildCapabilityFact(id, platform, symbols, modelFacts) {
280
- const definition = CAPABILITIES[id];
503
+ const definition = CAPABILITY_ANCHORS[platform]?.[id];
504
+ if (!definition) {
505
+ throw new Error(`No ${platform} anchor set for capability ${id}; requestedCapabilityIds must filter unanchored families.`);
506
+ }
281
507
  const requiredSymbols = definition.required.map((symbol) => checkSymbol(symbols, { ...symbol, required: true }));
282
508
  const optionalSymbols = definition.optional.map((symbol) => checkSymbol(symbols, { ...symbol, required: false }));
283
509
  const models = definition.models.map((symbol) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -103,6 +103,12 @@
103
103
  "minLength": 1,
104
104
  "description": "Installable social.plus block id for this object. Source of truth for blockIdForObject. Omitted when the object has a surface but no dedicated block (e.g. forum, story). Absent on app-layer/in-development objects."
105
105
  },
106
+ "sdkCapabilities": {
107
+ "type": "array",
108
+ "minItems": 1,
109
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
110
+ "description": "SDK capability ids from Vise's sdk-facts table (vise/src/tools/sdkFacts.ts CAPABILITIES) that ground this object. Declared only when the id exists and `vise sdk-facts` proves it on the TypeScript surface — never guessed; omit when no proven mapping exists (e.g. story, app-layer objects). Consumed by the social.plus blocks catalog gap-report so draft block requests carry proven capability ids instead of a TODO(human) marker."
111
+ },
106
112
  "availability": { "$ref": "#/$defs/availability" }
107
113
  }
108
114
  },
@@ -2,17 +2,17 @@
2
2
  "schemaVersion": "0.1.0",
3
3
  "status": "seeded",
4
4
  "items": [
5
- { "id": "feed", "name": "Feed", "description": "A stream of social posts or updates.", "category": "content", "surface": "feed", "block": "feed" },
6
- { "id": "comment", "name": "Comment", "description": "A response thread attached to content or an object.", "category": "participation", "surface": "comments", "block": "comments" },
7
- { "id": "forum", "name": "Forum", "description": "A structured discussion space optimized for topics and knowledge.", "category": "knowledge", "surface": "feed" },
8
- { "id": "community", "name": "Community", "description": "A persistent group identity around people, content, or purpose.", "category": "identity", "surface": "community", "block": "community" },
9
- { "id": "creator", "name": "Creator", "description": "A person or entity that owns an audience relationship.", "category": "identity", "surface": "profile", "block": "profile" },
10
- { "id": "profile", "name": "Profile", "description": "A user or creator identity surface with social relationship signals.", "category": "identity", "surface": "profile", "block": "profile" },
11
- { "id": "chat", "name": "Chat", "description": "Direct or group conversation.", "category": "conversation", "surface": "chat", "block": "chat" },
5
+ { "id": "feed", "name": "Feed", "description": "A stream of social posts or updates.", "category": "content", "surface": "feed", "block": "feed", "sdkCapabilities": ["posts"] },
6
+ { "id": "comment", "name": "Comment", "description": "A response thread attached to content or an object.", "category": "participation", "surface": "comments", "block": "comments", "sdkCapabilities": ["comments"] },
7
+ { "id": "forum", "name": "Forum", "description": "A structured discussion space optimized for topics and knowledge.", "category": "knowledge", "surface": "feed", "sdkCapabilities": ["posts"] },
8
+ { "id": "community", "name": "Community", "description": "A persistent group identity around people, content, or purpose.", "category": "identity", "surface": "community", "block": "community", "sdkCapabilities": ["community"] },
9
+ { "id": "creator", "name": "Creator", "description": "A person or entity that owns an audience relationship.", "category": "identity", "surface": "profile", "block": "profile", "sdkCapabilities": ["users"] },
10
+ { "id": "profile", "name": "Profile", "description": "A user or creator identity surface with social relationship signals.", "category": "identity", "surface": "profile", "block": "profile", "sdkCapabilities": ["users"] },
11
+ { "id": "chat", "name": "Chat", "description": "Direct or group conversation.", "category": "conversation", "surface": "chat", "block": "chat", "sdkCapabilities": ["chat"] },
12
12
  { "id": "story", "name": "Story", "description": "Ephemeral or sequenced media updates.", "category": "content", "surface": "feed" },
13
- { "id": "livestream", "name": "Livestream", "description": "Real-time video or audio participation.", "category": "live", "surface": "livestream", "block": "livestream" },
14
- { "id": "event", "name": "Event", "description": "A scheduled social moment.", "category": "live", "surface": "notifications", "block": "events" },
15
- { "id": "rsvp", "name": "RSVP", "description": "A user's intent to attend or participate in an event.", "category": "commitment", "surface": "notifications", "block": "events" },
13
+ { "id": "livestream", "name": "Livestream", "description": "Real-time video or audio participation.", "category": "live", "surface": "livestream", "block": "livestream", "sdkCapabilities": ["livestream"] },
14
+ { "id": "event", "name": "Event", "description": "A scheduled social moment.", "category": "live", "surface": "notifications", "block": "events", "sdkCapabilities": ["events"] },
15
+ { "id": "rsvp", "name": "RSVP", "description": "A user's intent to attend or participate in an event.", "category": "commitment", "surface": "notifications", "block": "events", "sdkCapabilities": ["events"] },
16
16
  { "id": "challenge", "name": "Challenge", "description": "A time-bounded task or contest.", "category": "participation", "availability": "in-development" },
17
17
  { "id": "mission", "name": "Mission", "description": "A guided objective that encourages progression.", "category": "progression", "availability": "in-development" },
18
18
  { "id": "reward", "name": "Reward", "description": "A benefit or acknowledgement earned through behavior.", "category": "incentive", "availability": "in-development" },
@@ -21,6 +21,6 @@
21
21
  { "id": "leaderboard", "name": "Leaderboard", "description": "Ranked comparison between participants.", "category": "competition", "availability": "in-development" },
22
22
  { "id": "knowledge", "name": "Knowledge", "description": "Reusable question, answer, guide, or expertise content.", "category": "knowledge" },
23
23
  { "id": "prediction", "name": "Prediction", "description": "A user's forecast or choice about a future outcome.", "category": "participation" },
24
- { "id": "notification", "name": "Notification", "description": "A timely signal that brings users back to an engagement moment.", "category": "activation", "surface": "notifications", "block": "notifications" }
24
+ { "id": "notification", "name": "Notification", "description": "A timely signal that brings users back to an engagement moment.", "category": "activation", "surface": "notifications", "block": "notifications", "sdkCapabilities": ["notification-tray"] }
25
25
  ]
26
26
  }
@@ -63,13 +63,13 @@
63
63
  "typescript": {
64
64
  "status": "extracted",
65
65
  "file": "models.typescript.json",
66
- "bytes": 35400,
67
- "sha256": "cdd531fe0904d1815f72760450b1c6610be3f72b4c2e1ba80011456c17ab1a6b",
66
+ "bytes": 102455,
67
+ "sha256": "8ba407b1d3f0228e6af4a0c5e99e8031950e106295bc64bf5dd67c600a9681e5",
68
68
  "fieldsGrounding": "names-and-types",
69
- "modelCount": 6,
69
+ "modelCount": 14,
70
70
  "extractor": "vise-ts-model-extractor",
71
71
  "extractorVersion": "0.1.0",
72
- "extractedAt": "2026-07-06T05:09:06.557Z"
72
+ "extractedAt": "2026-07-08T06:54:23.130Z"
73
73
  },
74
74
  "android": {
75
75
  "status": "extracted",