@amityco/social-plus-vise 1.6.0 → 1.8.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.
@@ -39,7 +39,7 @@ export async function inspectProject(repoPath, surfacePath) {
39
39
  const surfaces = await detectProjectSurfaces(repoRoot);
40
40
  const effectiveRoot = resolveSurfaceRoot(repoRoot, surfacePath);
41
41
  const selectedSurface = surfacePath ? await surfaceForPath(repoRoot, effectiveRoot) : surfaceMatchingRoot(surfaces, repoRoot);
42
- const { platforms, signals, designSignals } = await inspectRoot(effectiveRoot);
42
+ const { platforms, signals, designSignals, scanCoverage } = await inspectRoot(effectiveRoot);
43
43
  return {
44
44
  repoRoot,
45
45
  effectiveRoot,
@@ -48,6 +48,7 @@ export async function inspectProject(repoPath, surfacePath) {
48
48
  signals,
49
49
  designSignals,
50
50
  surfaces,
51
+ scanCoverage,
51
52
  };
52
53
  }
53
54
  function resolveSurfaceRoot(repoRoot, surfacePath) {
@@ -63,6 +64,7 @@ function resolveSurfaceRoot(repoRoot, surfacePath) {
63
64
  }
64
65
  async function inspectRoot(root) {
65
66
  const signals = [];
67
+ const scanContext = { validationRoot: root, scans: [] };
66
68
  const candidates = [
67
69
  ["android", "settings.gradle", "Gradle project settings"],
68
70
  ["android", "settings.gradle.kts", "Gradle Kotlin project settings"],
@@ -100,7 +102,7 @@ async function inspectRoot(root) {
100
102
  }
101
103
  const hasJsSignal = signals.some((signal) => signal.platform === "typescript" || signal.platform === "react-native");
102
104
  if (!hasJsSignal) {
103
- const sourceSignal = await detectSdkSignalFromSource(root);
105
+ const sourceSignal = await detectSdkSignalFromSource(root, scanContext);
104
106
  if (sourceSignal) {
105
107
  signals.push(sourceSignal);
106
108
  }
@@ -113,10 +115,18 @@ async function inspectRoot(root) {
113
115
  : hasAndroid
114
116
  ? rawPlatforms.filter((p) => p !== "typescript")
115
117
  : rawPlatforms;
116
- return { platforms, signals, designSignals: await detectDesignSignals(root) };
118
+ return {
119
+ platforms,
120
+ signals,
121
+ designSignals: await detectDesignSignals(root),
122
+ scanCoverage: {
123
+ complete: scanContext.scans.every((scan) => !scan.truncated),
124
+ scans: scanContext.scans,
125
+ },
126
+ };
117
127
  }
118
- async function detectSdkSignalFromSource(root) {
119
- const sourceFiles = await findFiles(root, [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".html", ".htm"], 500);
128
+ async function detectSdkSignalFromSource(root, scanContext) {
129
+ const sourceFiles = await findFiles(root, [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".html", ".htm"], 500, scanContext, "platform-detection.javascript-source");
120
130
  if (sourceFiles.length === 0) {
121
131
  return null;
122
132
  }
@@ -269,7 +279,25 @@ async function installedBlockEntryStillValid(root, entry) {
269
279
  return false;
270
280
  }
271
281
  }
272
- return true;
282
+ const publicEntrypoint = typeof entry.publicEntrypoint === "string" ? entry.publicEntrypoint : "";
283
+ const importName = typeof entry.importName === "string" ? entry.importName : "";
284
+ const mountFiles = Array.isArray(entry.mountFiles) ? entry.mountFiles : [];
285
+ if (!publicEntrypoint || !importName || mountFiles.length === 0) {
286
+ return false;
287
+ }
288
+ const usagePattern = platform === "flutter"
289
+ ? new RegExp(`\\b${escapeRegExp(importName)}\\s*\\(`)
290
+ : new RegExp(`<\\s*${escapeRegExp(importName)}(?:\\s|/|>)|(?:React\\.)?createElement\\s*\\(\\s*${escapeRegExp(importName)}\\b|\\b${escapeRegExp(importName)}\\s*\\(`);
291
+ for (const mountFile of mountFiles) {
292
+ if (typeof mountFile !== "string" || mountFile.trim() === "") {
293
+ continue;
294
+ }
295
+ const source = await readIfExists(path.join(root, mountFile));
296
+ if (source.includes(publicEntrypoint) && usagePattern.test(source)) {
297
+ return true;
298
+ }
299
+ }
300
+ return false;
273
301
  }
274
302
  async function sidecarOutcomeInScope(root, outcome) {
275
303
  const dir = sidecarDir(root);
@@ -354,29 +382,56 @@ export const validateSetupTool = {
354
382
  const repoPath = stringField(args, "repoPath");
355
383
  const inspection = await inspectProject(repoPath, optionalStringField(args, "surfacePath"));
356
384
  const platform = optionalStringField(args, "platform") ?? inspection.platforms[0] ?? "unknown";
357
- const findings = await validateSetup(inspection.effectiveRoot, platform);
385
+ const validation = await validateSetupWithCoverage(inspection.effectiveRoot, platform);
386
+ const platformWasExplicit = optionalStringField(args, "platform") !== undefined;
387
+ const scanCoverage = platformWasExplicit
388
+ ? validation.scanCoverage
389
+ : {
390
+ complete: inspection.scanCoverage.complete && validation.scanCoverage.complete,
391
+ scans: [...inspection.scanCoverage.scans, ...validation.scanCoverage.scans],
392
+ };
393
+ const findings = [...validation.findings];
394
+ if (!platformWasExplicit && !inspection.scanCoverage.complete && !findings.some((finding) => finding.ruleId === "project.scan.complete")) {
395
+ findings.push(incompleteInspectionFinding(inspection.scanCoverage));
396
+ }
358
397
  return textResult({
359
398
  platform,
360
399
  surfacePath: inspection.selectedSurface?.path,
361
400
  status: statusForFindings(findings),
362
401
  findings,
402
+ scanCoverage,
363
403
  });
364
404
  },
365
405
  };
366
406
  export async function validateSetup(repoPath, platform) {
407
+ return (await validateSetupWithCoverage(repoPath, platform)).findings;
408
+ }
409
+ function incompleteInspectionFinding(scanCoverage) {
410
+ const truncated = scanCoverage.scans.filter((scan) => scan.truncated);
411
+ return {
412
+ ruleId: "project.scan.complete",
413
+ severity: "error",
414
+ message: `Vise could not inspect the complete project inventory: ${truncated
415
+ .map((scan) => `${scan.scope} reached its ${scan.limit}-file limit under ${scan.root}`)
416
+ .join("; ")}.`,
417
+ recommendation: "Point Vise at the specific app with --surface/--surface-path before planning, initializing, validating, or selecting sensors.",
418
+ };
419
+ }
420
+ export async function validateSetupWithCoverage(repoPath, platform) {
367
421
  const root = path.resolve(repoPath);
368
422
  const findings = [];
423
+ const scanContext = { validationRoot: root, scans: [] };
369
424
  if (platform === "android") {
370
- findings.push(...(await validateAndroid(root)));
425
+ findings.push(...(await validateAndroid(root, scanContext)));
371
426
  }
372
427
  if (platform === "flutter") {
373
- findings.push(...(await validateFlutter(root)));
428
+ findings.push(...(await validateFlutter(root, scanContext)));
374
429
  }
375
430
  if (platform === "typescript" || platform === "react-native") {
376
- findings.push(...(await validateTypeScript(root, platform)));
431
+ findings.push(...(await validateTypeScript(root, platform, scanContext)));
377
432
  }
378
433
  if (platform === "ios") {
379
- findings.push(...(await validateIos(root)));
434
+ findings.push(...(await validateIos(root, scanContext)));
380
435
  }
381
436
  if (platform === "unknown") {
382
437
  findings.push({
@@ -386,21 +441,38 @@ export async function validateSetup(repoPath, platform) {
386
441
  recommendation: "Point repoPath at the app root or provide a platform override.",
387
442
  });
388
443
  }
389
- return findings;
444
+ const truncatedScans = scanContext.scans.filter((scan) => scan.truncated);
445
+ if (truncatedScans.length > 0) {
446
+ findings.push({
447
+ ruleId: "project.scan.complete",
448
+ severity: "error",
449
+ message: `Vise could not inspect the complete source surface: ${truncatedScans
450
+ .map((scan) => `${scan.scope} reached its ${scan.limit}-file limit under ${scan.root}`)
451
+ .join("; ")}.`,
452
+ recommendation: "Point Vise at the specific app with --surface/--surface-path, remove generated or vendored source from that surface, or raise the governed scan limit before relying on absence-based validation.",
453
+ });
454
+ }
455
+ return {
456
+ findings,
457
+ scanCoverage: {
458
+ complete: truncatedScans.length === 0,
459
+ scans: scanContext.scans,
460
+ },
461
+ };
390
462
  }
391
- async function validateAndroid(root) {
463
+ async function validateAndroid(root, scanContext) {
392
464
  const findings = [];
393
465
  const manifestPath = "app/src/main/AndroidManifest.xml";
394
466
  const manifest = await readIfExists(path.join(root, manifestPath));
395
- const gradleScripts = (await findFiles(root, [".gradle", ".kts"], 200)).filter((f) => /(?:^|[\\/])(?:build|settings)\.gradle(?:\.kts)?$/.test(f));
467
+ const gradleScripts = (await findFiles(root, [".gradle", ".kts"], 200, scanContext, "android.gradle-manifests")).filter((f) => /(?:^|[\\/])(?:build|settings)\.gradle(?:\.kts)?$/.test(f));
396
468
  const buildFiles = [...new Set([
397
469
  ...(await existingFiles(root, ["app/build.gradle", "app/build.gradle.kts", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"])),
398
470
  ...gradleScripts,
399
471
  ])];
400
- const versionCatalogFiles = (await findFiles(root, [".toml"], 50)).filter((f) => /(?:^|[\\/])gradle[\\/].*\.versions\.toml$/.test(f));
472
+ const versionCatalogFiles = (await findFiles(root, [".toml"], 50, scanContext, "android.version-catalogs")).filter((f) => /(?:^|[\\/])gradle[\\/].*\.versions\.toml$/.test(f));
401
473
  const buildContent = await readMany(buildFiles);
402
474
  const versionCatalogContent = await readMany(versionCatalogFiles);
403
- const sourceFiles = await findFiles(root, [".kt", ".java"], 300);
475
+ const sourceFiles = await findFiles(root, [".kt", ".java"], 300, scanContext, "android.source");
404
476
  const sourceContent = await readMany(sourceFiles);
405
477
  const setupFiles = filesMatching(sourceContent, [/AmityCoreClient\s*\.\s*setup/, /AmityClient\s*\.\s*setup/, /AmityUIKit\d*Manager\s*\.\s*setup/]);
406
478
  const coreLoginFiles = filesMatching(sourceContent, [/AmityCoreClient\s*\.\s*login/, /AmityClient\s*\.\s*login/]);
@@ -518,11 +590,11 @@ async function validateAndroid(root) {
518
590
  findings.push(...(await validateEnvSecretHygiene(root, "android", sourceContent)));
519
591
  return findings;
520
592
  }
521
- async function validateFlutter(root) {
593
+ async function validateFlutter(root, scanContext) {
522
594
  const findings = [];
523
595
  const pubspec = await readIfExists(path.join(root, "pubspec.yaml"));
524
596
  const pubspecLock = await readIfExists(path.join(root, "pubspec.lock"));
525
- const dartFiles = await findFiles(path.join(root, "lib"), [".dart"], 300);
597
+ const dartFiles = await findFiles(path.join(root, "lib"), [".dart"], 300, scanContext, "flutter.source");
526
598
  const dartContent = await readMany(dartFiles);
527
599
  const setupFiles = filesMatching(dartContent, [/AmityCoreClient\s*\.\s*setup/, /AmityUIKit\d*Manager\s*\.\s*setup/]);
528
600
  const coreLoginFiles = filesMatching(dartContent, [/AmityCoreClient\s*\.\s*login/]);
@@ -643,10 +715,10 @@ async function validateFlutter(root) {
643
715
  findings.push(...(await validateEnvSecretHygiene(root, "flutter", dartContent)));
644
716
  return findings;
645
717
  }
646
- async function validateTypeScript(root, platform) {
718
+ async function validateTypeScript(root, platform, scanContext) {
647
719
  const findings = [];
648
720
  const packageJson = await readIfExists(path.join(root, "package.json"));
649
- const sourceFiles = await findFiles(root, [".ts", ".tsx", ".js", ".jsx"], 500);
721
+ const sourceFiles = await findFiles(root, [".ts", ".tsx", ".js", ".jsx"], 500, scanContext, `${platform}.source`);
650
722
  const sourceContent = await readMany(sourceFiles);
651
723
  const setupFiles = filesMatching(sourceContent, [/Client\s*\.\s*createClient/, /Client\s*\.\s*create\s*\(/, /createClient\s*\(/, /Amity(?:UIKit|UiKit)Provider/]);
652
724
  const rawLoginFiles = filesMatching(sourceContent, [/Client\s*\.\s*login/, /\.login\s*\(/]);
@@ -3252,7 +3324,7 @@ function iosAmitySdkIsFloating(s) {
3252
3324
  return true;
3253
3325
  return false;
3254
3326
  }
3255
- async function validateIos(root) {
3327
+ async function validateIos(root, scanContext) {
3256
3328
  const findings = [];
3257
3329
  const joinExisting = async (names) => [...(await readMany(await existingFiles(root, names))).values()].join("\n");
3258
3330
  const podText = await joinExisting(["Podfile"]);
@@ -3277,7 +3349,7 @@ async function validateIos(root) {
3277
3349
  ];
3278
3350
  const manifestCombined = [podText, pkgSwiftText, projectYmlText, pbxprojText].join("\n");
3279
3351
  const hasManifest = manifestCombined.trim().length > 0;
3280
- const swiftFiles = await findFiles(root, [".swift", ".xcconfig", ".plist"], 400);
3352
+ const swiftFiles = await findFiles(root, [".swift", ".xcconfig", ".plist"], 400, scanContext, "ios.source");
3281
3353
  const swiftContent = await readMany(swiftFiles);
3282
3354
  const setupFiles = filesMatching(swiftContent, [/AmityClient\s*\(/, /AmityClient\s*\.\s*setup/, /AmityUIKit\d*Manager\s*\.\s*setup/]);
3283
3355
  const coreLoginFiles = filesMatching(swiftContent, [/\.login\s*\(/, /client\.login\s*\(/]);
@@ -3603,10 +3675,11 @@ function inferDesignKind(relativePath) {
3603
3675
  }
3604
3676
  return "theme";
3605
3677
  }
3606
- async function findFiles(root, extensions, maxFiles) {
3678
+ async function findFiles(root, extensions, maxFiles, scanContext, scope = "source") {
3607
3679
  const found = [];
3680
+ let truncated = false;
3608
3681
  async function walk(directory) {
3609
- if (found.length >= maxFiles) {
3682
+ if (truncated) {
3610
3683
  return;
3611
3684
  }
3612
3685
  let entries;
@@ -3616,8 +3689,8 @@ async function findFiles(root, extensions, maxFiles) {
3616
3689
  catch {
3617
3690
  return;
3618
3691
  }
3619
- for (const entry of entries) {
3620
- if (found.length >= maxFiles) {
3692
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3693
+ if (truncated) {
3621
3694
  return;
3622
3695
  }
3623
3696
  if (shouldSkipDirectory(entry.name)) {
@@ -3628,11 +3701,25 @@ async function findFiles(root, extensions, maxFiles) {
3628
3701
  await walk(entryPath);
3629
3702
  }
3630
3703
  else if (extensions.some((extension) => entry.name.endsWith(extension))) {
3704
+ if (found.length >= maxFiles) {
3705
+ truncated = true;
3706
+ return;
3707
+ }
3631
3708
  found.push(entryPath);
3632
3709
  }
3633
3710
  }
3634
3711
  }
3635
3712
  await walk(root);
3713
+ if (scanContext) {
3714
+ scanContext.scans.push({
3715
+ scope,
3716
+ root: path.relative(scanContext.validationRoot, root) || ".",
3717
+ extensions: [...extensions],
3718
+ scanned: found.length,
3719
+ limit: maxFiles,
3720
+ truncated,
3721
+ });
3722
+ }
3636
3723
  return found;
3637
3724
  }
3638
3725
  async function readMany(files) {
@@ -4151,7 +4238,7 @@ function validateCustomPostTypeDataTypeDeclared(root, platform, sourceContent) {
4151
4238
  if (!matches)
4152
4239
  continue;
4153
4240
  for (const match of matches) {
4154
- if (/data\s*[:=(]\s*[\{\[]\s*['"]?text['"]?\s*:/i.test(match))
4241
+ if (/data\s*[:=(]\s*[\{\[]\s*['"]?text['"]?\s*(?::|[,}])/i.test(match))
4155
4242
  continue;
4156
4243
  if (!/dataType\s*[:=\(]/i.test(match)) {
4157
4244
  findings.push(finding(ruleId, "warning", `File ${rel} creates a custom data post but does not declare a dataType.`, rel, `When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.`));
@@ -4221,8 +4308,8 @@ function validatePostBodyRendered(root, platform, sourceContent) {
4221
4308
  android: /\b(?:Text\s*\(|TextView|\.text\s*=|setText\s*\()/,
4222
4309
  };
4223
4310
  const postSummaryPat = {
4224
- typescript: /\bpost\s*\??\.\s*(?:postId|commentsCount|reactionsCount)\b|\bpostId\b/,
4225
- "react-native": /\bpost\s*\??\.\s*(?:postId|commentsCount|reactionsCount)\b|\bpostId\b/,
4311
+ typescript: /\bpost\s*\??\.\s*(?:postId|commentsCount|reactionsCount)\b/,
4312
+ "react-native": /\bpost\s*\??\.\s*(?:postId|commentsCount|reactionsCount)\b/,
4226
4313
  flutter: /\b(?:post|_posts\s*\[[^\]]+\])\s*\.\s*(?:postId|commentsCount|reactionsCount)\b/,
4227
4314
  ios: /\bpost\s*\.\s*(?:postId|commentsCount|reactionsCount)\b/,
4228
4315
  android: /\bpost\s*\.\s*(?:postId|commentsCount|reactionsCount|getPostId|getCommentCount|getReactionCount)\b/,
@@ -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) => {