@amityco/social-plus-vise 0.8.1 → 0.12.2
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.
- package/CHANGELOG.md +207 -0
- package/README.md +107 -40
- package/dist/capabilities.js +447 -0
- package/dist/outcomes.js +463 -5
- package/dist/server.js +115 -3
- package/dist/tools/ast.js +25 -0
- package/dist/tools/compliance.js +88 -20
- package/dist/tools/debug.js +267 -0
- package/dist/tools/design.js +1496 -0
- package/dist/tools/docs.js +9 -4
- package/dist/tools/harness.js +17 -1
- package/dist/tools/integration.js +83 -7
- package/dist/tools/project.js +872 -67
- package/dist/tools/sdkVersion.js +129 -0
- package/dist/types.js +4 -0
- package/package.json +27 -6
- package/rules/auth.yaml +298 -38
- package/rules/comments.yaml +0 -72
- package/rules/feed.yaml +1151 -12
- package/rules/live-data.yaml +316 -36
- package/rules/push.yaml +140 -0
- package/rules/sdk-lifecycle.yaml +1428 -138
- package/rules/security.yaml +60 -0
- package/skills/social-plus-vise/SKILL.md +98 -55
- package/skills/social-plus-vise/reference/debugging.md +39 -0
- package/skills/social-plus-vise/reference/operations.md +59 -0
- package/skills/vise-harness-engineer/SKILL.md +35 -0
- package/social.plus-vise.png +0 -0
package/dist/outcomes.js
CHANGED
|
@@ -5,15 +5,18 @@ export function hasAnswer(answers, id) {
|
|
|
5
5
|
const CLASSIFY_ORDER = [
|
|
6
6
|
"setup-push",
|
|
7
7
|
"setup-live-data",
|
|
8
|
+
"add-comments",
|
|
8
9
|
"add-moderation",
|
|
9
10
|
"add-chat",
|
|
10
|
-
"add-comments",
|
|
11
11
|
"add-feed",
|
|
12
|
+
"add-community",
|
|
13
|
+
"add-follow",
|
|
14
|
+
"add-notifications",
|
|
12
15
|
"troubleshoot",
|
|
13
16
|
"validate-setup",
|
|
14
17
|
"setup-sdk",
|
|
15
18
|
];
|
|
16
|
-
const PUSH_PATTERNS = [/\b(push
|
|
19
|
+
const PUSH_PATTERNS = [/\b(push(?:\s+notification)?|push(?:\s+notifications)?|firebase|fcm|apns)\b/];
|
|
17
20
|
const LIVE_PATTERNS = [
|
|
18
21
|
/\b(live object|live objects|live collection|live collections|realtime collection|real-time collection|observe|observer|subscribe|subscription|unsubscribe|live update|live updates)\b/,
|
|
19
22
|
];
|
|
@@ -29,13 +32,48 @@ const MODERATION_OUTCOME_PATTERNS = [
|
|
|
29
32
|
const CHAT_PATTERNS = [
|
|
30
33
|
/\b(chat|messaging|dm|direct message|conversation|group chat|channel|inbox)\b/,
|
|
31
34
|
];
|
|
35
|
+
// Community MANAGEMENT/membership — deliberately NOT bare "community" (which
|
|
36
|
+
// co-occurs with feed: "community feed" must route to add-feed, handled by
|
|
37
|
+
// classify order placing add-community AFTER add-feed).
|
|
38
|
+
const COMMUNITY_PATTERNS = [
|
|
39
|
+
/\b(create|creating|build|building|manage|managing|join|joining|leave|leaving|set ?up)\s+(a\s+|the\s+)?communit/,
|
|
40
|
+
/\bcommunit\w*\s+(member|membership|role|invit|categor|setting|management|directory|discovery)/,
|
|
41
|
+
/\bcommunity\s+(creation|management|members?|roles?|invitations?|categories|settings|moderation)\b/,
|
|
42
|
+
];
|
|
43
|
+
// Social graph (follow/followers). "block user" is intentionally left to
|
|
44
|
+
// add-moderation; blocking is still a capability under add-follow.
|
|
45
|
+
const FOLLOW_PATTERNS = [
|
|
46
|
+
/\b(follow|unfollow|follower|followers|following)\b/,
|
|
47
|
+
/\b(social graph|user relationship|follow request|followers? list|following list)\b/,
|
|
48
|
+
];
|
|
49
|
+
// In-app notification tray/inbox/settings. Deliberately NOT bare "notification"
|
|
50
|
+
// and NOT "push notification" (which routes to setup-push).
|
|
51
|
+
// "notification feed"/"notification inbox" deliberately excluded — "feed" and
|
|
52
|
+
// "inbox" are claimed by add-feed / add-chat respectively. Tray/center/in-app/
|
|
53
|
+
// settings are non-colliding.
|
|
54
|
+
const NOTIFICATION_PATTERNS = [
|
|
55
|
+
/\b(notification tray|notification cent(?:er|re)|in-?app notification)/,
|
|
56
|
+
/\bnotification (?:setting|preference)/,
|
|
57
|
+
];
|
|
32
58
|
const TROUBLESHOOT_PATTERNS = [/\b(error|broken|crash|not working|fail|timeout|401|403)\b/];
|
|
33
59
|
const VALIDATE_PATTERNS = [/\b(validate|check|correct|setup right|initiali[sz])\b/];
|
|
34
|
-
const SETUP_PATTERNS = [/\b(setup|set up|install|integrate|wire|configure)\b/];
|
|
60
|
+
const SETUP_PATTERNS = [/\b(setup|set up|install|integrate|wire|configure|init sdk|sdk setup|session lifecycle)\b|initialise?s?\b/];
|
|
35
61
|
export const BROAD_SOCIAL_REGEX = /\b(nice|social features|social feature|engagement|community experience)\b/i;
|
|
36
62
|
export const DESIGN_REGEX = /\bdesign token|design tokens|theme|same design|design system|brand/i;
|
|
37
63
|
export function classifyOutcome(request) {
|
|
38
64
|
const normalized = request.toLowerCase();
|
|
65
|
+
// Precedence: a feed/timeline build that also mentions comments is a feed
|
|
66
|
+
// build, not a standalone "add comments to existing content" task. add-feed
|
|
67
|
+
// carries conditional comment guidance, so a multi-feature feed request must
|
|
68
|
+
// route there — otherwise add-comments (which precedes add-feed in the
|
|
69
|
+
// classify order) wins on the bare word "comment" and silently drops every
|
|
70
|
+
// feed/avatar/poll/notification step. Pure comment requests (no feed signal)
|
|
71
|
+
// still fall through to add-comments below.
|
|
72
|
+
const matchesFeed = outcomeRegistry["add-feed"].patterns.some((p) => p.test(normalized));
|
|
73
|
+
const matchesComments = outcomeRegistry["add-comments"].patterns.some((p) => p.test(normalized));
|
|
74
|
+
if (matchesFeed && matchesComments) {
|
|
75
|
+
return "add-feed";
|
|
76
|
+
}
|
|
39
77
|
for (const id of CLASSIFY_ORDER) {
|
|
40
78
|
const def = outcomeRegistry[id];
|
|
41
79
|
if (def.patterns.some((pattern) => pattern.test(normalized))) {
|
|
@@ -134,10 +172,18 @@ const setupSdk = {
|
|
|
134
172
|
step: "Initialize the social.plus client exactly once with API key and explicit region.",
|
|
135
173
|
evidence: [platformQuickStart(ctx.platform).path, "requiredInputs.social.plus API key local env/config variable", "requiredInputs.social.plus region"],
|
|
136
174
|
},
|
|
175
|
+
{
|
|
176
|
+
step: "When repairing setup, reuse the app's existing region or endpoint config source instead of hardcoding a guessed default value.",
|
|
177
|
+
evidence: [platformQuickStart(ctx.platform).path, "requiredInputs.social.plus region"],
|
|
178
|
+
},
|
|
137
179
|
{
|
|
138
180
|
step: "Wire login after user identity is known and before social.plus API queries/subscriptions.",
|
|
139
181
|
evidence: ["social-plus-sdk/getting-started/authentication", "requiredInputs.user identity source for login"],
|
|
140
182
|
},
|
|
183
|
+
{
|
|
184
|
+
step: "When adding renewal handling, keep it in the existing login path and retain the handler for the full session lifetime.",
|
|
185
|
+
evidence: ["social-plus-sdk/getting-started/authentication", "requiredInputs.user identity source for login"],
|
|
186
|
+
},
|
|
141
187
|
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
142
188
|
],
|
|
143
189
|
validation: (platform) => [`${platform}.setup.present`, `${platform}.login.present`, `${platform}.region.explicit`],
|
|
@@ -436,6 +482,14 @@ const addFeed = {
|
|
|
436
482
|
required: true,
|
|
437
483
|
blocksImplementationWhenMissing: true,
|
|
438
484
|
});
|
|
485
|
+
questions.push({
|
|
486
|
+
id: "engagement_surfaces",
|
|
487
|
+
question: "Which engagement surfaces are in scope alongside the feed: comments/replies, reactions, notifications, sharing?",
|
|
488
|
+
why: "A feed silently shipped without comments or reactions is the most common scope gap — the request often omits them, so the agent drops them without ever deciding. Naming the engagement surfaces turns a silent omission into an explicit, reviewable choice. This does NOT block a read-only feed: if unspecified, implement the surfaces the design shows and explicitly list any treated as out-of-scope in the final report.",
|
|
489
|
+
required: false,
|
|
490
|
+
blocksImplementationWhenMissing: false,
|
|
491
|
+
options: ["comments + replies", "reactions", "notifications", "sharing", "feed only (no engagement)"],
|
|
492
|
+
});
|
|
439
493
|
return filterAnswered(ctx.answers, questions);
|
|
440
494
|
},
|
|
441
495
|
requiredInputs: () => [
|
|
@@ -470,8 +524,63 @@ const addFeed = {
|
|
|
470
524
|
"social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
|
|
471
525
|
],
|
|
472
526
|
},
|
|
527
|
+
{
|
|
528
|
+
step: "When repairing or refactoring a feed query, preserve existing pagination inputs and state (for example pageToken, nextPage, hasMore/loadMore, or infinite-query wiring) unless the customer explicitly changes feed behavior.",
|
|
529
|
+
evidence: [
|
|
530
|
+
"requiredInputs.feed scope",
|
|
531
|
+
"implementationRules.file-specific edits",
|
|
532
|
+
],
|
|
533
|
+
},
|
|
473
534
|
{ step: "Reuse the host app's existing visual system for the social surface.", evidence: designEvidence },
|
|
474
535
|
{ step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
|
|
536
|
+
{
|
|
537
|
+
step: "In post card renderers, resolve each media type from the parent post OR its childrenPosts — in a feed the parent is usually dataType 'text' and the image/video/poll/room rides on a child post. Handle at minimum: text, image, video, file, poll, room. Do NOT gate media on the parent's dataType alone (e.g. post.dataType === 'poll') — that never matches a text parent and the content silently never renders. When rendering the text body itself (post or comment), apply @mention highlights if metadata carries mention entries ({ type: 'user', index, length, userId }, length excluding the '@'): wrap each [index, index + length + 1] span in a styled element and resolve userId to a display name, rather than printing raw text. Pass mentionees on create so mentioned users are notified.",
|
|
538
|
+
evidence: ["social-plus-sdk/social/posts"],
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
step: "Read SDK objects through their own typed accessors and types (e.g. post.getImageInfo(), the Amity.* types) instead of casting return values to hand-written shapes like (post.data as { text?: string }). A hand-written cast silences the type-checker, so when an SDK upgrade renames a field your build still compiles and the bug only surfaces at runtime — reading through SDK types lets tsc/analyze flag the breakage.",
|
|
542
|
+
evidence: ["social-plus-sdk/social/posts"],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
step: "For community and user avatars, use the SDK-provided URL (community.avatar?.fileUrl / community.avatarImage?.getUrl / community.getAvatar()?.getUrl) and fall back to an initial only when the field is absent. Do not derive an initial from the display name as the sole identifier.",
|
|
546
|
+
evidence: ["social-plus-sdk/social/user-profile"],
|
|
547
|
+
},
|
|
548
|
+
...(COMMENT_PATTERNS.some((p) => p.test(ctx.request.toLowerCase())) ? [
|
|
549
|
+
{
|
|
550
|
+
step: "If post cards display a comment count, implement the comment thread end to end — both read AND write. Read: CommentRepository.getComments live collection with an explicit pageSize. Write: a composer that calls CommentRepository.createComment, gated behind the current user's ban/permission state. A read-only comment list is incomplete; users expect to post. Wire cleanup on unmount.",
|
|
551
|
+
evidence: [
|
|
552
|
+
"social-plus-sdk/social/content-management/comments/creation/text-comment",
|
|
553
|
+
"social-plus-sdk/social/content-management/comments/retrieval/query-comments",
|
|
554
|
+
"social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview",
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
step: "If comments support replies (comment.childrenNumber > 0), render the reply hierarchy: load replies with getComments({ parentId: <commentId> }) and create replies with createComment({ ..., parentId }). The SDK supports up to Level 2 nesting — resolve deeper replies to the Level 1 ancestor via getParentId(). A flat single-level list drops the conversation structure.",
|
|
559
|
+
evidence: ["social-plus-sdk/social/content-management/comments/creation/text-comment"],
|
|
560
|
+
},
|
|
561
|
+
] : []),
|
|
562
|
+
...(/notification|bell|tray|inbox/i.test(ctx.request) ? [
|
|
563
|
+
{
|
|
564
|
+
step: "If the design includes a notification bell or notification center, implement it with the notificationTray SDK: getNotificationTrayItems (live collection), markItemsSeen on open, and an unread badge sourced from getNotificationTraySeen.",
|
|
565
|
+
evidence: ["social-plus-sdk/social/notifications"],
|
|
566
|
+
},
|
|
567
|
+
] : []),
|
|
568
|
+
{
|
|
569
|
+
step: "If the feed includes poll posts, render each answer by branching on answer.dataType: for 'text' use answer.data directly as a string (PollAnswer.data is the text, not an object); for 'image' render answer.image?.fileUrl. Support voting via PollRepository.votePoll(pollId, [answerId]) and PollRepository.unvotePoll(pollId). When poll.status === 'closed' or poll.isVoted is true, display voteCount percentage bars instead of vote buttons. Show the poll's time status from poll.closedAt (when it ended) or poll.closedIn (when it will end) so users know if voting is open.",
|
|
570
|
+
evidence: ["social-plus-sdk/social/content-management/posts/creation/poll-post"],
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
step: "In post card headers, show the post's target context when post.targetType === 'community': display 'Author.displayName › Community.displayName' by subscribing to CommunityRepository.getCommunity(post.targetId, cb) for the live community name.",
|
|
574
|
+
evidence: ["social-plus-sdk/social/communities", "social-plus-sdk/social/posts"],
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
step: "For room-type posts, subscribe to RoomRepository.getRoom(roomId, cb) to get the Room object and display room.title (not room.roomId). Show room.status (live/idle/ended) as a badge.",
|
|
578
|
+
evidence: ["social-plus-sdk/live-video/room"],
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
step: "Give each post card an actions menu gated by the viewer's relationship to the post. If the viewer is the author (post.postedUserId === currentUserId), show edit (PostRepository.editPost) and delete (PostRepository.deletePost). For posts the viewer does not own, show report/flag (PostRepository.flagPost). Author-ownership actions and moderator-role actions are distinct — do not show edit/delete to non-authors.",
|
|
582
|
+
evidence: ["social-plus-sdk/social/posts", "social-plus-sdk/social/moderation"],
|
|
583
|
+
},
|
|
475
584
|
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
476
585
|
];
|
|
477
586
|
},
|
|
@@ -706,6 +815,10 @@ const addModeration = {
|
|
|
706
815
|
},
|
|
707
816
|
{ step: "Implement block/mute state application to rendered content.", evidence: designEvidence },
|
|
708
817
|
{ step: "Add hidden/blocked content rendering per customer policy.", evidence: ["requiredInputs.post-action rendering policy"] },
|
|
818
|
+
{
|
|
819
|
+
step: "If building a moderator/review surface for a review-enabled community (ADMIN_REVIEW_POST_REQUIRED), query the reviewing feed (feedType: 'reviewing', gated on the REVIEW_COMMUNITY_POST permission) and wire approvePost()/declinePost() — not just the published feed. A regular member feed stays on published.",
|
|
820
|
+
evidence: ["social-plus-sdk/social/content-management/posts/moderation/post-review"],
|
|
821
|
+
},
|
|
709
822
|
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
710
823
|
];
|
|
711
824
|
},
|
|
@@ -828,8 +941,8 @@ const addChat = {
|
|
|
828
941
|
evidence: ["social-plus-sdk/chat/channels", "social-plus-sdk/chat/messages"],
|
|
829
942
|
},
|
|
830
943
|
{ step: "Reuse the host app's existing visual system for chat UI.", evidence: designEvidence },
|
|
831
|
-
{ step: "Add message send with error handling and auth gate.", evidence: ["implementationRules.file-specific edits"] },
|
|
832
|
-
{ step: "Wire read receipts and typing indicators if required.", evidence: ["requiredInputs.read receipt and typing indicator requirements"] },
|
|
944
|
+
{ step: "Add message send with error handling and auth gate. Observe each message's syncState and surface failed sends (error state) with a retry/delete affordance instead of rendering optimistic success unconditionally; clean up unrecoverable failures (e.g. deleteFailedMessages) on init.", evidence: ["implementationRules.file-specific edits"] },
|
|
945
|
+
{ step: "Wire read receipts and typing indicators if required — and mark the channel/messages read on open (channel.markAsRead() / message.markRead(), or startMessageReceiptSync paired with stopMessageReceiptSync on the view lifecycle) so the server-side unread count actually decrements. Reading the unread count without ever marking read leaves the badge stuck.", evidence: ["requiredInputs.read receipt and typing indicator requirements"] },
|
|
833
946
|
{ step: "Add moderation affordance on messages.", evidence: ["requiredInputs.moderation flow for chat messages"] },
|
|
834
947
|
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
835
948
|
];
|
|
@@ -858,6 +971,347 @@ const addChat = {
|
|
|
858
971
|
],
|
|
859
972
|
resolveNotes: () => [],
|
|
860
973
|
};
|
|
974
|
+
const addCommunity = {
|
|
975
|
+
id: "add-community",
|
|
976
|
+
patterns: COMMUNITY_PATTERNS,
|
|
977
|
+
interpretation: "Build a community surface — create/join/leave communities, list members and roles, and handle invitations/join-requests — using the customer app's design system.",
|
|
978
|
+
docsQuery: (platform) => `${platform} community membership join leave members`,
|
|
979
|
+
docs: (platform) => [
|
|
980
|
+
{
|
|
981
|
+
path: "social-plus-sdk/social/communities-spaces/organization/join-leave-community",
|
|
982
|
+
reason: "Join/leave and join-request APIs for public vs private communities.",
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
path: "social-plus-sdk/social/communities-spaces/organization/query-community-members",
|
|
986
|
+
reason: "Query/observe community members, membership, and roles.",
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
path: "social-plus-sdk/social/communities-spaces/organization/community-invitation",
|
|
990
|
+
reason: "Community invitation flow (create/accept invitations).",
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
path: liveDataPlatformPath(platform),
|
|
994
|
+
reason: "Live Collection patterns for real-time community and member-list updates.",
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
intakeQuestions: (ctx) => {
|
|
998
|
+
const questions = [
|
|
999
|
+
{
|
|
1000
|
+
id: "community_scope",
|
|
1001
|
+
question: "Which community surface are you building (create a community, browse/join communities, a community detail with members, or membership management)?",
|
|
1002
|
+
why: "Each surface uses different repositories and flows; Vise must not assume.",
|
|
1003
|
+
required: true,
|
|
1004
|
+
blocksImplementationWhenMissing: true,
|
|
1005
|
+
options: ["create community", "browse/join communities", "community detail + members", "membership management"],
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: "community_privacy",
|
|
1009
|
+
question: "Are communities public (instant join) or private (join-request approval)?",
|
|
1010
|
+
why: "Private communities use the join-request approval flow (AmityJoinRequest); public communities join instantly. The UI and API differ.",
|
|
1011
|
+
required: true,
|
|
1012
|
+
blocksImplementationWhenMissing: true,
|
|
1013
|
+
options: ["public", "private (join requests)", "both"],
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
id: "community_target",
|
|
1017
|
+
question: "Where does the community ID come from (route param, selection, or a create flow)?",
|
|
1018
|
+
why: "The coding agent must not invent a communityId; it needs a concrete source.",
|
|
1019
|
+
required: true,
|
|
1020
|
+
blocksImplementationWhenMissing: true,
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
id: "lifecycle_owner",
|
|
1024
|
+
question: "What owns the lifecycle of the live community/member subscription (Activity, ViewController, Widget, React component)?",
|
|
1025
|
+
why: "Live community/member observers must be cleaned up to prevent leaks and stale updates.",
|
|
1026
|
+
required: true,
|
|
1027
|
+
blocksImplementationWhenMissing: true,
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
id: "target_screen",
|
|
1031
|
+
question: "Which screen or component should display the community surface?",
|
|
1032
|
+
why: "Vise needs the target file/route for placement.",
|
|
1033
|
+
required: true,
|
|
1034
|
+
blocksImplementationWhenMissing: true,
|
|
1035
|
+
},
|
|
1036
|
+
];
|
|
1037
|
+
return filterAnswered(ctx.answers, questions);
|
|
1038
|
+
},
|
|
1039
|
+
requiredInputs: () => [
|
|
1040
|
+
"community surface: create, browse/join, detail+members, or membership management",
|
|
1041
|
+
"community privacy model: public (instant) vs private (join request)",
|
|
1042
|
+
"concrete community ID source (route param, selection, or create flow)",
|
|
1043
|
+
"lifecycle owner for community/member observer cleanup",
|
|
1044
|
+
"target screen or component",
|
|
1045
|
+
],
|
|
1046
|
+
implementationRules: () => [
|
|
1047
|
+
"Do not invent or hardcode a communityId. Ask for the source or implement a selection/create flow.",
|
|
1048
|
+
"Do not ask for API keys in chat. Create an env/config placeholder.",
|
|
1049
|
+
"Query communities and members through Live Collections, not one-shot queries, so the UI stays live.",
|
|
1050
|
+
"Handle public (instant join) and private (join-request approval) flows explicitly — do not assume one.",
|
|
1051
|
+
"Gate moderator-only actions (roles, ban, remove) behind a role check; community/member observers must be cleaned up on lifecycle end.",
|
|
1052
|
+
],
|
|
1053
|
+
implementationSteps: (ctx) => {
|
|
1054
|
+
const designEvidence = ctx.designSignals.length > 0
|
|
1055
|
+
? ctx.designSignals.map((signal) => signal.file)
|
|
1056
|
+
: ["requiredInputs.design token or theme source file"];
|
|
1057
|
+
return [
|
|
1058
|
+
{
|
|
1059
|
+
step: "Confirm the community surface, privacy model, community ID source, and target screen before writing code.",
|
|
1060
|
+
evidence: ["requiredInputs.community surface", "requiredInputs.community privacy model", "requiredInputs.concrete community ID source"],
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
step: "Implement the community/member query with the platform's Live Collection (AmityCommunityRepository / getMembers), with cleanup on the lifecycle owner.",
|
|
1064
|
+
evidence: ["social-plus-sdk/social/communities-spaces/organization/query-community-members", liveDataPlatformPath(ctx.platform)],
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
step: "Wire join/leave — handle public (joinCommunity) and private (join-request approval) flows.",
|
|
1068
|
+
evidence: ["social-plus-sdk/social/communities-spaces/organization/join-leave-community"],
|
|
1069
|
+
},
|
|
1070
|
+
{ step: "Reuse the host app's existing visual system for the community UI.", evidence: designEvidence },
|
|
1071
|
+
{ step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
|
|
1072
|
+
{ step: "Add membership management (roles/moderation) and invitations where in scope, gated by role.", evidence: ["social-plus-sdk/social/communities-spaces/organization/community-invitation"] },
|
|
1073
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
1074
|
+
];
|
|
1075
|
+
},
|
|
1076
|
+
validation: () => [
|
|
1077
|
+
"community membership uses a Live Collection (not a one-shot query)",
|
|
1078
|
+
"join/leave handles public vs private (join-request) flows",
|
|
1079
|
+
"no invented communityId",
|
|
1080
|
+
"community/member observer cleaned up on lifecycle end",
|
|
1081
|
+
"validate_setup",
|
|
1082
|
+
"run_sensors",
|
|
1083
|
+
],
|
|
1084
|
+
stopConditions: (ctx) => filterStops(ctx.answers, [
|
|
1085
|
+
{ id: "community_scope", text: "The community surface is unknown; do not assume create vs browse vs members vs management." },
|
|
1086
|
+
{ id: "community_privacy", text: "The privacy model is unknown; public (instant) and private (join-request) flows differ and must not be guessed." },
|
|
1087
|
+
{ id: "community_target", text: "The community ID source is unknown; do not invent a communityId." },
|
|
1088
|
+
{ id: "lifecycle_owner", text: "The lifecycle owner for community/member observer cleanup is unknown." },
|
|
1089
|
+
]),
|
|
1090
|
+
resolvePlan: () => [
|
|
1091
|
+
{ file: "Target screen/component", intent: "Query communities/members via Live Collection; render loading/empty/error/data states." },
|
|
1092
|
+
{ file: "Join/leave flow", intent: "Handle public join and private join-request approval." },
|
|
1093
|
+
{ file: "Membership management", intent: "Roles/moderation/invitations, gated by role." },
|
|
1094
|
+
],
|
|
1095
|
+
resolveVerification: () => [
|
|
1096
|
+
"Confirm communities/members load via a Live Collection and update in real time.",
|
|
1097
|
+
"Confirm join/leave handles both public and private flows.",
|
|
1098
|
+
"Confirm the community/member observer is cleaned up on lifecycle end.",
|
|
1099
|
+
"Confirm no communityId is hardcoded.",
|
|
1100
|
+
],
|
|
1101
|
+
resolveNotes: () => [],
|
|
1102
|
+
};
|
|
1103
|
+
const addFollow = {
|
|
1104
|
+
id: "add-follow",
|
|
1105
|
+
patterns: FOLLOW_PATTERNS,
|
|
1106
|
+
interpretation: "Build the social graph — follow/unfollow, follower/following lists, follow-request approval, and block/unblock — using the customer app's design system.",
|
|
1107
|
+
docsQuery: (platform) => `${platform} follow unfollow followers following block user`,
|
|
1108
|
+
docs: (platform) => [
|
|
1109
|
+
{
|
|
1110
|
+
path: "social-plus-sdk/social/user-relationship/following/get-follower-following-list",
|
|
1111
|
+
reason: "Query/observe follower and following lists; follow/unfollow and follow status.",
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
path: "social-plus-sdk/social/user-relationship/blocking/block-unblock-user",
|
|
1115
|
+
reason: "Block/unblock a user and how blocking affects the relationship.",
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
path: liveDataPlatformPath(platform),
|
|
1119
|
+
reason: "Live Collection patterns for real-time follower/following list updates.",
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
intakeQuestions: (ctx) => {
|
|
1123
|
+
const questions = [
|
|
1124
|
+
{
|
|
1125
|
+
id: "follow_surface",
|
|
1126
|
+
question: "Which social-graph surface (follow/unfollow button, follower list, following list, or blocked-users management)?",
|
|
1127
|
+
why: "Each uses different AmityUserRepository calls and UI; Vise must not assume.",
|
|
1128
|
+
required: true,
|
|
1129
|
+
blocksImplementationWhenMissing: true,
|
|
1130
|
+
options: ["follow/unfollow button", "follower list", "following list", "blocked users"],
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
id: "follow_model",
|
|
1134
|
+
question: "Is following automatic, or does it require a follow-request approval?",
|
|
1135
|
+
why: "Follow-request flows have a pending/accept/decline state (AmityFollowStatusFilter) that changes the API and UI.",
|
|
1136
|
+
required: true,
|
|
1137
|
+
blocksImplementationWhenMissing: true,
|
|
1138
|
+
options: ["automatic", "follow request approval"],
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
id: "follow_target",
|
|
1142
|
+
question: "Where does the target userId come from (route param, profile selection, current user)?",
|
|
1143
|
+
why: "The coding agent must not invent a userId; it needs a concrete source.",
|
|
1144
|
+
required: true,
|
|
1145
|
+
blocksImplementationWhenMissing: true,
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
id: "lifecycle_owner",
|
|
1149
|
+
question: "What owns the lifecycle of the live follower/following subscription?",
|
|
1150
|
+
why: "Live relationship observers must be cleaned up to prevent leaks and stale counts.",
|
|
1151
|
+
required: true,
|
|
1152
|
+
blocksImplementationWhenMissing: true,
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
id: "target_screen",
|
|
1156
|
+
question: "Which screen or component should display the social-graph surface?",
|
|
1157
|
+
why: "Vise needs the target file/route.",
|
|
1158
|
+
required: true,
|
|
1159
|
+
blocksImplementationWhenMissing: true,
|
|
1160
|
+
},
|
|
1161
|
+
];
|
|
1162
|
+
return filterAnswered(ctx.answers, questions);
|
|
1163
|
+
},
|
|
1164
|
+
requiredInputs: () => [
|
|
1165
|
+
"social-graph surface: follow button, follower list, following list, or blocked users",
|
|
1166
|
+
"follow model: automatic vs follow-request approval",
|
|
1167
|
+
"concrete target userId source (route param, selection, current user)",
|
|
1168
|
+
"lifecycle owner for relationship observer cleanup",
|
|
1169
|
+
"target screen or component",
|
|
1170
|
+
],
|
|
1171
|
+
implementationRules: () => [
|
|
1172
|
+
"Do not invent or hardcode a userId. Ask for the source or wire it from the profile/route.",
|
|
1173
|
+
"Do not ask for API keys in chat. Create an env/config placeholder.",
|
|
1174
|
+
"Query follower/following lists through Live Collections so counts and state stay live.",
|
|
1175
|
+
"Handle follow-request (pending/accept/decline) explicitly when following is not automatic.",
|
|
1176
|
+
"Relationship observers must be cleaned up on lifecycle end.",
|
|
1177
|
+
],
|
|
1178
|
+
implementationSteps: (ctx) => {
|
|
1179
|
+
const designEvidence = ctx.designSignals.length > 0 ? ctx.designSignals.map((signal) => signal.file) : ["requiredInputs.design token or theme source file"];
|
|
1180
|
+
return [
|
|
1181
|
+
{
|
|
1182
|
+
step: "Confirm the social-graph surface, follow model, target userId source, and target screen before writing code.",
|
|
1183
|
+
evidence: ["requiredInputs.social-graph surface", "requiredInputs.follow model", "requiredInputs.concrete target userId source"],
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
step: "Implement follow/unfollow (AmityUserRepository) and the follower/following query as a Live Collection, with cleanup.",
|
|
1187
|
+
evidence: ["social-plus-sdk/social/user-relationship/following/get-follower-following-list", liveDataPlatformPath(ctx.platform)],
|
|
1188
|
+
},
|
|
1189
|
+
{ step: "Handle the follow-request approval flow (pending/accept/decline) when following is not automatic.", evidence: ["requiredInputs.follow model"] },
|
|
1190
|
+
{ step: "Wire block/unblock and a blocked-users surface where in scope.", evidence: ["social-plus-sdk/social/user-relationship/blocking/block-unblock-user"] },
|
|
1191
|
+
{ step: "Reuse the host app's existing visual system; implement loading/empty/error/data states.", evidence: designEvidence },
|
|
1192
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
1193
|
+
];
|
|
1194
|
+
},
|
|
1195
|
+
validation: () => [
|
|
1196
|
+
"follower/following lists use a Live Collection (not a one-shot query)",
|
|
1197
|
+
"follow model handled (automatic vs follow-request approval)",
|
|
1198
|
+
"no invented userId",
|
|
1199
|
+
"relationship observer cleaned up on lifecycle end",
|
|
1200
|
+
"validate_setup",
|
|
1201
|
+
"run_sensors",
|
|
1202
|
+
],
|
|
1203
|
+
stopConditions: (ctx) => filterStops(ctx.answers, [
|
|
1204
|
+
{ id: "follow_surface", text: "The social-graph surface is unknown; do not assume follow button vs lists vs blocked users." },
|
|
1205
|
+
{ id: "follow_model", text: "The follow model is unknown; automatic vs follow-request approval flows differ." },
|
|
1206
|
+
{ id: "follow_target", text: "The target userId source is unknown; do not invent a userId." },
|
|
1207
|
+
{ id: "lifecycle_owner", text: "The lifecycle owner for relationship observer cleanup is unknown." },
|
|
1208
|
+
]),
|
|
1209
|
+
resolvePlan: () => [
|
|
1210
|
+
{ file: "Target screen/component", intent: "Follow/unfollow + follower/following lists via Live Collection; loading/empty/error/data states." },
|
|
1211
|
+
{ file: "Follow-request flow", intent: "Pending/accept/decline when following is not automatic." },
|
|
1212
|
+
{ file: "Blocking", intent: "Block/unblock + blocked-users management." },
|
|
1213
|
+
],
|
|
1214
|
+
resolveVerification: () => [
|
|
1215
|
+
"Confirm follower/following lists load via a Live Collection and update live.",
|
|
1216
|
+
"Confirm the follow model (automatic vs request) is handled.",
|
|
1217
|
+
"Confirm the relationship observer is cleaned up on lifecycle end.",
|
|
1218
|
+
"Confirm no userId is hardcoded.",
|
|
1219
|
+
],
|
|
1220
|
+
resolveNotes: () => [],
|
|
1221
|
+
};
|
|
1222
|
+
const addNotifications = {
|
|
1223
|
+
id: "add-notifications",
|
|
1224
|
+
patterns: NOTIFICATION_PATTERNS,
|
|
1225
|
+
interpretation: "Build an in-app notification tray/inbox — observe the notification tray, mark items seen, and respect notification settings — using the customer app's design system. (This is distinct from push-notification setup.)",
|
|
1226
|
+
docsQuery: (platform) => `${platform} in-app notification tray seen settings`,
|
|
1227
|
+
docs: (platform) => [
|
|
1228
|
+
{
|
|
1229
|
+
path: "social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status",
|
|
1230
|
+
reason: "Observe the notification tray and its seen/mark-as-seen status.",
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
path: liveDataPlatformPath(platform),
|
|
1234
|
+
reason: "Live Object/Collection patterns for real-time tray updates and seen state.",
|
|
1235
|
+
},
|
|
1236
|
+
],
|
|
1237
|
+
intakeQuestions: (ctx) => {
|
|
1238
|
+
const questions = [
|
|
1239
|
+
{
|
|
1240
|
+
id: "notification_surface",
|
|
1241
|
+
question: "Which notification surface (in-app tray/inbox list, an unseen badge, or notification settings/preferences)?",
|
|
1242
|
+
why: "The tray list, the seen-badge, and settings use different APIs; Vise must not assume.",
|
|
1243
|
+
required: true,
|
|
1244
|
+
blocksImplementationWhenMissing: true,
|
|
1245
|
+
options: ["tray / inbox list", "unseen badge", "notification settings"],
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
id: "lifecycle_owner",
|
|
1249
|
+
question: "What owns the lifecycle of the live notification-tray subscription?",
|
|
1250
|
+
why: "The tray observer must be cleaned up to prevent leaks and stale badges.",
|
|
1251
|
+
required: true,
|
|
1252
|
+
blocksImplementationWhenMissing: true,
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
id: "target_screen",
|
|
1256
|
+
question: "Which screen or component should display the notifications?",
|
|
1257
|
+
why: "Vise needs the target file/route.",
|
|
1258
|
+
required: true,
|
|
1259
|
+
blocksImplementationWhenMissing: true,
|
|
1260
|
+
},
|
|
1261
|
+
];
|
|
1262
|
+
return filterAnswered(ctx.answers, questions);
|
|
1263
|
+
},
|
|
1264
|
+
requiredInputs: () => [
|
|
1265
|
+
"notification surface: tray/inbox list, unseen badge, or settings",
|
|
1266
|
+
"lifecycle owner for the tray observer",
|
|
1267
|
+
"target screen or component",
|
|
1268
|
+
],
|
|
1269
|
+
implementationRules: () => [
|
|
1270
|
+
"This is the IN-APP notification tray, not push setup (see setup-push for FCM/APNS).",
|
|
1271
|
+
"Do not ask for API keys in chat. Create an env/config placeholder.",
|
|
1272
|
+
"Observe the notification tray as a Live Object/Collection so the seen state and badge stay live.",
|
|
1273
|
+
"Mark items/tray seen so the unseen count clears server-side; clean up the observer on lifecycle end.",
|
|
1274
|
+
],
|
|
1275
|
+
implementationSteps: (ctx) => {
|
|
1276
|
+
const designEvidence = ctx.designSignals.length > 0 ? ctx.designSignals.map((signal) => signal.file) : ["requiredInputs.design token or theme source file"];
|
|
1277
|
+
return [
|
|
1278
|
+
{
|
|
1279
|
+
step: "Confirm the notification surface (tray list / badge / settings) and target screen before writing code.",
|
|
1280
|
+
evidence: ["requiredInputs.notification surface", "requiredInputs.target screen or component"],
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
step: "Observe the notification tray (getNotificationTraySeen / tray Live Object) with cleanup on the lifecycle owner.",
|
|
1284
|
+
evidence: ["social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status", liveDataPlatformPath(ctx.platform)],
|
|
1285
|
+
},
|
|
1286
|
+
{ step: "Mark the tray/items seen so the unseen badge clears server-side (markAsSeen).", evidence: ["social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status"] },
|
|
1287
|
+
{ step: "Respect notification settings/preferences where in scope (getSettings).", evidence: ["requiredInputs.notification surface"] },
|
|
1288
|
+
{ step: "Reuse the host app's existing visual system; implement loading/empty/error/data states.", evidence: designEvidence },
|
|
1289
|
+
{ step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
|
|
1290
|
+
];
|
|
1291
|
+
},
|
|
1292
|
+
validation: () => [
|
|
1293
|
+
"notification tray observed as a Live Object/Collection (not a one-shot query)",
|
|
1294
|
+
"tray/items marked seen so the unseen count clears server-side",
|
|
1295
|
+
"tray observer cleaned up on lifecycle end",
|
|
1296
|
+
"this is in-app tray, not push setup",
|
|
1297
|
+
"validate_setup",
|
|
1298
|
+
"run_sensors",
|
|
1299
|
+
],
|
|
1300
|
+
stopConditions: (ctx) => filterStops(ctx.answers, [
|
|
1301
|
+
{ id: "notification_surface", text: "The notification surface is unknown; do not assume tray vs badge vs settings." },
|
|
1302
|
+
{ id: "lifecycle_owner", text: "The lifecycle owner for the tray observer is unknown." },
|
|
1303
|
+
]),
|
|
1304
|
+
resolvePlan: () => [
|
|
1305
|
+
{ file: "Target screen/component", intent: "Observe the notification tray; render list/badge with loading/empty/error/data states." },
|
|
1306
|
+
{ file: "Seen state", intent: "Mark tray/items seen so the unseen count clears." },
|
|
1307
|
+
],
|
|
1308
|
+
resolveVerification: () => [
|
|
1309
|
+
"Confirm the tray is observed live and updates in real time.",
|
|
1310
|
+
"Confirm items/tray are marked seen and the unseen count clears.",
|
|
1311
|
+
"Confirm the tray observer is cleaned up on lifecycle end.",
|
|
1312
|
+
],
|
|
1313
|
+
resolveNotes: () => [],
|
|
1314
|
+
};
|
|
861
1315
|
const troubleshoot = {
|
|
862
1316
|
id: "troubleshoot",
|
|
863
1317
|
patterns: TROUBLESHOOT_PATTERNS,
|
|
@@ -878,6 +1332,7 @@ const troubleshoot = {
|
|
|
878
1332
|
implementationRules: () => [],
|
|
879
1333
|
implementationSteps: () => [
|
|
880
1334
|
{ step: "Gather more evidence before implementation.", evidence: ["stopConditions", "search_docs", "inspect_project"] },
|
|
1335
|
+
{ step: "If the issue is an SDK-specific runtime symptom, run vise debug first and use the repair brief before broader repo exploration.", evidence: ["search_docs", "inspect_project"] },
|
|
881
1336
|
],
|
|
882
1337
|
validation: () => [],
|
|
883
1338
|
stopConditions: () => [],
|
|
@@ -944,6 +1399,9 @@ const outcomeRegistry = {
|
|
|
944
1399
|
"add-comments": addComments,
|
|
945
1400
|
"add-moderation": addModeration,
|
|
946
1401
|
"add-chat": addChat,
|
|
1402
|
+
"add-community": addCommunity,
|
|
1403
|
+
"add-follow": addFollow,
|
|
1404
|
+
"add-notifications": addNotifications,
|
|
947
1405
|
troubleshoot,
|
|
948
1406
|
"validate-setup": validateSetup,
|
|
949
1407
|
unknown,
|