@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
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-completeness assessment — ADVISORY ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Boundary (see the validation-boundaries principle): completeness is a "this is
|
|
5
|
+
* missing" claim — a universal-negative over open-ended correct implementations.
|
|
6
|
+
* It is structurally false-positive-prone and therefore NEVER a hard gate. This
|
|
7
|
+
* module only surfaces nudges; `vise check`'s status/exit code are untouched.
|
|
8
|
+
*
|
|
9
|
+
* Memory-independence comes from inversion: VISE authors the canonical capability
|
|
10
|
+
* set per outcome; the agent must OPT OUT of a capability with a recorded reason
|
|
11
|
+
* (`// vise: scope-omit <id> <reason>`), which `check` reads and reports. The
|
|
12
|
+
* agent subtracts with justification — it doesn't have to remember the set.
|
|
13
|
+
*
|
|
14
|
+
* Gate-eligibility (not used here — these stay advisory): a capability could only
|
|
15
|
+
* ever graduate to a gate if it has a named SDK-call anchor AND a no-fire fixture.
|
|
16
|
+
* Every capability below is anchored on SDK symbols for exactly that reason, but
|
|
17
|
+
* the output is advisory regardless.
|
|
18
|
+
*/
|
|
19
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
// Canonical, Vise-authored capability set — the SDK feature surface (grounded in
|
|
22
|
+
// rules/*.yaml + SKILL.md, not guessed). Anchored on SDK symbol names (consistent
|
|
23
|
+
// enough across TS/Flutter/Android/iOS to match cross-platform). All advisory.
|
|
24
|
+
export const CAPABILITIES = [
|
|
25
|
+
// ── add-feed: post dataTypes (a feed parent is usually 'text'; rich content
|
|
26
|
+
// rides on child posts — render each type the feed can return) ──────────
|
|
27
|
+
{
|
|
28
|
+
id: "post-image",
|
|
29
|
+
label: "Image posts",
|
|
30
|
+
outcomes: ["add-feed"],
|
|
31
|
+
symbols: [/\bgetImageInfo\b/, /AmityImage/i, /['"]image['"]/, /childrenPosts/i, /getChildren/i],
|
|
32
|
+
hint: "resolve image posts via getImageInfo from the parent or a child post (parent is usually dataType 'text')",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "post-video",
|
|
36
|
+
label: "Video posts",
|
|
37
|
+
outcomes: ["add-feed"],
|
|
38
|
+
// Native idioms differ from TS: Android matches a sealed-class case
|
|
39
|
+
// `AmityPost.Data.VIDEO`, Flutter an enum `AmityDataType.VIDEO`, both expose
|
|
40
|
+
// getVideo()/videoData. Without these the catalog false-reports video as missing
|
|
41
|
+
// on Kotlin/Dart even when the agent handles it (found on Netflix native builds).
|
|
42
|
+
symbols: [/\bgetVideo\w*/, /AmityVideo/i, /['"]video['"]/, /Data(?:Type)?\.VIDEO\b/, /\bvideoData\b/],
|
|
43
|
+
hint: "resolve video posts via getVideo / the VIDEO dataType from the parent or a child post",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "post-file",
|
|
47
|
+
label: "File posts",
|
|
48
|
+
outcomes: ["add-feed"],
|
|
49
|
+
symbols: [/\bgetFileInfo\b/, /AmityFile(?:Info|Data)?/i, /['"]file['"]/],
|
|
50
|
+
hint: "render file posts (getFileInfo); upload via AmityFileRepository to get a fileId, never an external URL",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "post-poll",
|
|
54
|
+
label: "Poll posts",
|
|
55
|
+
outcomes: ["add-feed"],
|
|
56
|
+
symbols: [/\bgetPollInfo\b/, /\bvotePoll\b/, /\bunvotePoll\b/, /AmityPoll/i, /PollRepository/i],
|
|
57
|
+
hint: "resolve polls via getPollInfo; read answer.text + answer.image, support votePoll/unvotePoll, show closedAt",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "post-livestream",
|
|
61
|
+
label: "Livestream / room posts",
|
|
62
|
+
outcomes: ["add-feed"],
|
|
63
|
+
symbols: [/livestream/i, /liveStream/, /getLiveStream/i, /AmityStream/i, /['"](?:liveStream|room)['"]/, /getRoomInfo/i],
|
|
64
|
+
hint: "render livestream/room posts by title/status — never the raw roomId",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "post-custom",
|
|
68
|
+
label: "Custom post types",
|
|
69
|
+
outcomes: ["add-feed"],
|
|
70
|
+
symbols: [/dataType\s*[:=]\s*['"]custom/i, /['"]custom\.[\w-]+['"]/, /customDataType/i],
|
|
71
|
+
hint: "render custom-dataType posts (declare dataType on create so the SDK routes them, not a JSON blob)",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "mentions",
|
|
75
|
+
label: "@mention rendering",
|
|
76
|
+
outcomes: ["add-feed", "add-comments", "add-chat"],
|
|
77
|
+
symbols: [/\bmentionees\b/, /\bmentionedUsers\b/, /metadata.*mention/i, /['"]mention['"]/],
|
|
78
|
+
hint: "wrap @mention spans from metadata offsets and resolve userId→name; pass mentionees on create to notify",
|
|
79
|
+
},
|
|
80
|
+
// ── add-feed: broader social.plus content surfaces (advisory — opt out if a
|
|
81
|
+
// plain post feed; these are distinct social.plus features) ─────────────
|
|
82
|
+
{
|
|
83
|
+
id: "story",
|
|
84
|
+
label: "Stories (ephemeral image/video)",
|
|
85
|
+
outcomes: ["add-feed"],
|
|
86
|
+
symbols: [/AmityStoryRepository/i, /createImageStory/i, /createVideoStory/i, /getActiveStories/i, /AmityStoryTarget/i, /\bAmityStory\b/],
|
|
87
|
+
hint: "stories via AmityStoryRepository (createImageStory/createVideoStory) — render the story tray, support reactions/comments, and mark stories seen for impression analytics",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "event",
|
|
91
|
+
label: "Community events",
|
|
92
|
+
outcomes: ["add-feed"],
|
|
93
|
+
symbols: [/AmityEventRepository/i, /\bgetEvents\b/, /\bcreateEvent\b/, /AmityEvent(?:Type|Status|QueryBuilder)?\b/],
|
|
94
|
+
hint: "community events via AmityEventRepository (getEvents/createEvent) — render an events list/detail",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "livestream-broadcast",
|
|
98
|
+
label: "Livestream rooms / broadcasting",
|
|
99
|
+
outcomes: ["add-feed"],
|
|
100
|
+
symbols: [/AmityRoomRepository/i, /\bcreateRoom\b/, /createRoomAndStartStreaming/i, /getBroadcastData/i, /createLiveStreamPost/i, /\bgoLive\b/i, /startPublish/i],
|
|
101
|
+
hint: "live rooms/broadcasting via AmityRoomRepository (createRoom, go-live, live-viewing); post the stream with createLiveStreamPost (distinct from rendering a livestream post in the feed)",
|
|
102
|
+
},
|
|
103
|
+
// ── add-feed: engagement surfaces ─────────────────────────────────────────
|
|
104
|
+
{
|
|
105
|
+
id: "comments",
|
|
106
|
+
label: "Comment thread (list + composer)",
|
|
107
|
+
outcomes: ["add-feed"],
|
|
108
|
+
symbols: [/\bgetComments\b/, /\bcreateComment\b/, /CommentRepository/i],
|
|
109
|
+
hint: "pair getComments (list) with createComment (composer), gated by the user's ban state",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "reactions",
|
|
113
|
+
label: "Reactions",
|
|
114
|
+
outcomes: ["add-feed", "add-comments"],
|
|
115
|
+
symbols: [/\baddReaction\b/, /\bremoveReaction\b/, /ReactionRepository/i, /\bmyReactions\b/],
|
|
116
|
+
hint: "addReaction/removeReaction with a tenant-configured reaction name (not a hardcoded literal)",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "pagination",
|
|
120
|
+
label: "Pagination / load-more",
|
|
121
|
+
outcomes: ["add-feed", "add-comments", "add-chat"],
|
|
122
|
+
symbols: [/\bloadMore\b/, /\bonNextPage\b/, /\bnextPage\b/, /\bhasNextPage\b/, /\bloadNext\b/],
|
|
123
|
+
hint: "drive the collection's loadMore/nextPage (opaque cursors); never numeric page math",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "composer",
|
|
127
|
+
label: "Post composer (create)",
|
|
128
|
+
outcomes: ["add-feed"],
|
|
129
|
+
symbols: [/\bcreatePost\b/, /PostRepository[\s\S]{0,20}create/i],
|
|
130
|
+
hint: "createPost with a dynamic targetType (not a hardcoded literal)",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "pinned-posts",
|
|
134
|
+
label: "Pinned / announcement posts",
|
|
135
|
+
outcomes: ["add-feed"],
|
|
136
|
+
symbols: [/getPinnedPosts/i, /AmityPinnedPost/i, /\bpinnedPost/i, /\bpinPost\b/i, /\bisPinned\b/i, /AmityPinPlacement/i],
|
|
137
|
+
hint: "surface pinned/announcement posts (getPinnedPosts) above the feed",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "post-impressions",
|
|
141
|
+
label: "Post impressions / reach analytics",
|
|
142
|
+
outcomes: ["add-feed"],
|
|
143
|
+
symbols: [/markAsViewed/i, /markPostAsViewed/i, /\bimpression/i, /\breach\b/i, /AmityViewedType/i],
|
|
144
|
+
hint: "mark posts viewed (markAsViewed) so reach/impression analytics are recorded",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "moderation",
|
|
148
|
+
label: "Moderation affordance (report/flag)",
|
|
149
|
+
outcomes: ["add-feed", "add-comments", "add-chat"],
|
|
150
|
+
symbols: [/\bflagPost\b/, /\bflagComment\b/, /\bflagMessage\b/, /\.report\(\)/, /\bflaggedByMe\b/, /\bisFlagged/i],
|
|
151
|
+
hint: "show report/flag for non-authors; gate moderator-only actions by role",
|
|
152
|
+
},
|
|
153
|
+
// ── add-comments: depth ───────────────────────────────────────────────────
|
|
154
|
+
{
|
|
155
|
+
id: "comment-composer",
|
|
156
|
+
label: "Comment composer (write)",
|
|
157
|
+
outcomes: ["add-comments"],
|
|
158
|
+
symbols: [/\bcreateComment\b/],
|
|
159
|
+
hint: "a comment surface needs a composer (createComment), not just a read-only list",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "replies",
|
|
163
|
+
label: "Comment replies (threads)",
|
|
164
|
+
outcomes: ["add-comments"],
|
|
165
|
+
symbols: [/parentId/i, /\bchildrenNumber\b/],
|
|
166
|
+
hint: "render replies via getComments({ parentId }) when childrenNumber > 0",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "comment-edit-delete",
|
|
170
|
+
label: "Comment edit / delete (author)",
|
|
171
|
+
outcomes: ["add-comments"],
|
|
172
|
+
symbols: [/\bupdateComment\b/, /\beditComment\b/, /\bdeleteComment\b/],
|
|
173
|
+
hint: "show edit (updateComment) / delete (deleteComment) for the author of a comment",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "comment-moderation",
|
|
177
|
+
label: "Comment moderation (flag)",
|
|
178
|
+
outcomes: ["add-comments"],
|
|
179
|
+
symbols: [/\bflagComment\b/, /\.report\(\)/, /\bflaggedByMe\b/],
|
|
180
|
+
hint: "show flag/report on comments for non-authors",
|
|
181
|
+
},
|
|
182
|
+
// ── add-chat: depth ───────────────────────────────────────────────────────
|
|
183
|
+
{
|
|
184
|
+
id: "send-message",
|
|
185
|
+
label: "Send message",
|
|
186
|
+
outcomes: ["add-chat"],
|
|
187
|
+
symbols: [/\bcreateMessage\b/, /\bsendMessage\b/, /MessageRepository/i],
|
|
188
|
+
hint: "createMessage; observe each message's syncState for failed sends",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "message-types",
|
|
192
|
+
label: "Message types (image / video / file / custom)",
|
|
193
|
+
outcomes: ["add-chat"],
|
|
194
|
+
symbols: [/createImageMessage/i, /createVideoMessage/i, /createFileMessage/i, /createCustomMessage/i, /messageType/i, /AmityMessageType/i],
|
|
195
|
+
hint: "support non-text message types (image/video/file/custom), not just text",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "message-sync-state",
|
|
199
|
+
label: "Message delivery state",
|
|
200
|
+
outcomes: ["add-chat"],
|
|
201
|
+
symbols: [/\bsyncState\b/, /\bdeleteFailedMessages\b/],
|
|
202
|
+
hint: "surface failed (error) syncState with a retry/delete affordance",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "message-edit-delete",
|
|
206
|
+
label: "Message edit / delete (author)",
|
|
207
|
+
outcomes: ["add-chat"],
|
|
208
|
+
symbols: [/\bupdateMessage\b/, /\beditMessage\b/, /\bdeleteMessage\b/, /\bsoftDeleteMessage\b/],
|
|
209
|
+
hint: "let the author edit (updateMessage) and delete (softDeleteMessage) their messages",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "read-state",
|
|
213
|
+
label: "Read state / unread count",
|
|
214
|
+
outcomes: ["add-chat"],
|
|
215
|
+
symbols: [/\bmarkRead\b/, /\bmarkAsRead\b/, /startMessageReceiptSync/i, /\bunreadCount\b/],
|
|
216
|
+
hint: "mark channel/messages read so the server's unread count decrements",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "typing-indicator",
|
|
220
|
+
label: "Typing indicator",
|
|
221
|
+
outcomes: ["add-chat"],
|
|
222
|
+
symbols: [/startTyping/i, /stopTyping/i, /\bisTyping\b/, /typingPreview/i],
|
|
223
|
+
hint: "start/stop typing and render others' typing state",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "channel-members",
|
|
227
|
+
label: "Channel membership",
|
|
228
|
+
outcomes: ["add-chat"],
|
|
229
|
+
symbols: [/getMembers/i, /ChannelMember/i, /\bmembership\b/i, /\bgetChannelMembers\b/i],
|
|
230
|
+
hint: "list/observe channel members; handle join/leave",
|
|
231
|
+
},
|
|
232
|
+
// ── add-community ─────────────────────────────────────────────────────────
|
|
233
|
+
{
|
|
234
|
+
id: "community-create",
|
|
235
|
+
label: "Create community",
|
|
236
|
+
outcomes: ["add-community"],
|
|
237
|
+
symbols: [/\bcreateCommunity\b/, /AmityCommunityRepository[\s\S]{0,20}create/i, /AmityCommunityCreateOptions/i],
|
|
238
|
+
hint: "create communities via AmityCommunityRepository (createCommunity) with a chosen privacy model",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "community-join-leave",
|
|
242
|
+
label: "Join / leave community",
|
|
243
|
+
outcomes: ["add-community"],
|
|
244
|
+
symbols: [/\bjoinCommunity\b/, /\bleaveCommunity\b/, /AmityCommunityRepository/i],
|
|
245
|
+
hint: "joinCommunity/leaveCommunity for public communities",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "community-join-requests",
|
|
249
|
+
label: "Join requests (private communities)",
|
|
250
|
+
outcomes: ["add-community"],
|
|
251
|
+
symbols: [/\bjoinRequest/i, /AmityJoinRequest/i, /joinRequestStatus/i],
|
|
252
|
+
hint: "private communities use the join-request approval flow (AmityJoinRequest) — handle pending/approve/decline",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: "community-members",
|
|
256
|
+
label: "Member list",
|
|
257
|
+
outcomes: ["add-community"],
|
|
258
|
+
symbols: [/\bgetMembers\b/, /AmityCommunityMembership/i, /AmityCommunityMembershipFilter/i],
|
|
259
|
+
hint: "list/observe members via getMembers (a Live Collection), with membership filters",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "community-roles",
|
|
263
|
+
label: "Member roles / moderation",
|
|
264
|
+
outcomes: ["add-community"],
|
|
265
|
+
symbols: [/\baddRole/i, /\bremoveRole/i, /\bbanMember\b/i, /\bremoveMember\b/i, /\.roles?\b/],
|
|
266
|
+
hint: "manage moderator roles / ban-remove members, gated by a role check",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "community-invitation",
|
|
270
|
+
label: "Invitations",
|
|
271
|
+
outcomes: ["add-community"],
|
|
272
|
+
symbols: [/createInvitation/i, /AmityInvitation/i, /AmityMembershipAcceptanceType/i],
|
|
273
|
+
hint: "invite members via createInvitation; handle accept/decline (AmityInvitationStatus)",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "community-categories",
|
|
277
|
+
label: "Categories",
|
|
278
|
+
outcomes: ["add-community"],
|
|
279
|
+
symbols: [/AmityCommunityCategory/i, /getCategories/i, /categoryIds?/i],
|
|
280
|
+
hint: "organize communities by category (AmityCommunityCategory)",
|
|
281
|
+
},
|
|
282
|
+
// ── add-moderation: depth ─────────────────────────────────────────────────
|
|
283
|
+
{
|
|
284
|
+
id: "report-flow",
|
|
285
|
+
label: "Report / flag flow",
|
|
286
|
+
outcomes: ["add-moderation"],
|
|
287
|
+
symbols: [/\bflagPost\b/, /\bflagComment\b/, /\bflagMessage\b/, /\.report\(\)/, /\bunflag\b/],
|
|
288
|
+
hint: "flag/unflag with a confirmation affordance",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "ban-mute",
|
|
292
|
+
label: "Ban / mute members",
|
|
293
|
+
outcomes: ["add-moderation"],
|
|
294
|
+
symbols: [/\bbanUser\b/, /\bunbanUser\b/, /\bmuteChannel\b/, /\bmuteMember\b/, /\bglobalBan\b/i],
|
|
295
|
+
hint: "ban/unban or mute members (gated by moderator role)",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "review-queue",
|
|
299
|
+
label: "Post review queue (ADMIN_REVIEW)",
|
|
300
|
+
outcomes: ["add-moderation"],
|
|
301
|
+
symbols: [/\bapprovePost\b/, /\bdeclinePost\b/, /reviewing/i, /REVIEW_COMMUNITY_POST/i],
|
|
302
|
+
hint: "for a moderator surface with review enabled, query feedType 'reviewing' and wire approve/decline (gated on REVIEW_COMMUNITY_POST)",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: "hidden-content",
|
|
306
|
+
label: "Hidden / flagged content rendering",
|
|
307
|
+
outcomes: ["add-moderation"],
|
|
308
|
+
symbols: [/isDeleted/i, /\bisFlagged/i, /hasFlaggedComment/i, /blockedContent/i, /\bhidden\b/i],
|
|
309
|
+
hint: "render a placeholder for deleted/flagged content rather than hiding it silently",
|
|
310
|
+
},
|
|
311
|
+
// ── add-follow (social graph) ─────────────────────────────────────────────
|
|
312
|
+
{
|
|
313
|
+
id: "follow-unfollow",
|
|
314
|
+
label: "Follow / unfollow",
|
|
315
|
+
outcomes: ["add-follow"],
|
|
316
|
+
symbols: [/\bfollow\b/i, /\bunfollow\b/i, /AmityUserRepository/i],
|
|
317
|
+
hint: "follow/unfollow via AmityUserRepository; reflect the live follow state on the button",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "followers-following",
|
|
321
|
+
label: "Follower / following lists",
|
|
322
|
+
outcomes: ["add-follow"],
|
|
323
|
+
symbols: [/getFollowers/i, /getFollowings/i, /getFollowerList/i, /followRelationship/i],
|
|
324
|
+
hint: "list/observe followers and following as a Live Collection (getFollowers/getFollowings)",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "follow-status",
|
|
328
|
+
label: "Follow-request status",
|
|
329
|
+
outcomes: ["add-follow"],
|
|
330
|
+
symbols: [/AmityFollowStatusFilter/i, /followStatus/i, /\bpending\b/i, /acceptFollow/i, /declineFollow/i],
|
|
331
|
+
hint: "handle the follow-request pending/accept/decline flow when following is not automatic",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: "block-unblock",
|
|
335
|
+
label: "Block / unblock user",
|
|
336
|
+
outcomes: ["add-follow"],
|
|
337
|
+
symbols: [/\bblockUser\b/i, /\bunblockUser\b/i, /\bunBlockUser\b/i],
|
|
338
|
+
hint: "block/unblock a user (blockUser/unblockUser)",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "blocked-users",
|
|
342
|
+
label: "Blocked-users list",
|
|
343
|
+
outcomes: ["add-follow"],
|
|
344
|
+
symbols: [/getBlockedUsers/i, /blockedUsers/i],
|
|
345
|
+
hint: "surface a managed blocked-users list (getBlockedUsers)",
|
|
346
|
+
},
|
|
347
|
+
// ── add-notifications (in-app tray) ───────────────────────────────────────
|
|
348
|
+
{
|
|
349
|
+
id: "notification-tray",
|
|
350
|
+
label: "Notification tray / inbox",
|
|
351
|
+
outcomes: ["add-notifications"],
|
|
352
|
+
symbols: [/notificationTray/i, /getNotificationTraySeen/i, /NotificationTrayManager/i, /NotificationTray/],
|
|
353
|
+
hint: "observe the notification tray (getNotificationTraySeen / tray Live Object) and render the inbox",
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "notification-seen",
|
|
357
|
+
label: "Mark tray seen (unseen badge)",
|
|
358
|
+
outcomes: ["add-notifications"],
|
|
359
|
+
symbols: [/markAsSeen/i, /\bmarkSeen\b/i, /\bisSeen\b/i, /unseenCount/i],
|
|
360
|
+
hint: "mark the tray/items seen (markAsSeen) so the unseen badge clears server-side",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "notification-settings",
|
|
364
|
+
label: "Notification settings / preferences",
|
|
365
|
+
outcomes: ["add-notifications"],
|
|
366
|
+
symbols: [/getSettings/i, /notificationSettings?/i, /notificationPreference/i, /notifications\(\)/],
|
|
367
|
+
hint: "respect Amity's server-side notification settings/preferences (getSettings)",
|
|
368
|
+
},
|
|
369
|
+
];
|
|
370
|
+
const ADVISORY_NOTE = "Advisory completeness — NEVER fails `vise check`. Build each missing capability, or opt out with a recorded reason: `// vise: scope-omit <id> <reason>`.";
|
|
371
|
+
/** The Vise-authored capability checklist for an outcome (for `vise plan` feed-forward). */
|
|
372
|
+
export function capabilityChecklist(outcome) {
|
|
373
|
+
return CAPABILITIES.filter((c) => c.outcomes.includes(outcome)).map((c) => ({ id: c.id, label: c.label, hint: c.hint }));
|
|
374
|
+
}
|
|
375
|
+
/** Pure assessment over already-read source text. */
|
|
376
|
+
export function assessCompleteness(source, outcome) {
|
|
377
|
+
const caps = CAPABILITIES.filter((c) => c.outcomes.includes(outcome));
|
|
378
|
+
const optOuts = new Map();
|
|
379
|
+
const omitPattern = /vise:\s*scope-omit\s+([a-z][\w-]*)\s*(?:[—:|-]+\s*(.*))?/gi;
|
|
380
|
+
let match;
|
|
381
|
+
while ((match = omitPattern.exec(source)) !== null) {
|
|
382
|
+
optOuts.set(match[1].toLowerCase(), (match[2] ?? "").trim() || "no reason given");
|
|
383
|
+
}
|
|
384
|
+
const present = [];
|
|
385
|
+
const missing = [];
|
|
386
|
+
const optedOut = [];
|
|
387
|
+
for (const cap of caps) {
|
|
388
|
+
if (optOuts.has(cap.id)) {
|
|
389
|
+
optedOut.push({ id: cap.id, reason: optOuts.get(cap.id) });
|
|
390
|
+
}
|
|
391
|
+
else if (cap.symbols.some((symbol) => symbol.test(source))) {
|
|
392
|
+
present.push({ id: cap.id, label: cap.label });
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
missing.push({ id: cap.id, label: cap.label, hint: cap.hint });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { outcome, present, missing, optedOut, note: ADVISORY_NOTE };
|
|
399
|
+
}
|
|
400
|
+
// ── Bounded source read (advisory; perf-bounded like the design check scan) ──
|
|
401
|
+
const SCAN_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".dart", ".kt", ".java", ".swift", ".vue"]);
|
|
402
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", "vendor", ".dart_tool", "pods", "macos", "windows", "linux"]);
|
|
403
|
+
const MAX_FILES = 1500;
|
|
404
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
405
|
+
export async function assessProjectCompleteness(root, outcome) {
|
|
406
|
+
if (CAPABILITIES.every((c) => !c.outcomes.includes(outcome))) {
|
|
407
|
+
return null; // outcome has no completeness checklist
|
|
408
|
+
}
|
|
409
|
+
const resolved = path.resolve(root);
|
|
410
|
+
const parts = [];
|
|
411
|
+
const stack = [resolved];
|
|
412
|
+
let count = 0;
|
|
413
|
+
while (stack.length > 0 && count < MAX_FILES) {
|
|
414
|
+
const dir = stack.pop();
|
|
415
|
+
let entries;
|
|
416
|
+
try {
|
|
417
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
if (count >= MAX_FILES) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
const full = path.join(dir, entry.name);
|
|
427
|
+
if (entry.isDirectory()) {
|
|
428
|
+
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".") && entry.name !== "sp-vise") {
|
|
429
|
+
stack.push(full);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (entry.isFile() && SCAN_EXTS.has(path.extname(entry.name).toLowerCase())) {
|
|
433
|
+
count += 1;
|
|
434
|
+
try {
|
|
435
|
+
const info = await stat(full);
|
|
436
|
+
if (info.size <= MAX_FILE_BYTES) {
|
|
437
|
+
parts.push(await readFile(full, "utf8"));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// skip unreadable
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return assessCompleteness(parts.join("\n"), outcome);
|
|
447
|
+
}
|