@amityco/social-plus-vise 0.14.20 → 0.14.22

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 CHANGED
@@ -4,6 +4,27 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.14.22 — 2026-06-05
8
+
9
+ ### Added
10
+ - **Multi-surface social workplans:** broad social requests now include a `socialWorkplan` sequence that decomposes feed, comments, chat, and profile work into ordered per-surface plans with focused `vise plan` / `vise init` commands.
11
+
12
+ ### Changed
13
+ - **Broad social intake:** when a multi-surface workplan exists, Vise no longer blocks the top-level plan on `feature_surface`; focused per-surface runs still use `--answer feature_surface=<surface>` before implementation.
14
+
15
+ ### Verified
16
+ - Product-flow, CLI, fixture, improvement-discovery, and host-agent smoke coverage now lock the multi-surface workplan path while preserving answered single-surface init behavior.
17
+
18
+ ## 0.14.21 — 2026-06-05
19
+
20
+ ### Changed
21
+ - **Core surface product expectations:** feed, comment tray, chat inbox, and profile plans now surface shared product expectations for post-type scope, composer type scope, live comment lists, comment creation, comment UI states, live channel lists, channel-list sorting, profile identity, and profile avatars.
22
+ - **Plan-time scope questions:** add-feed plans now ask for feed post-type and composer scope; add-comments asks for comment tray scope; add-chat asks for inbox unread/sorting scope; add-follow asks for profile identity scope.
23
+ - **Broad social routing clarity:** `feature_surface` now lists `comments` as a first-class option alongside feed, profile, community, chat, and notifications.
24
+
25
+ ### Verified
26
+ - Targeted product-flow and capability coverage now locks the feed-forward path for the new shared expectations across TypeScript, React Native, Android, Flutter, and iOS SDK surfaces.
27
+
7
28
  ## 0.14.20 — 2026-06-05
8
29
 
9
30
  ### Changed
package/README.md CHANGED
@@ -161,14 +161,15 @@ Aggregate: **98/99 expected feed capabilities** and **27/27 selected optional ca
161
161
 
162
162
  ### Current Release Validation
163
163
 
164
- Version 0.14.20 carries current release proof around the full feed-forward, product-expectation, and validation flow:
164
+ Version 0.14.22 carries current release proof around the full feed-forward, product-expectation, and validation flow:
165
165
 
166
166
  | Surface | What was validated |
167
167
  |---|---|
168
168
  | **Product flow** | Local end-to-end smoke covers design extraction, plan feed-forward, blocking intake, answered init, capability check, design conformance, and sensor discovery. |
169
- | **Plan questions** | Plans surface blocking questions such as `feature_surface` and `design_contract_confirmation`, plus optional choices such as `feed_optional_capabilities`. |
169
+ | **Multi-surface planning** | Broad social requests are decomposed into a `socialWorkplan` sequence for feed, comments, chat, and profile work instead of forcing a single top-level surface choice. Each surface includes focused `vise plan` / `vise init` commands for the host agent to run when implementing that slice. |
170
+ | **Plan questions** | Plans surface blocking questions such as `design_contract_confirmation`, product-scope questions such as `feed_post_type_scope`, `feed_composer_type_scope`, `comment_tray_scope`, `chat_inbox_scope`, and `profile_identity_scope`, plus optional choices such as `feed_optional_capabilities`. Focused plans still accept `feature_surface` answers when the agent is ready to implement one surface. |
170
171
  | **Capability-to-sensor flow** | Vise checks platform support, matches the prompt to available capabilities, offers supported features as questions, records answers, and turns selected answers into sensors in `vise check`. |
171
- | **Shared product expectations** | Public IDs such as `feed.target-resolved`, `comments.thread-read-write`, `chat.channel-target-resolved`, `community.avatar-from-sdk`, `community.display-name-from-sdk`, `moderation.role-gated-action`, `follow.relationship-live`, `profile.social-counts`, and `notifications.tray-live` stay platform-agnostic while check results retain concrete `contractRuleId` and `validator.sensorId` evidence when deterministic sensors exist. |
172
+ | **Shared product expectations** | Public IDs such as `feed.target-resolved`, `feed.post-type-scope-explicit`, `comments.creation-affordance`, `chat.channel-list-order-explicit`, `community.avatar-from-sdk`, `moderation.role-gated-action`, `follow.relationship-live`, `profile.identity-from-sdk`, `profile.social-counts`, and `notifications.tray-live` stay platform-agnostic while check results retain concrete `contractRuleId` and `validator.sensorId` evidence when deterministic sensors exist. |
172
173
  | **Rule detection** | TP-track dashboard detects **321/321 seeded rule gaps (100.0%)** in the static corpus. |
173
174
  | **Packed-package smoke** | Packed-package and host-agent smokes exercise the release tarball path, surfaced plan questions, selected optional capability sensors, rejected design confirmation handling, and exact contract-rule evidence for shared product expectations. |
174
175
 
@@ -569,6 +569,50 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
569
569
  deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
570
570
  hint: "implement the selected rich composer paths or record an explicit text-only/rich-post scope decision",
571
571
  },
572
+ {
573
+ id: "feed.post-type-scope-explicit",
574
+ label: "Feed post-type scope",
575
+ outcomes: ["add-feed"],
576
+ kind: "shared-expectation",
577
+ availability: [
578
+ {
579
+ label: "SDK rich post models/accessors",
580
+ symbols: [
581
+ /\b(?:ContentDataImage|ImageData|AmityImagePost\w*|ImagePost\w*)\b/i,
582
+ /\b(?:ContentDataVideo|VideoData|AmityVideoPost\w*|VideoPost\w*)\b/i,
583
+ /\b(?:ContentDataFile|FileData|AmityFilePost\w*|FilePost\w*)\b/i,
584
+ /\b(?:ContentDataPoll|PollData|AmityPollPost\w*|PollPost\w*)\b/i,
585
+ /\b(?:ContentDataClip|AmityClipPost\w*|ClipPost\w*)\b/i,
586
+ /\b(?:ContentDataRoom|LiveStreamData|AmityRoomPost\w*|RoomPost\w*|LiveStreamPost\w*)\b/i,
587
+ ],
588
+ },
589
+ ],
590
+ deterministicPlatforms: [],
591
+ hint: "before building, name the SDK-available post types the feed will render: image, video, file, poll, clip, room/livestream, custom, or an explicit text-only scope",
592
+ },
593
+ {
594
+ id: "feed.composer-type-scope-explicit",
595
+ label: "Post composer type scope",
596
+ outcomes: ["add-feed"],
597
+ kind: "shared-expectation",
598
+ availability: [
599
+ {
600
+ label: "SDK rich post creation",
601
+ symbols: [
602
+ /\bcreateImagePost\b/i,
603
+ /\bcreateVideoPost\b/i,
604
+ /\bcreateFilePost\b/i,
605
+ /\bcreatePollPost\b/i,
606
+ /\bcreateClipPost\b/i,
607
+ /\bcreateRoomPost\b/i,
608
+ /\bcreateMixedAttachmentPost\b/i,
609
+ /\bcreatePost\b/i,
610
+ ],
611
+ },
612
+ ],
613
+ deterministicPlatforms: [],
614
+ hint: "before building a composer, name which SDK-available creation paths are in scope and which are intentionally excluded",
615
+ },
572
616
  {
573
617
  id: "comments.target-resolved",
574
618
  label: "Resolved comment target",
@@ -595,6 +639,48 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
595
639
  deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
596
640
  hint: "if comments are shown, pair the list with a composer and loading/error/empty states unless the surface is explicitly read-only",
597
641
  },
642
+ {
643
+ id: "comments.list-live",
644
+ label: "Live comment list",
645
+ outcomes: ["add-feed", "add-comments"],
646
+ kind: "shared-expectation",
647
+ availability: [
648
+ {
649
+ label: "SDK comment query/live APIs",
650
+ symbols: [/\bgetComments\b/i, /\bqueryComments\b/i, /\bCommentRepository\b/i, /\bLiveCollection\b/i, /\bPagingData\b/i],
651
+ },
652
+ ],
653
+ deterministicPlatforms: [],
654
+ hint: "render a live comment list from the parent entity, not a static empty tray or placeholder",
655
+ },
656
+ {
657
+ id: "comments.creation-affordance",
658
+ label: "Comment creation affordance",
659
+ outcomes: ["add-feed", "add-comments"],
660
+ kind: "shared-expectation",
661
+ availability: [
662
+ {
663
+ label: "SDK comment creation",
664
+ symbols: [/\bcreateComment\b/i, /\bCommentCreate\b/i, /\bAmityCommentCreateOptions\b/i],
665
+ },
666
+ ],
667
+ deterministicPlatforms: [],
668
+ hint: "when comments are in scope, include a composer that calls the SDK createComment path, or explicitly record read-only scope",
669
+ },
670
+ {
671
+ id: "comments.ui-states-present",
672
+ label: "Comment loading/empty/error states",
673
+ outcomes: ["add-feed", "add-comments"],
674
+ kind: "shared-expectation",
675
+ availability: [
676
+ {
677
+ label: "SDK comment query/live APIs",
678
+ symbols: [/\bgetComments\b/i, /\bqueryComments\b/i, /\bCommentRepository\b/i, /\bLiveCollection\b/i, /\bPagingData\b/i],
679
+ },
680
+ ],
681
+ deterministicPlatforms: [],
682
+ hint: "comment trays should distinguish loading, empty, error, and data states before showing an empty list",
683
+ },
598
684
  {
599
685
  id: "chat.channel-target-resolved",
600
686
  label: "Resolved chat channel",
@@ -651,6 +737,34 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
651
737
  deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
652
738
  hint: "match the SDK channel type to the selected chat shape: conversation for direct messages, community/group for shared channels, broadcast where applicable",
653
739
  },
740
+ {
741
+ id: "chat.channel-list-live",
742
+ label: "Live chat channel list",
743
+ outcomes: ["add-chat"],
744
+ kind: "shared-expectation",
745
+ availability: [
746
+ {
747
+ label: "SDK channel query/live APIs",
748
+ symbols: [/\bChannelRepository\b/i, /\bgetChannels\b/i, /\bqueryChannels\b/i, /\bAmityChannel\b/i, /\bLiveCollection\b/i, /\bPagingData\b/i],
749
+ },
750
+ ],
751
+ deterministicPlatforms: [],
752
+ hint: "build the inbox from SDK channel queries or live collections, not static rows or a fixed channel",
753
+ },
754
+ {
755
+ id: "chat.channel-list-order-explicit",
756
+ label: "Explicit chat channel-list order",
757
+ outcomes: ["add-chat"],
758
+ kind: "shared-expectation",
759
+ availability: [
760
+ {
761
+ label: "SDK channel sort/order",
762
+ symbols: [/\bChannelSort\b/i, /\bAmityChannelSortOption\b/i, /\bsortByLastActivity\b/i, /\bsortBy\b/i, /\blastActivity\b/i, /\bsortingOrder\b/i],
763
+ },
764
+ ],
765
+ deterministicPlatforms: [],
766
+ hint: "declare inbox/channel-list sorting, usually last activity descending, so the UI cannot reverse recency by default",
767
+ },
654
768
  {
655
769
  id: "chat.unread-visible",
656
770
  label: "Chat unread counts",
@@ -814,6 +928,34 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
814
928
  deterministicPlatforms: [],
815
929
  hint: "make automatic follow vs pending approval explicit, including accept/decline states where the SDK exposes them",
816
930
  },
931
+ {
932
+ id: "profile.identity-from-sdk",
933
+ label: "Profile identity from SDK",
934
+ outcomes: ["add-follow"],
935
+ kind: "shared-expectation",
936
+ availability: [
937
+ {
938
+ label: "SDK user identity APIs",
939
+ symbols: [/\bUserRepository\b/i, /\bgetUser\b/i, /\bgetUsers\b/i, /\bAmityUser\b/i, /\bdisplayName\b/i, /\buserId\b/i],
940
+ },
941
+ ],
942
+ deterministicPlatforms: [],
943
+ hint: "source profile name, id, avatar, and relationship state from the SDK user/profile object instead of placeholders",
944
+ },
945
+ {
946
+ id: "profile.avatar-from-sdk",
947
+ label: "Profile avatar from SDK",
948
+ outcomes: ["add-follow"],
949
+ kind: "shared-expectation",
950
+ availability: [
951
+ {
952
+ label: "SDK user avatar fields/accessors",
953
+ symbols: [/\bavatar\b/i, /\bavatarImage\b/i, /\bavatarUrl\b/i, /\bavatarFileId\b/i, /\bgetAvatar\b/i, /\bfileUrl\b/i, /\bfileURL\b/i],
954
+ },
955
+ ],
956
+ deterministicPlatforms: [],
957
+ hint: "render the SDK user avatar when present and fall back to initials only when the SDK has no avatar field",
958
+ },
817
959
  {
818
960
  id: "profile.social-counts",
819
961
  label: "Profile follower/following counts",
package/dist/outcomes.js CHANGED
@@ -492,7 +492,7 @@ const addFeed = {
492
492
  why: "The request is broad, so the coding agent needs one product surface before it can choose SDK docs, UI states, and target files.",
493
493
  required: true,
494
494
  blocksImplementationWhenMissing: true,
495
- options: ["feed", "profile", "community", "chat", "notifications"],
495
+ options: ["feed", "comments", "profile", "community", "chat", "notifications"],
496
496
  });
497
497
  }
498
498
  questions.push({
@@ -526,12 +526,28 @@ const addFeed = {
526
526
  blocksImplementationWhenMissing: false,
527
527
  options: ["comments + replies", "reactions", "notifications", "sharing", "feed only (no engagement)"],
528
528
  });
529
+ questions.push({
530
+ id: "feed_post_type_scope",
531
+ question: "Which SDK-available post types should the feed render?",
532
+ why: "A feed that renders only text can look complete in a smoke test while image, video, poll, clip, room, and custom posts are blank. The agent should either render the available post types or record an explicit text-only/product subset decision.",
533
+ required: false,
534
+ blocksImplementationWhenMissing: false,
535
+ options: ["all SDK-available post types", "text only", "text + image + video", "text + image + video + poll", "custom subset"],
536
+ });
537
+ questions.push({
538
+ id: "feed_composer_type_scope",
539
+ question: "Which SDK-available post creation types should the composer expose?",
540
+ why: "Post creation often ships as text-only while the feed can render richer content. The agent should align composer scope with the product decision and SDK availability.",
541
+ required: false,
542
+ blocksImplementationWhenMissing: false,
543
+ options: ["all SDK-available creation types", "text only", "text + media", "text + media + poll", "custom subset"],
544
+ });
529
545
  return filterAnswered(ctx.answers, questions);
530
546
  },
531
547
  requiredInputs: () => [
532
548
  "live data shape: single object or collection",
533
549
  "lifecycle owner for subscribe/unsubscribe cleanup",
534
- "feature choice: feed, profile, community, chat, notifications, or another social surface",
550
+ "feature choice: feed, comments, profile, community, chat, notifications, or another social surface",
535
551
  "feed scope: global, user, or community",
536
552
  "concrete feed target for reads and post creation: existing communityId, selected user target, current user target, or app-defined feed target",
537
553
  "target screen or route for the feed UI",
@@ -586,6 +602,7 @@ const addFeed = {
586
602
  evidence: [
587
603
  "capabilityAvailability.available",
588
604
  "optionalCapabilities.choices",
605
+ "intake.feed_composer_type_scope",
589
606
  "social-plus-sdk/social/content-management/posts/creation/image-post",
590
607
  "social-plus-sdk/core-concepts/file-handling/upload",
591
608
  ],
@@ -594,7 +611,7 @@ const addFeed = {
594
611
  { step: "Implement loading, empty, error, and data states.", evidence: ["implementationRules.file-specific edits"] },
595
612
  {
596
613
  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/clip/room rides on a child post. Handle at minimum: text, image, video, file, poll, clip, room. Do not render placeholder labels like '[Image post]' or '[Poll post]'; read the SDK data/accessors and render the actual content. 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.",
597
- evidence: ["social-plus-sdk/social/posts"],
614
+ evidence: ["social-plus-sdk/social/posts", "intake.feed_post_type_scope", "capabilityAvailability.available"],
598
615
  },
599
616
  {
600
617
  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.",
@@ -662,6 +679,11 @@ const addFeed = {
662
679
  "unread.server-synced",
663
680
  "feed.rich-post-rendering",
664
681
  "feed.rich-post-composer-scope",
682
+ "feed.post-type-scope-explicit",
683
+ "feed.composer-type-scope-explicit",
684
+ "comments.list-live",
685
+ "comments.creation-affordance",
686
+ "comments.ui-states-present",
665
687
  "comments.thread-read-write",
666
688
  "profile.social-counts",
667
689
  ],
@@ -750,6 +772,14 @@ const addComments = {
750
772
  required: true,
751
773
  blocksImplementationWhenMissing: true,
752
774
  },
775
+ {
776
+ id: "comment_tray_scope",
777
+ question: "Should the comment tray include the live list, composer, and loading/error/empty states?",
778
+ why: "A tray that only opens, only shows placeholders, or only reads comments misses the expected comment workflow. If any part is intentionally omitted, the agent should record that product decision.",
779
+ required: false,
780
+ blocksImplementationWhenMissing: false,
781
+ options: ["list + composer + states", "read-only list", "composer elsewhere", "custom subset"],
782
+ },
753
783
  ];
754
784
  return filterAnswered(ctx.answers, questions);
755
785
  },
@@ -784,8 +814,8 @@ const addComments = {
784
814
  evidence: [commentDocPath(ctx.platform), liveDataPlatformPath(ctx.platform)],
785
815
  },
786
816
  { step: "Reuse the host app's existing visual system for the comment UI.", evidence: designEvidence },
787
- { step: "Implement loading, empty, error, and data states for comments.", evidence: ["implementationRules.file-specific edits"] },
788
- { step: "Add comment creation (composer) with auth gate and error handling.", evidence: ["requiredInputs.concrete parent entity ID"] },
817
+ { step: "Implement the live comment list plus loading, empty, error, and data states before showing an empty tray.", evidence: ["implementationRules.file-specific edits", "intake.comment_tray_scope"] },
818
+ { step: "Add comment creation (composer) with auth gate and error handling unless the product explicitly chose a read-only tray.", evidence: ["requiredInputs.concrete parent entity ID", "intake.comment_tray_scope"] },
789
819
  { step: "Wire moderation affordance (report/hide/delete) on each comment.", evidence: ["implementationRules.file-specific edits"] },
790
820
  { step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
791
821
  ];
@@ -794,6 +824,9 @@ const addComments = {
794
824
  "comment target resolved",
795
825
  "no invented postId/commentId",
796
826
  "comments.target-resolved",
827
+ "comments.list-live",
828
+ "comments.creation-affordance",
829
+ "comments.ui-states-present",
797
830
  "comments.thread-read-write",
798
831
  "moderation.affordance-present",
799
832
  "pagination.cursor-opaque",
@@ -988,6 +1021,14 @@ const addChat = {
988
1021
  required: true,
989
1022
  blocksImplementationWhenMissing: false,
990
1023
  },
1024
+ {
1025
+ id: "chat_inbox_scope",
1026
+ question: "Should the chat inbox show live channels with unread badges and explicit recency sorting?",
1027
+ why: "A message thread can work while the inbox silently drops unread state or reverses channel recency. The agent should confirm the channel-list behavior before building.",
1028
+ required: false,
1029
+ blocksImplementationWhenMissing: false,
1030
+ options: ["live channels + unread + last activity sort", "thread only", "no unread badges", "custom sort"],
1031
+ },
991
1032
  ];
992
1033
  return filterAnswered(ctx.answers, questions);
993
1034
  },
@@ -1019,7 +1060,8 @@ const addChat = {
1019
1060
  },
1020
1061
  { step: "Reuse the host app's existing visual system for chat UI.", evidence: designEvidence },
1021
1062
  { 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"] },
1022
- { step: "Render unread counts/badges from the SDK on channel rows or the chat tab (`channel.getUnreadCount()`, `getSubChannelsUnreadCount()`, or `AmityCoreClient.observeUserUnread()` / `getTotalChannelUnread()`), and declare message order explicitly with `AmityMessageQuerySortOption.FIRST_CREATED` or `LAST_CREATED` so the UI cannot invert the thread by relying on defaults.", evidence: ["social-plus-sdk/chat/channels", "social-plus-sdk/chat/messages"] },
1063
+ { step: "Render the chat inbox from SDK channel queries/live collections. Show unread counts/badges from the SDK on channel rows or the chat tab (`channel.getUnreadCount()`, `getSubChannelsUnreadCount()`, or `AmityCoreClient.observeUserUnread()` / `getTotalChannelUnread()`), and declare channel-list sorting explicitly (typically last activity descending) so recency cannot be reversed by UI defaults.", evidence: ["social-plus-sdk/chat/channels", "intake.chat_inbox_scope", "capabilityAvailability.available"] },
1064
+ { step: "Declare message order explicitly with `AmityMessageQuerySortOption.FIRST_CREATED` or `LAST_CREATED` so the message thread cannot invert by relying on defaults.", evidence: ["social-plus-sdk/chat/messages"] },
1023
1065
  { 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"] },
1024
1066
  { step: "Add moderation affordance on messages.", evidence: ["requiredInputs.moderation flow for chat messages"] },
1025
1067
  { step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
@@ -1030,6 +1072,8 @@ const addChat = {
1030
1072
  "chat.message-observer-cleanup",
1031
1073
  "chat.send-error-handling",
1032
1074
  "chat.channel-shape-matched",
1075
+ "chat.channel-list-live",
1076
+ "chat.channel-list-order-explicit",
1033
1077
  "moderation.affordance-present",
1034
1078
  "unread.server-synced",
1035
1079
  "pagination.cursor-opaque",
@@ -1243,6 +1287,14 @@ const addFollow = {
1243
1287
  required: true,
1244
1288
  blocksImplementationWhenMissing: true,
1245
1289
  },
1290
+ {
1291
+ id: "profile_identity_scope",
1292
+ question: "Should profile screens show SDK-backed identity, avatar, follower/following counts, and relationship state?",
1293
+ why: "Profile screens often keep placeholder counts or initials while follow state works elsewhere. The agent should confirm which profile fields are in scope and source them from the SDK.",
1294
+ required: false,
1295
+ blocksImplementationWhenMissing: false,
1296
+ options: ["identity + avatar + counts + relationship", "counts only", "relationship only", "custom subset"],
1297
+ },
1246
1298
  ];
1247
1299
  return filterAnswered(ctx.answers, questions);
1248
1300
  },
@@ -1272,6 +1324,7 @@ const addFollow = {
1272
1324
  evidence: ["social-plus-sdk/social/user-relationship/following/get-follower-following-list", liveDataPlatformPath(ctx.platform)],
1273
1325
  },
1274
1326
  { step: "Handle the follow-request approval flow (pending/accept/decline) when following is not automatic.", evidence: ["requiredInputs.follow model"] },
1327
+ { step: "Populate profile identity from the SDK user/profile object: displayName/userId, SDK avatar URL with initials only as fallback, follower/following counts, and relationship state. Do not ship placeholder count labels or generated initials as the only avatar source.", evidence: ["social-plus-sdk/social/user-profile", "intake.profile_identity_scope", "capabilityAvailability.available"] },
1275
1328
  { step: "Wire block/unblock and a blocked-users surface where in scope.", evidence: ["social-plus-sdk/social/user-relationship/blocking/block-unblock-user"] },
1276
1329
  { step: "Reuse the host app's existing visual system; implement loading/empty/error/data states.", evidence: designEvidence },
1277
1330
  { step: "Run validate_setup and detected command sensors after edits.", evidence: ["validate_setup", "run_sensors"] },
@@ -1281,6 +1334,8 @@ const addFollow = {
1281
1334
  "follow.target-resolved",
1282
1335
  "follow.relationship-live",
1283
1336
  "follow.model-explicit",
1337
+ "profile.identity-from-sdk",
1338
+ "profile.avatar-from-sdk",
1284
1339
  "profile.social-counts",
1285
1340
  "validate_setup",
1286
1341
  "run_sensors",
@@ -8,12 +8,19 @@ export const PRODUCT_EXPECTATION_TITLES = {
8
8
  "unread.server-synced": "Unread counts use the server-synced stream",
9
9
  "feed.rich-post-rendering": "Feed renders rich post types",
10
10
  "feed.rich-post-composer-scope": "Feed composer surfaces rich post scope",
11
+ "feed.post-type-scope-explicit": "Feed post-type scope is explicit",
12
+ "feed.composer-type-scope-explicit": "Post composer type scope is explicit",
11
13
  "comments.target-resolved": "Comment target comes from the parent entity",
12
14
  "comments.thread-read-write": "Comment threads support reading and creation",
15
+ "comments.list-live": "Comment tray lists comments live",
16
+ "comments.creation-affordance": "Comment tray includes comment creation",
17
+ "comments.ui-states-present": "Comment tray renders loading, empty, and error states",
13
18
  "chat.channel-target-resolved": "Chat channel comes from app state",
14
19
  "chat.message-observer-cleanup": "Chat message observers clean up on lifecycle end",
15
20
  "chat.send-error-handling": "Chat send failures are handled",
16
21
  "chat.channel-shape-matched": "Chat channel type matches the requested shape",
22
+ "chat.channel-list-live": "Chat channel list stays live",
23
+ "chat.channel-list-order-explicit": "Chat channel list sorting is explicit",
17
24
  "chat.unread-visible": "Chat unread counts are visible",
18
25
  "chat.message-order-explicit": "Chat message order is explicit",
19
26
  "community.target-resolved": "Community target comes from app state",
@@ -25,6 +32,8 @@ export const PRODUCT_EXPECTATION_TITLES = {
25
32
  "follow.target-resolved": "Follow target comes from app state",
26
33
  "follow.relationship-live": "Follow relationships stay live",
27
34
  "follow.model-explicit": "Follow approval model is explicit",
35
+ "profile.identity-from-sdk": "Profile identity comes from the SDK",
36
+ "profile.avatar-from-sdk": "Profile avatar comes from the SDK",
28
37
  "profile.social-counts": "Profile social counts come from the SDK",
29
38
  "notifications.tray-live": "Notification tray is observed live",
30
39
  "notifications.mark-seen": "Notification seen state clears server-side",
@@ -82,7 +82,21 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
82
82
  const designReview = designReviewGuidance(repoRoot, designContract, answers);
83
83
  const acceptedDesignContract = designReview.status === "accepted" ? designContract : null;
84
84
  const designBrief = acceptedDesignContract ? buildDesignBrief(acceptedDesignContract) : undefined;
85
- const intake = intakeFor(ctx, definition.intakeQuestions(ctx), outcome, designBrief, capabilityAvailability, designReview);
85
+ const socialWorkplan = await socialWorkplanFor({
86
+ request,
87
+ answers,
88
+ repoPath: repoRoot,
89
+ platform,
90
+ platforms: inspection.platforms,
91
+ designSignals: inspection.designSignals,
92
+ designBrief,
93
+ designReview,
94
+ sensors,
95
+ });
96
+ const outcomeQuestions = socialWorkplan
97
+ ? definition.intakeQuestions(ctx).filter((question) => question.id !== "feature_surface")
98
+ : definition.intakeQuestions(ctx);
99
+ const intake = intakeFor(ctx, outcomeQuestions, outcome, designBrief, capabilityAvailability, designReview);
86
100
  // Advisory SDK-version currency guidance (npm registry for TS/RN; version-agnostic
87
101
  // for native). Best-effort — degrades to greenfield "install latest + pin" if the
88
102
  // registry is unreachable. Never gates.
@@ -108,6 +122,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
108
122
  platform,
109
123
  supportLevel,
110
124
  intent: intentFor(request, definition.interpretation),
125
+ socialWorkplan,
111
126
  capabilityAvailability,
112
127
  designReview,
113
128
  decisionsRequired,
@@ -123,7 +138,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
123
138
  availableSurfaces: inspection.surfaces,
124
139
  applicableRules: await applicableCompliancePlanRuleSummaries(outcome, inspection.platforms),
125
140
  sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
126
- stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath),
141
+ stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath, Boolean(socialWorkplan)),
127
142
  evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
128
143
  designContract: acceptedDesignContract ? designContractGuidance(acceptedDesignContract) : undefined,
129
144
  completenessChecklist: completenessChecklistFor(outcome),
@@ -162,6 +177,113 @@ function optionalCapabilitiesFor(outcome, answers, request, availability) {
162
177
  selected: selectedOptionalCapabilityIds(outcome, answers, request, availableIds),
163
178
  };
164
179
  }
180
+ const SOCIAL_SURFACE_SEQUENCE = [
181
+ {
182
+ id: "feed",
183
+ outcome: "add-feed",
184
+ label: "Feed and post creation",
185
+ aliases: [/\b(feed|timeline|news feed|post list|posts?|create post|post creation|compose post|composer|reactions?)\b/i],
186
+ defaultForGenericBroad: true,
187
+ },
188
+ {
189
+ id: "comments",
190
+ outcome: "add-comments",
191
+ label: "Comments and replies",
192
+ aliases: [/\b(comments?|replies|reply|discussion thread|comment tray|comment composer)\b/i],
193
+ defaultForGenericBroad: true,
194
+ },
195
+ {
196
+ id: "chat",
197
+ outcome: "add-chat",
198
+ label: "Chat inbox and thread",
199
+ aliases: [/\b(chat|messaging|dm|direct message|conversation|group chat|channel|inbox)\b/i],
200
+ defaultForGenericBroad: true,
201
+ },
202
+ {
203
+ id: "profile",
204
+ outcome: "add-follow",
205
+ label: "Profile and follow graph",
206
+ aliases: [/\b(profile|profiles|follow|unfollow|followers?|following|social graph|relationship)\b/i],
207
+ defaultForGenericBroad: true,
208
+ },
209
+ {
210
+ id: "community",
211
+ outcome: "add-community",
212
+ label: "Community management",
213
+ aliases: [/\b(create|manage|join|leave|membership|members?|roles?|invitations?|categories?)\s+(?:a\s+|the\s+)?communit/i, /\bcommunity\s+(creation|management|members?|roles?|invitations?|categories|settings|moderation)\b/i],
214
+ },
215
+ {
216
+ id: "notifications",
217
+ outcome: "add-notifications",
218
+ label: "In-app notifications",
219
+ aliases: [/\b(notification tray|notification cent(?:er|re)|in-?app notifications?|notification settings?|notification preferences?)\b/i],
220
+ },
221
+ ];
222
+ async function socialWorkplanFor(args) {
223
+ if (!BROAD_SOCIAL_REGEX.test(args.request) || hasAnswer(args.answers, "feature_surface")) {
224
+ return undefined;
225
+ }
226
+ const matched = SOCIAL_SURFACE_SEQUENCE.filter((surface) => surface.aliases.some((pattern) => pattern.test(args.request)));
227
+ const selected = matched.length > 0
228
+ ? matched
229
+ : SOCIAL_SURFACE_SEQUENCE.filter((surface) => surface.defaultForGenericBroad);
230
+ if (selected.length <= 1) {
231
+ return undefined;
232
+ }
233
+ const sequence = [];
234
+ for (const surface of selected) {
235
+ const definition = getOutcomeDefinition(surface.outcome);
236
+ const capabilityAvailability = await platformCapabilityAvailability(surface.outcome, args.platform);
237
+ const surfaceAnswers = { ...args.answers, feature_surface: surface.id };
238
+ const surfaceCtx = {
239
+ ...planContextFor({
240
+ request: args.request,
241
+ outcome: surface.outcome,
242
+ platform: args.platform,
243
+ platforms: args.platforms,
244
+ designSignals: args.designSignals,
245
+ answers: surfaceAnswers,
246
+ }),
247
+ broadSocialRequest: false,
248
+ };
249
+ const intake = intakeFor(surfaceCtx, definition.intakeQuestions(surfaceCtx), surface.outcome, args.designBrief, capabilityAvailability, args.designReview);
250
+ const matchedPrompt = matched.some((match) => match.id === surface.id);
251
+ const commonArgs = `. --request ${shellQuote(args.request)} --answer feature_surface=${surface.id}`;
252
+ sequence.push({
253
+ id: surface.id,
254
+ order: sequence.length + 1,
255
+ outcome: surface.outcome,
256
+ label: surface.label,
257
+ matchedPrompt,
258
+ reason: matchedPrompt
259
+ ? `The request explicitly mentions ${surface.label.toLowerCase()}.`
260
+ : "The request is generic broad social work, so Vise includes this core surface in the default sequence.",
261
+ planCommand: `vise plan ${commonArgs}`,
262
+ initCommand: `vise init ${commonArgs}`,
263
+ intake: {
264
+ status: intake.status,
265
+ questions: intake.questions,
266
+ remainingBlocking: intake.remainingBlocking,
267
+ },
268
+ capabilityAvailability,
269
+ validation: ["validate_setup", "run_sensors", ...definition.validation(args.platform)],
270
+ docs: definition.docs(args.platform).filter((doc) => doc.path !== "unknown"),
271
+ optionalCapabilities: optionalCapabilitiesFor(surface.outcome, surfaceAnswers, args.request, capabilityAvailability),
272
+ sensors: args.sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
273
+ });
274
+ }
275
+ const status = sequence.some((surface) => surface.intake.remainingBlocking > 0) ? "needs-answers" : "ready";
276
+ return {
277
+ kind: "social-multi-surface",
278
+ status,
279
+ note: "This broad social request is decomposed into ordered per-surface plans. Initialize and check one surface at a time so each sidecar has one outcome, while using this sequence as the coordinated product workplan.",
280
+ sequence,
281
+ nextStep: "Resolve each surface's blocking questions, then run the listed plan/init command for that surface before implementation. After each surface build, run `vise check .`, `vise sync .`, `vise validate .`, and `vise run-sensors .`.",
282
+ };
283
+ }
284
+ function shellQuote(value) {
285
+ return `'${value.replace(/'/g, "'\\''")}'`;
286
+ }
165
287
  function designReviewGuidance(repoRoot, contract, answers) {
166
288
  if (!contract) {
167
289
  return {
@@ -371,7 +493,7 @@ function composeImplementationRules(ctx, outcomeRules) {
371
493
  }
372
494
  return rules;
373
495
  }
374
- function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath) {
496
+ function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath, hasSocialWorkplan = false) {
375
497
  const stops = [
376
498
  "A required secret is missing and no safe ignored local env file or non-secret template path is clear.",
377
499
  "The target file is ambiguous or missing and no safe conventional location is detected.",
@@ -387,7 +509,7 @@ function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath) {
387
509
  stops.push(`Multiple app surfaces detected (${surfaces.map((surface) => surface.path).join(", ")}); call this tool again with surfacePath set to the target app surface.`);
388
510
  }
389
511
  stops.push(...outcomeStops);
390
- if (ctx.broadSocialRequest && !hasAnswer(ctx.answers, "feature_surface")) {
512
+ if (ctx.broadSocialRequest && !hasAnswer(ctx.answers, "feature_surface") && !hasSocialWorkplan) {
391
513
  stops.push("The requested social feature is too broad; confirm the first feature surface before implementing.");
392
514
  }
393
515
  if (ctx.mentionsDesign && ctx.designSignals.length === 0 && !hasAnswer(ctx.answers, "design_source")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "0.14.20",
3
+ "version": "0.14.22",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",