@amityco/social-plus-vise 0.14.10 → 0.14.11
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 +10 -0
- package/dist/capabilities.js +7 -5
- package/dist/outcomes.js +5 -8
- package/dist/productExpectations.js +90 -0
- package/dist/tools/project.js +120 -39
- package/package.json +1 -1
- package/rules/chat.yaml +292 -0
- package/rules/feed.yaml +292 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.11 — 2026-06-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Cross-platform product validators:** rich post composer scope, chat unread visibility, chat message ordering, and profile follower/following counts now have deterministic sensors beyond Android where the bundled SDK surface supports the capability.
|
|
11
|
+
- **Shared expectation bindings:** TypeScript, React Native, Flutter, and iOS product gaps now report shared public expectation IDs such as `feed.rich-post-composer-scope`, `comments.thread-read-write`, `chat.unread-visible`, and `profile.social-counts` while retaining platform sensor evidence.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Availability-aware chat gating:** TypeScript and React Native unread support is now recognized from `ChannelUnread` SDK facts; message-order validation remains direct `validate_setup` only there until the SDK surface proves a message-specific sort API.
|
|
15
|
+
- **Plan validation IDs:** add-feed and add-chat plans use shared product expectation IDs across platforms instead of Android-only public validation IDs.
|
|
16
|
+
|
|
7
17
|
## 0.14.10 — 2026-06-05
|
|
8
18
|
|
|
9
19
|
### Changed
|
package/dist/capabilities.js
CHANGED
|
@@ -468,7 +468,7 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
468
468
|
],
|
|
469
469
|
},
|
|
470
470
|
],
|
|
471
|
-
deterministicPlatforms: ["android"],
|
|
471
|
+
deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
|
|
472
472
|
hint: "implement the selected rich composer paths or record an explicit text-only/rich-post scope decision",
|
|
473
473
|
},
|
|
474
474
|
{
|
|
@@ -480,7 +480,7 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
480
480
|
{ label: "SDK comment query", symbols: [/\bgetComments\b/i, /\bqueryComments\b/i] },
|
|
481
481
|
{ label: "SDK comment creation", symbols: [/\bcreateComment\b/i] },
|
|
482
482
|
],
|
|
483
|
-
deterministicPlatforms: ["android"],
|
|
483
|
+
deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
|
|
484
484
|
hint: "if comments are shown, pair the list with a composer and loading/error/empty states unless the surface is explicitly read-only",
|
|
485
485
|
},
|
|
486
486
|
{
|
|
@@ -493,6 +493,8 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
493
493
|
label: "SDK unread state",
|
|
494
494
|
symbols: [
|
|
495
495
|
/\bunreadCount\b/i,
|
|
496
|
+
/\bChannelUnread\b/i,
|
|
497
|
+
/\bChannelUnreadInfo\b/i,
|
|
496
498
|
/\bgetUnread\w*\b/i,
|
|
497
499
|
/\bgetSubChannelsUnreadCount\b/i,
|
|
498
500
|
/\bobserveUserUnread\b/i,
|
|
@@ -501,7 +503,7 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
501
503
|
],
|
|
502
504
|
},
|
|
503
505
|
],
|
|
504
|
-
deterministicPlatforms: ["android"],
|
|
506
|
+
deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
|
|
505
507
|
hint: "render channel-row or global unread state from the SDK, or explicitly choose an unread-free chat surface",
|
|
506
508
|
},
|
|
507
509
|
{
|
|
@@ -515,7 +517,7 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
515
517
|
symbols: [/\bsortBy\b/i, /\bSortOption\b/i, /\bMessageQuerySort\b/i, /\bAmityMessageQuerySortOption\b/i],
|
|
516
518
|
},
|
|
517
519
|
],
|
|
518
|
-
deterministicPlatforms: ["android"],
|
|
520
|
+
deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
|
|
519
521
|
hint: "declare first-created/newest-created order in the SDK query or a clearly named UI sort so the thread cannot be reversed by defaults",
|
|
520
522
|
},
|
|
521
523
|
{
|
|
@@ -536,7 +538,7 @@ export const SHARED_PRODUCT_EXPECTATIONS = [
|
|
|
536
538
|
],
|
|
537
539
|
},
|
|
538
540
|
],
|
|
539
|
-
deterministicPlatforms: ["android"],
|
|
541
|
+
deterministicPlatforms: ["android", "flutter", "ios", "typescript"],
|
|
540
542
|
hint: "if follower/following labels are rendered, source the counts or lists from the SDK instead of placeholders",
|
|
541
543
|
},
|
|
542
544
|
];
|
package/dist/outcomes.js
CHANGED
|
@@ -655,13 +655,9 @@ const addFeed = {
|
|
|
655
655
|
"no invented communityId/targetId/feedId",
|
|
656
656
|
`${platform}.feed.target.literal`,
|
|
657
657
|
"feed.rich-post-rendering",
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
"comments.thread-read-write",
|
|
662
|
-
"profile.social-counts",
|
|
663
|
-
]
|
|
664
|
-
: []),
|
|
658
|
+
"feed.rich-post-composer-scope",
|
|
659
|
+
"comments.thread-read-write",
|
|
660
|
+
"profile.social-counts",
|
|
665
661
|
],
|
|
666
662
|
stopConditions: (ctx) => filterStops(ctx.answers, [
|
|
667
663
|
{ id: "feed_target", text: "The concrete feed target is unknown; do not invent communityId, targetId, feedId, or global feed assumptions." },
|
|
@@ -1027,7 +1023,8 @@ const addChat = {
|
|
|
1027
1023
|
`${platform}.chat.message-observer-cleanup`,
|
|
1028
1024
|
`${platform}.chat.send-error-handling`,
|
|
1029
1025
|
`${platform}.chat.moderation-affordance-present`,
|
|
1030
|
-
|
|
1026
|
+
"chat.unread-visible",
|
|
1027
|
+
...(platform === "android" || platform === "flutter" || platform === "ios" ? ["chat.message-order-explicit"] : []),
|
|
1031
1028
|
],
|
|
1032
1029
|
stopConditions: (ctx) => filterStops(ctx.answers, [
|
|
1033
1030
|
{ id: "chat_shape", text: "The chat shape is unknown; cannot implement without knowing 1:1, group, or community channel." },
|
|
@@ -24,31 +24,121 @@ export const PRODUCT_EXPECTATION_BINDINGS = [
|
|
|
24
24
|
sensorId: "ios.feed.post-datatype-handled",
|
|
25
25
|
platform: "ios",
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
expectationId: "feed.rich-post-composer-scope",
|
|
29
|
+
sensorId: "typescript.feed.rich-post-composer-surfaced",
|
|
30
|
+
platform: "typescript",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
expectationId: "feed.rich-post-composer-scope",
|
|
34
|
+
sensorId: "react-native.feed.rich-post-composer-surfaced",
|
|
35
|
+
platform: "react-native",
|
|
36
|
+
},
|
|
27
37
|
{
|
|
28
38
|
expectationId: "feed.rich-post-composer-scope",
|
|
29
39
|
sensorId: "android.feed.rich-post-composer-surfaced",
|
|
30
40
|
platform: "android",
|
|
31
41
|
},
|
|
42
|
+
{
|
|
43
|
+
expectationId: "feed.rich-post-composer-scope",
|
|
44
|
+
sensorId: "flutter.feed.rich-post-composer-surfaced",
|
|
45
|
+
platform: "flutter",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
expectationId: "feed.rich-post-composer-scope",
|
|
49
|
+
sensorId: "ios.feed.rich-post-composer-surfaced",
|
|
50
|
+
platform: "ios",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
expectationId: "comments.thread-read-write",
|
|
54
|
+
sensorId: "typescript.comments.creation-affordance-present",
|
|
55
|
+
platform: "typescript",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
expectationId: "comments.thread-read-write",
|
|
59
|
+
sensorId: "react-native.comments.creation-affordance-present",
|
|
60
|
+
platform: "react-native",
|
|
61
|
+
},
|
|
32
62
|
{
|
|
33
63
|
expectationId: "comments.thread-read-write",
|
|
34
64
|
sensorId: "android.comments.thread-ui-states-present",
|
|
35
65
|
platform: "android",
|
|
36
66
|
},
|
|
67
|
+
{
|
|
68
|
+
expectationId: "comments.thread-read-write",
|
|
69
|
+
sensorId: "flutter.comments.creation-affordance-present",
|
|
70
|
+
platform: "flutter",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
expectationId: "comments.thread-read-write",
|
|
74
|
+
sensorId: "ios.comments.creation-affordance-present",
|
|
75
|
+
platform: "ios",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
expectationId: "chat.unread-visible",
|
|
79
|
+
sensorId: "typescript.chat.unread-visible",
|
|
80
|
+
platform: "typescript",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
expectationId: "chat.unread-visible",
|
|
84
|
+
sensorId: "react-native.chat.unread-visible",
|
|
85
|
+
platform: "react-native",
|
|
86
|
+
},
|
|
37
87
|
{
|
|
38
88
|
expectationId: "chat.unread-visible",
|
|
39
89
|
sensorId: "android.chat.unread-visible",
|
|
40
90
|
platform: "android",
|
|
41
91
|
},
|
|
92
|
+
{
|
|
93
|
+
expectationId: "chat.unread-visible",
|
|
94
|
+
sensorId: "flutter.chat.unread-visible",
|
|
95
|
+
platform: "flutter",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
expectationId: "chat.unread-visible",
|
|
99
|
+
sensorId: "ios.chat.unread-visible",
|
|
100
|
+
platform: "ios",
|
|
101
|
+
},
|
|
42
102
|
{
|
|
43
103
|
expectationId: "chat.message-order-explicit",
|
|
44
104
|
sensorId: "android.chat.sort-explicit",
|
|
45
105
|
platform: "android",
|
|
46
106
|
},
|
|
107
|
+
{
|
|
108
|
+
expectationId: "chat.message-order-explicit",
|
|
109
|
+
sensorId: "flutter.chat.sort-explicit",
|
|
110
|
+
platform: "flutter",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
expectationId: "chat.message-order-explicit",
|
|
114
|
+
sensorId: "ios.chat.sort-explicit",
|
|
115
|
+
platform: "ios",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
expectationId: "profile.social-counts",
|
|
119
|
+
sensorId: "typescript.profile.social-counts-from-sdk",
|
|
120
|
+
platform: "typescript",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
expectationId: "profile.social-counts",
|
|
124
|
+
sensorId: "react-native.profile.social-counts-from-sdk",
|
|
125
|
+
platform: "react-native",
|
|
126
|
+
},
|
|
47
127
|
{
|
|
48
128
|
expectationId: "profile.social-counts",
|
|
49
129
|
sensorId: "android.profile.social-counts-from-sdk",
|
|
50
130
|
platform: "android",
|
|
51
131
|
},
|
|
132
|
+
{
|
|
133
|
+
expectationId: "profile.social-counts",
|
|
134
|
+
sensorId: "flutter.profile.social-counts-from-sdk",
|
|
135
|
+
platform: "flutter",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
expectationId: "profile.social-counts",
|
|
139
|
+
sensorId: "ios.profile.social-counts-from-sdk",
|
|
140
|
+
platform: "ios",
|
|
141
|
+
},
|
|
52
142
|
];
|
|
53
143
|
const bindingsBySensorId = new Map(PRODUCT_EXPECTATION_BINDINGS.map((binding) => [binding.sensorId, binding]));
|
|
54
144
|
export function productExpectationBindingForSensor(sensorId) {
|
package/dist/tools/project.js
CHANGED
|
@@ -277,7 +277,7 @@ async function validateAndroid(root) {
|
|
|
277
277
|
findings.push(...validatePostsStatusFilter(root, "android", sourceContent));
|
|
278
278
|
findings.push(...validatePaginationCursorOpaque(root, "android", sourceContent));
|
|
279
279
|
findings.push(...validatePostsParentChild(root, "android", sourceContent));
|
|
280
|
-
findings.push(...
|
|
280
|
+
findings.push(...validateRichPostComposerScope(root, "android", sourceContent));
|
|
281
281
|
findings.push(...validateFeedTargetTypeExplicit(root, "android", sourceContent));
|
|
282
282
|
findings.push(...validateChannelTypeMatchesShape(root, "android", sourceContent));
|
|
283
283
|
findings.push(...validateReactionConfiguredNameUsed(root, "android", sourceContent));
|
|
@@ -291,9 +291,9 @@ async function validateAndroid(root) {
|
|
|
291
291
|
findings.push(...validateImagePostChildResolutionAwaited(root, "android", sourceContent));
|
|
292
292
|
findings.push(...validateCommentReferenceTypeEnum(root, "android", sourceContent));
|
|
293
293
|
findings.push(...validateSessionHandlerRetention(root, "android", sourceContent));
|
|
294
|
-
findings.push(...
|
|
295
|
-
findings.push(...
|
|
296
|
-
findings.push(...
|
|
294
|
+
findings.push(...validateChatUnreadVisible(root, "android", sourceContent));
|
|
295
|
+
findings.push(...validateChatMessageOrderExplicit(root, "android", sourceContent));
|
|
296
|
+
findings.push(...validateProfileSocialCounts(root, "android", sourceContent));
|
|
297
297
|
findings.push(...validateChat(root, "android", sourceContent));
|
|
298
298
|
findings.push(...(await validateDesignReuse(root, "android", sourceContent)));
|
|
299
299
|
findings.push(...(await validateEnvSecretHygiene(root, "android", sourceContent)));
|
|
@@ -371,6 +371,7 @@ async function validateFlutter(root) {
|
|
|
371
371
|
findings.push(...validatePollAnswerDataShape(root, "flutter", dartContent));
|
|
372
372
|
findings.push(...validateCommentsQueryHasLimit(root, "flutter", dartContent));
|
|
373
373
|
findings.push(...validateCommentCreationAffordance(root, "flutter", dartContent));
|
|
374
|
+
findings.push(...validateRichPostComposerScope(root, "flutter", dartContent));
|
|
374
375
|
findings.push(...validateCommunityDisplayNameFromSdk(root, "flutter", dartContent));
|
|
375
376
|
findings.push(...validateRoomPostFetched(root, "flutter", dartContent));
|
|
376
377
|
findings.push(...validatePostsStatusFilter(root, "flutter", dartContent));
|
|
@@ -389,6 +390,9 @@ async function validateFlutter(root) {
|
|
|
389
390
|
findings.push(...validateImagePostChildResolutionAwaited(root, "flutter", dartContent));
|
|
390
391
|
findings.push(...validateCommentReferenceTypeEnum(root, "flutter", dartContent));
|
|
391
392
|
findings.push(...validateSessionHandlerRetention(root, "flutter", dartContent));
|
|
393
|
+
findings.push(...validateChatUnreadVisible(root, "flutter", dartContent));
|
|
394
|
+
findings.push(...validateChatMessageOrderExplicit(root, "flutter", dartContent));
|
|
395
|
+
findings.push(...validateProfileSocialCounts(root, "flutter", dartContent));
|
|
392
396
|
findings.push(...validateChat(root, "flutter", dartContent));
|
|
393
397
|
findings.push(...(await validateDesignReuse(root, "flutter", dartContent)));
|
|
394
398
|
findings.push(...(await validateEnvSecretHygiene(root, "flutter", dartContent)));
|
|
@@ -482,6 +486,7 @@ async function validateTypeScript(root, platform) {
|
|
|
482
486
|
findings.push(...validatePollAnswerDataShape(root, platform, sourceContent));
|
|
483
487
|
findings.push(...validateCommentsQueryHasLimit(root, platform, sourceContent));
|
|
484
488
|
findings.push(...validateCommentCreationAffordance(root, platform, sourceContent));
|
|
489
|
+
findings.push(...validateRichPostComposerScope(root, platform, sourceContent));
|
|
485
490
|
findings.push(...validateCommunityDisplayNameFromSdk(root, platform, sourceContent));
|
|
486
491
|
findings.push(...validateRoomPostFetched(root, platform, sourceContent));
|
|
487
492
|
findings.push(...validateReactionStalePostRef(root, platform, sourceContent));
|
|
@@ -502,6 +507,9 @@ async function validateTypeScript(root, platform) {
|
|
|
502
507
|
findings.push(...validateImagePostChildResolutionAwaited(root, platform, sourceContent));
|
|
503
508
|
findings.push(...validateCommentReferenceTypeEnum(root, platform, sourceContent));
|
|
504
509
|
findings.push(...validateSessionHandlerRetention(root, platform, sourceContent));
|
|
510
|
+
findings.push(...validateChatUnreadVisible(root, platform, sourceContent));
|
|
511
|
+
findings.push(...validateChatMessageOrderExplicit(root, platform, sourceContent));
|
|
512
|
+
findings.push(...validateProfileSocialCounts(root, platform, sourceContent));
|
|
505
513
|
findings.push(...validateChat(root, platform, sourceContent));
|
|
506
514
|
findings.push(...(await validateDesignReuse(root, platform, sourceContent)));
|
|
507
515
|
if (platform === "typescript") {
|
|
@@ -1881,6 +1889,7 @@ async function validateIos(root) {
|
|
|
1881
1889
|
findings.push(...validatePollAnswerDataShape(root, "ios", swiftContent));
|
|
1882
1890
|
findings.push(...validateCommentsQueryHasLimit(root, "ios", swiftContent));
|
|
1883
1891
|
findings.push(...validateCommentCreationAffordance(root, "ios", swiftContent));
|
|
1892
|
+
findings.push(...validateRichPostComposerScope(root, "ios", swiftContent));
|
|
1884
1893
|
findings.push(...validateCommunityDisplayNameFromSdk(root, "ios", swiftContent));
|
|
1885
1894
|
findings.push(...validateRoomPostFetched(root, "ios", swiftContent));
|
|
1886
1895
|
findings.push(...validatePostsStatusFilter(root, "ios", swiftContent));
|
|
@@ -1899,6 +1908,9 @@ async function validateIos(root) {
|
|
|
1899
1908
|
findings.push(...validateImagePostChildResolutionAwaited(root, "ios", swiftContent));
|
|
1900
1909
|
findings.push(...validateCommentReferenceTypeEnum(root, "ios", swiftContent));
|
|
1901
1910
|
findings.push(...validateSessionHandlerRetention(root, "ios", swiftContent));
|
|
1911
|
+
findings.push(...validateChatUnreadVisible(root, "ios", swiftContent));
|
|
1912
|
+
findings.push(...validateChatMessageOrderExplicit(root, "ios", swiftContent));
|
|
1913
|
+
findings.push(...validateProfileSocialCounts(root, "ios", swiftContent));
|
|
1902
1914
|
findings.push(...validateChat(root, "ios", swiftContent));
|
|
1903
1915
|
findings.push(...(await validateDesignReuse(root, "ios", swiftContent)));
|
|
1904
1916
|
findings.push(...(await validateEnvSecretHygiene(root, "ios", swiftContent)));
|
|
@@ -2700,30 +2712,51 @@ function validatePostDataTypeHandled(root, platform, sourceContent) {
|
|
|
2700
2712
|
}
|
|
2701
2713
|
return findings;
|
|
2702
2714
|
}
|
|
2703
|
-
function
|
|
2715
|
+
function validateRichPostComposerScope(root, platform, sourceContent) {
|
|
2704
2716
|
const findings = [];
|
|
2705
|
-
const ruleId =
|
|
2717
|
+
const ruleId = `${platform}.feed.rich-post-composer-surfaced`;
|
|
2706
2718
|
let textComposerFile;
|
|
2707
2719
|
let hasRichPostCreation = false;
|
|
2708
|
-
const textCreatePat =
|
|
2709
|
-
|
|
2710
|
-
|
|
2720
|
+
const textCreatePat = {
|
|
2721
|
+
typescript: /\b(?:PostRepository\s*\.\s*)?createPost\s*\([\s\S]{0,320}(?:dataType\s*:\s*["']text["']|data\s*:\s*\{[\s\S]{0,120}\btext\b)/i,
|
|
2722
|
+
"react-native": /\b(?:PostRepository\s*\.\s*)?createPost\s*\([\s\S]{0,320}(?:dataType\s*:\s*["']text["']|data\s*:\s*\{[\s\S]{0,120}\btext\b)/i,
|
|
2723
|
+
flutter: /\bcreatePost\s*\([\s\S]{0,320}(?:AmityDataType\.TEXT|dataType\s*:\s*["']?text|data\s*:\s*\{[\s\S]{0,120}text|text\s*:)/i,
|
|
2724
|
+
ios: /\bcreatePost\s*\([\s\S]{0,320}(?:text\s*:|dataType\s*:\s*\.text|["']text["']\s*:)/i,
|
|
2725
|
+
android: /\bcreateTextPost\s*\(/,
|
|
2726
|
+
};
|
|
2727
|
+
const composerUiPat = {
|
|
2728
|
+
typescript: /\b(PostComposer|Composer|textarea|TextArea|TextField|onSubmit|onPost|handlePost|placeholder\s*=\s*["'][^"']*post)/i,
|
|
2729
|
+
"react-native": /\b(PostComposer|Composer|TextInput|Pressable|TouchableOpacity|onPress|onPost|handlePost|placeholder\s*=\s*["'][^"']*post)/i,
|
|
2730
|
+
flutter: /\b(PostComposer|Composer|TextField|TextEditingController|onPressed|FloatingActionButton|ElevatedButton)/i,
|
|
2731
|
+
ios: /\b(PostComposer|Composer|TextEditor|TextField|Button\s*\(|onPost|sendPost)/i,
|
|
2732
|
+
android: /\b(PostComposer|Composer|OutlinedTextField|TextField|onPost|createPost\s*\()/,
|
|
2733
|
+
};
|
|
2734
|
+
const richCreatePat = {
|
|
2735
|
+
typescript: /\bcreate(?:Image|Video|File|Audio|Poll|Clip|Room|LiveStream|MixedAttachment)Post\s*\(|\bcreatePoll\s*\(|\bupload(?:Image|Video|File|Clip)\s*\(|\b(?:File|Image|Video|Poll|Room|Clip)Repository\b|\battachments\s*:|\b(?:fileId|pollId|clipId|roomId)\b|\bdataType\s*:\s*["'](?:image|video|file|poll|clip|room|liveStream|livestream|audio|custom)["']|\/\/\s*vise:\s*rich-post-composer/i,
|
|
2736
|
+
"react-native": /\bcreate(?:Image|Video|File|Audio|Poll|Clip|Room|LiveStream|MixedAttachment)Post\s*\(|\bcreatePoll\s*\(|\bupload(?:Image|Video|File|Clip)\s*\(|\b(?:File|Image|Video|Poll|Room|Clip)Repository\b|\battachments\s*:|\b(?:fileId|pollId|clipId|roomId)\b|\bdataType\s*:\s*["'](?:image|video|file|poll|clip|room|liveStream|livestream|audio|custom)["']|\/\/\s*vise:\s*rich-post-composer/i,
|
|
2737
|
+
flutter: /\bcreate(?:Image|Video|File|Audio|Poll|Clip|Room|LiveStream|MixedAttachment)Post\s*\(|\bcreatePoll\s*\(|\bupload(?:Image|Video|File|Clip)\s*\(|\bAmity(?:Image|Video|Clip|Poll|Room|File)\b|\battachments\s*:|\b(?:fileId|pollId|clipId|roomId)\b|\bAmityDataType\.(?:IMAGE|VIDEO|FILE|POLL|CLIP|ROOM|LIVE_STREAM|AUDIO|CUSTOM)|\/\/\s*vise:\s*rich-post-composer/i,
|
|
2738
|
+
ios: /\bcreate(?:Image|Video|File|Audio|Poll|Clip|Room|LiveStream|MixedAttachment)Post\s*\(|\bcreatePoll\s*\(|\bupload(?:Image|Video|File|Clip)\s*\(|\bAmity(?:Image|Video|Clip|Poll|Room|File)\b|\battachments\s*:|\b(?:fileId|pollId|clipId|roomId)\b|\bdataType\s*:\s*\.(?:image|video|file|poll|clip|room|liveStream|livestream|audio|custom)|\/\/\s*vise:\s*rich-post-composer/i,
|
|
2739
|
+
android: /\bcreate(?:Image|Video|File|Audio|Poll|Clip|Room|LiveStream|MixedAttachment)Post\s*\(|\bcreatePoll\s*\(|\bupload(?:Image|Video|File|Clip)\s*\(|\bAmity(?:Image|Video|Clip|Poll|Room|File)\b|\bActivityResultContracts\.(?:GetContent|PickVisualMedia)\b|\brememberLauncherForActivityResult\b|\/\/\s*vise:\s*rich-post-composer/,
|
|
2740
|
+
};
|
|
2741
|
+
const textPat = textCreatePat[platform] ?? textCreatePat.typescript;
|
|
2742
|
+
const uiPat = composerUiPat[platform] ?? composerUiPat.typescript;
|
|
2743
|
+
const richPat = richCreatePat[platform] ?? richCreatePat.typescript;
|
|
2711
2744
|
for (const [file, content] of sourceContent) {
|
|
2712
|
-
if (
|
|
2745
|
+
if (path.basename(file).endsWith(".d.ts"))
|
|
2713
2746
|
continue;
|
|
2714
2747
|
if (/\/\/\s*vise:\s*rich-post-composer\s+intentional/i.test(content))
|
|
2715
2748
|
return findings;
|
|
2716
|
-
const astLang = astLanguageForFile(file,
|
|
2749
|
+
const astLang = astLanguageForFile(file, platform);
|
|
2717
2750
|
const checkContent = astLang ? stripComments(astLang, content) : content;
|
|
2718
|
-
if (
|
|
2751
|
+
if (richPat.test(checkContent)) {
|
|
2719
2752
|
hasRichPostCreation = true;
|
|
2720
2753
|
}
|
|
2721
|
-
if (!textComposerFile &&
|
|
2754
|
+
if (!textComposerFile && textPat.test(checkContent) && uiPat.test(checkContent)) {
|
|
2722
2755
|
textComposerFile = file;
|
|
2723
2756
|
}
|
|
2724
2757
|
}
|
|
2725
2758
|
if (textComposerFile && !hasRichPostCreation) {
|
|
2726
|
-
findings.push(finding(ruleId, "warning", "
|
|
2759
|
+
findings.push(finding(ruleId, "warning", "Feed composer only creates text posts. The user can read a mixed social.plus feed but cannot create image, video, file, poll, clip, room, or mixed-attachment posts.", relativeFile(root, textComposerFile), "Surface the rich-post scope explicitly. Either implement the needed createImagePost/createVideoPost/createFilePost/createPollPost/createClipPost/createRoomPost paths with the matching upload flow, or record a deliberate scope decision with // vise: rich-post-composer intentional — <reason>."));
|
|
2727
2760
|
}
|
|
2728
2761
|
return findings;
|
|
2729
2762
|
}
|
|
@@ -2981,66 +3014,114 @@ function validateCommentThreadUiStates(root, platform, sourceContent) {
|
|
|
2981
3014
|
}
|
|
2982
3015
|
return findings;
|
|
2983
3016
|
}
|
|
2984
|
-
function
|
|
3017
|
+
function validateChatUnreadVisible(root, platform, sourceContent) {
|
|
2985
3018
|
const findings = [];
|
|
2986
|
-
const ruleId =
|
|
3019
|
+
const ruleId = `${platform}.chat.unread-visible`;
|
|
2987
3020
|
let chatListFile;
|
|
2988
3021
|
let hasUnreadSignal = false;
|
|
3022
|
+
const channelListPat = {
|
|
3023
|
+
typescript: /\b(?:ChannelRepository|ChatRepository|AmityChannelRepository)\s*\.\s*(?:getChannels|queryChannels)\b|\bgetChannels\s*\(/,
|
|
3024
|
+
"react-native": /\b(?:ChannelRepository|ChatRepository|AmityChannelRepository)\s*\.\s*(?:getChannels|queryChannels)\b|\bgetChannels\s*\(/,
|
|
3025
|
+
flutter: /\bnewChannelRepository\s*\(\s*\)\s*\.\s*getChannels\b|\bgetChannels\s*\(|\bAmityChannel\b/,
|
|
3026
|
+
ios: /\bAmityChannelRepository\b|\bAmityChannelQuery\b|\bgetChannels\s*\(/,
|
|
3027
|
+
android: /\bAmityChannel\b[\s\S]{0,240}\b(?:getChannels|ChannelRow|ChatScreen|Messages)\b|\bgetChannels\s*\(/,
|
|
3028
|
+
};
|
|
3029
|
+
const chatListUiPat = {
|
|
3030
|
+
typescript: /\b(ChannelList|ChannelRow|ChatList|channels\s*\.\s*map|FlatList|channel\s*\??\.\s*(?:displayName|name|channelId))/i,
|
|
3031
|
+
"react-native": /\b(ChannelList|ChannelRow|ChatList|FlatList|channels\s*\.\s*map|channel\s*\??\.\s*(?:displayName|name|channelId))/i,
|
|
3032
|
+
flutter: /\b(ChannelList|ChannelRow|ChatScreen|ListView|AmityChannel|channel\.(?:displayName|name|channelId))/i,
|
|
3033
|
+
ios: /\b(ChannelList|ChannelRow|ChatView|List\s*\(|ForEach|AmityChannel|channel\.(?:displayName|displayName|channelId))/i,
|
|
3034
|
+
android: /\b(?:getChannels|ChannelRow|ChatScreen|Messages|LazyColumn|RecyclerView)\b/,
|
|
3035
|
+
};
|
|
3036
|
+
const unreadPat = {
|
|
3037
|
+
typescript: /\bunreadCount\b|\bgetUnread\w*\b|\bgetTotalChannelUnread\b|\bgetTotalChannelsUnreadInfo\b|\bobserveUserUnread\b|\bUserUnread\b/i,
|
|
3038
|
+
"react-native": /\bunreadCount\b|\bgetUnread\w*\b|\bgetTotalChannelUnread\b|\bgetTotalChannelsUnreadInfo\b|\bobserveUserUnread\b|\bUserUnread\b/i,
|
|
3039
|
+
flutter: /\bunreadCount\b|\bgetUnread\w*\b|\bgetSubChannelsUnreadCount\b|\bgetTotalChannelsUnreadInfo\b|\bobserveUserUnread\b|\bUserUnread\b/i,
|
|
3040
|
+
ios: /\bunreadCount\b|\bgetUnread\w*\b|\bsubChannelsUnreadCount\b|\btotalChannelUnread\b|\bobserveUserUnread\b|\bUserUnread\b/i,
|
|
3041
|
+
android: /\bgetUnreadCount\s*\(|\bgetSubChannelsUnreadCount\s*\(|\bgetTotalChannelsUnreadInfo\s*\(|\bgetTotalChannelUnread\s*\(|\bobserveUserUnread\s*\(|\bUserUnread\b|\bunreadCount\b/i,
|
|
3042
|
+
};
|
|
3043
|
+
const queryPat = channelListPat[platform] ?? channelListPat.typescript;
|
|
3044
|
+
const uiPat = chatListUiPat[platform] ?? chatListUiPat.typescript;
|
|
3045
|
+
const unreadSignalPat = unreadPat[platform] ?? unreadPat.typescript;
|
|
2989
3046
|
for (const [file, content] of sourceContent) {
|
|
2990
|
-
if (!file.endsWith(".kt") && !file.endsWith(".java"))
|
|
2991
|
-
continue;
|
|
2992
3047
|
if (/\/\/\s*vise:\s*chat-unread intentional/i.test(content))
|
|
2993
3048
|
return findings;
|
|
2994
|
-
const astLang = astLanguageForFile(file,
|
|
3049
|
+
const astLang = astLanguageForFile(file, platform);
|
|
2995
3050
|
const checkContent = astLang ? stripComments(astLang, content) : content;
|
|
2996
|
-
if (
|
|
3051
|
+
if (unreadSignalPat.test(checkContent)) {
|
|
2997
3052
|
hasUnreadSignal = true;
|
|
2998
3053
|
}
|
|
2999
|
-
if (!chatListFile &&
|
|
3054
|
+
if (!chatListFile && queryPat.test(checkContent) && uiPat.test(checkContent)) {
|
|
3000
3055
|
chatListFile = file;
|
|
3001
3056
|
}
|
|
3002
3057
|
}
|
|
3003
3058
|
if (chatListFile && !hasUnreadSignal) {
|
|
3004
|
-
findings.push(finding(ruleId, "warning", "
|
|
3059
|
+
findings.push(finding(ruleId, "warning", "Chat channel list is present but no SDK unread count is rendered or subscribed. The UI will drop unread badges/counts.", relativeFile(root, chatListFile), "Render unread from the SDK, for example channel.getUnreadCount()/getSubChannelsUnreadCount() for channel rows or AmityCoreClient.observeUserUnread()/getTotalChannelUnread() for a global badge. Add // vise: chat-unread intentional — <reason> only for a deliberately unread-free chat surface."));
|
|
3005
3060
|
}
|
|
3006
3061
|
return findings;
|
|
3007
3062
|
}
|
|
3008
|
-
function
|
|
3063
|
+
function validateChatMessageOrderExplicit(root, platform, sourceContent) {
|
|
3009
3064
|
const findings = [];
|
|
3010
|
-
const ruleId =
|
|
3065
|
+
const ruleId = `${platform}.chat.sort-explicit`;
|
|
3066
|
+
const messageQueryPat = {
|
|
3067
|
+
typescript: /\b(?:MessageRepository|ChatRepository|AmityMessageRepository)\s*\.\s*(?:getMessages|queryMessages)\s*\(/,
|
|
3068
|
+
"react-native": /\b(?:MessageRepository|ChatRepository|AmityMessageRepository)\s*\.\s*(?:getMessages|queryMessages)\s*\(/,
|
|
3069
|
+
flutter: /\bnewMessageRepository\s*\(\s*\)\s*\.\s*getMessages\b|\bgetMessages\s*\(/,
|
|
3070
|
+
ios: /\bAmityMessageRepository\b|\bAmityMessageQuery\b|\bgetMessages\s*\(/,
|
|
3071
|
+
android: /AmityChatClient\s*\.\s*newMessageRepository\s*\(\s*\)\s*\.getMessages\s*\([^)]*\)[\s\S]{0,180}\.build\s*\(\s*\)[\s\S]{0,80}\.query\s*\(\s*\)/,
|
|
3072
|
+
};
|
|
3073
|
+
const explicitSortPat = {
|
|
3074
|
+
typescript: /\bsortBy\s*:|\borderBy\s*:|\bsort\s*:|\bMessageQuerySort\b|\bFIRST_CREATED\b|\bLAST_CREATED\b|\bsorted(?:By|With)?\b|\breverse(?:d)?\s*\(/i,
|
|
3075
|
+
"react-native": /\bsortBy\s*:|\borderBy\s*:|\bsort\s*:|\bMessageQuerySort\b|\bFIRST_CREATED\b|\bLAST_CREATED\b|\bsorted(?:By|With)?\b|\breverse(?:d)?\s*\(/i,
|
|
3076
|
+
flutter: /\.sortBy\s*\(|\bsortBy\s*:|\bAmityMessageQuerySortOption\b|\bFIRST_CREATED\b|\bLAST_CREATED\b|\bsortedBy(?:Descending)?\s*\(|\breversed\s*\(/i,
|
|
3077
|
+
ios: /\bsortBy\s*:|\.sortBy\s*=|\bAmityMessageQuerySortOption\b|\bfirstCreated\b|\blastCreated\b|\bsorted\s*\(|\.reversed\s*\(/i,
|
|
3078
|
+
android: /\.sortBy\s*\(|\bAmityMessageQuerySortOption\.(?:FIRST_CREATED|LAST_CREATED)\b|\bsortedBy(?:Descending)?\s*\(|\breversed\s*\(\s*\)/,
|
|
3079
|
+
};
|
|
3080
|
+
const queryPat = messageQueryPat[platform] ?? messageQueryPat.typescript;
|
|
3081
|
+
const sortPat = explicitSortPat[platform] ?? explicitSortPat.typescript;
|
|
3011
3082
|
for (const [file, content] of sourceContent) {
|
|
3012
|
-
if (!file.endsWith(".kt") && !file.endsWith(".java"))
|
|
3013
|
-
continue;
|
|
3014
3083
|
if (/\/\/\s*vise:\s*chat-sort intentional/i.test(content))
|
|
3015
3084
|
continue;
|
|
3016
|
-
const astLang = astLanguageForFile(file,
|
|
3085
|
+
const astLang = astLanguageForFile(file, platform);
|
|
3017
3086
|
const checkContent = astLang ? stripComments(astLang, content) : content;
|
|
3018
|
-
|
|
3019
|
-
if (!hasMessageQuery)
|
|
3087
|
+
if (!queryPat.test(checkContent))
|
|
3020
3088
|
continue;
|
|
3021
|
-
|
|
3022
|
-
if (hasExplicitSort)
|
|
3089
|
+
if (sortPat.test(checkContent))
|
|
3023
3090
|
continue;
|
|
3024
|
-
findings.push(finding(ruleId, "warning", "
|
|
3091
|
+
findings.push(finding(ruleId, "warning", "Message query relies on implicit SDK ordering. This commonly reverses the visible chat order when the UI assumes newest-first or oldest-first.", relativeFile(root, file), "Declare the intended order explicitly with .sortBy(AmityMessageQuerySortOption.FIRST_CREATED or LAST_CREATED), or apply a clearly named UI sort/reverse before rendering. Add // vise: chat-sort intentional — <reason> if the SDK default is intentionally used."));
|
|
3025
3092
|
break;
|
|
3026
3093
|
}
|
|
3027
3094
|
return findings;
|
|
3028
3095
|
}
|
|
3029
|
-
function
|
|
3096
|
+
function validateProfileSocialCounts(root, platform, sourceContent) {
|
|
3030
3097
|
const findings = [];
|
|
3031
|
-
const ruleId =
|
|
3098
|
+
const ruleId = `${platform}.profile.social-counts-from-sdk`;
|
|
3099
|
+
const sdkCountsPat = {
|
|
3100
|
+
typescript: /\bfollowerCount\b|\bfollowingCount\b|\bgetFollowerCount\s*\(|\bgetFollowingCount\s*\(|\bgetFollowers\s*\(|\bgetFollowings\s*\(|\bUserRepository\b/i,
|
|
3101
|
+
"react-native": /\bfollowerCount\b|\bfollowingCount\b|\bgetFollowerCount\s*\(|\bgetFollowingCount\s*\(|\bgetFollowers\s*\(|\bgetFollowings\s*\(|\bUserRepository\b/i,
|
|
3102
|
+
flutter: /\bfollowerCount\b|\bfollowingCount\b|\bgetFollowerCount\s*\(|\bgetFollowingCount\s*\(|\bgetFollowers\s*\(|\bgetFollowings\s*\(|\bnewUserRepository\s*\(/i,
|
|
3103
|
+
ios: /\bfollowerCount\b|\bfollowingCount\b|\bgetFollowerCount\s*\(|\bgetFollowingCount\s*\(|\bgetFollowers\s*\(|\bgetFollowings\s*\(|\bAmityUserRepository\b/i,
|
|
3104
|
+
android: /\bgetFollowerCount\s*\(|\bgetFollowingCount\s*\(|\bgetFollowers\s*\(|\bgetFollowings\s*\(|\bAmityCoreClient\s*\.\s*newUserRepository\s*\(\s*\)/,
|
|
3105
|
+
};
|
|
3106
|
+
const placeholderCountsPat = {
|
|
3107
|
+
typescript: /value\s*=\s*["'][—-]+["']|value\s*=\s*["']0["']|>\s*[—-]+\s*<|>\s*0\s*</,
|
|
3108
|
+
"react-native": /value\s*=\s*["'][—-]+["']|value\s*=\s*["']0["']|>\s*[—-]+\s*<|>\s*0\s*</,
|
|
3109
|
+
flutter: /Text\s*\(\s*["'][—-]+["']|Text\s*\(\s*["']0["']|value\s*:\s*["'][—-]+["']|value\s*:\s*["']0["']/,
|
|
3110
|
+
ios: /Text\s*\(\s*["'][—-]+["']\s*\)|Text\s*\(\s*["']0["']\s*\)|value\s*:\s*["'][—-]+["']|value\s*:\s*["']0["']/,
|
|
3111
|
+
android: /value\s*=\s*["'][—-]+["']|Text\s*\(\s*["'][—-]+["']|value\s*=\s*["']0["']|Text\s*\(\s*["']0["']/,
|
|
3112
|
+
};
|
|
3113
|
+
const sdkPat = sdkCountsPat[platform] ?? sdkCountsPat.typescript;
|
|
3114
|
+
const placeholderPat = placeholderCountsPat[platform] ?? placeholderCountsPat.typescript;
|
|
3032
3115
|
for (const [file, content] of sourceContent) {
|
|
3033
|
-
if (!file.endsWith(".kt") && !file.endsWith(".java"))
|
|
3034
|
-
continue;
|
|
3035
3116
|
if (/\/\/\s*vise:\s*profile-counts intentional/i.test(content))
|
|
3036
3117
|
continue;
|
|
3037
|
-
const astLang = astLanguageForFile(file,
|
|
3118
|
+
const astLang = astLanguageForFile(file, platform);
|
|
3038
3119
|
const checkContent = astLang ? stripComments(astLang, content) : content;
|
|
3039
3120
|
const rendersFollowerLabels = /Followers?/.test(checkContent) && /Following/.test(checkContent);
|
|
3040
3121
|
if (!rendersFollowerLabels)
|
|
3041
3122
|
continue;
|
|
3042
|
-
const hasSdkCounts =
|
|
3043
|
-
const hasPlaceholderCounts =
|
|
3123
|
+
const hasSdkCounts = sdkPat.test(checkContent);
|
|
3124
|
+
const hasPlaceholderCounts = placeholderPat.test(checkContent);
|
|
3044
3125
|
if (hasPlaceholderCounts && !hasSdkCounts) {
|
|
3045
3126
|
findings.push(finding(ruleId, "warning", "Profile screen labels follower/following counts but fills them with placeholders instead of SDK values.", relativeFile(root, file), "Use AmityUser.getFollowerCount()/getFollowingCount() from the current user/live profile object, or query getFollowers()/getFollowings() if the count must update from a collection. Add // vise: profile-counts intentional — <reason> only when the placeholders are deliberate."));
|
|
3046
3127
|
break;
|
package/package.json
CHANGED
package/rules/chat.yaml
CHANGED
|
@@ -872,6 +872,154 @@
|
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
874
|
},
|
|
875
|
+
{
|
|
876
|
+
"id": "typescript.chat.unread-visible",
|
|
877
|
+
"version": 1,
|
|
878
|
+
"title": "TypeScript chat UI must render SDK unread counts",
|
|
879
|
+
"severity": "warning",
|
|
880
|
+
"rationale": "A chat channel list without unread counts or badges silently drops an expected messaging affordance. The SDK exposes unread state on channels or user unread streams for global badges.",
|
|
881
|
+
"applies_when": {
|
|
882
|
+
"platforms": [
|
|
883
|
+
"typescript"
|
|
884
|
+
],
|
|
885
|
+
"outcomes": [
|
|
886
|
+
"add-feed",
|
|
887
|
+
"add-chat",
|
|
888
|
+
"validate-setup"
|
|
889
|
+
]
|
|
890
|
+
},
|
|
891
|
+
"enforcement": {
|
|
892
|
+
"deterministic": [
|
|
893
|
+
{
|
|
894
|
+
"check": "validator-finding-absent",
|
|
895
|
+
"finding_rule_id": "typescript.chat.unread-visible"
|
|
896
|
+
}
|
|
897
|
+
],
|
|
898
|
+
"attestation": {
|
|
899
|
+
"allowed": true,
|
|
900
|
+
"host_agent_min_confidence": "high",
|
|
901
|
+
"human_allowed": true,
|
|
902
|
+
"evidence_required": [
|
|
903
|
+
{
|
|
904
|
+
"field": "chat_unread_source",
|
|
905
|
+
"description": "Where unread counts are sourced from the SDK and rendered, or why the chat surface intentionally omits unread state.",
|
|
906
|
+
"upload_policy": "upload-with-consent"
|
|
907
|
+
}
|
|
908
|
+
]
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
"id": "react-native.chat.unread-visible",
|
|
914
|
+
"version": 1,
|
|
915
|
+
"title": "React Native chat UI must render SDK unread counts",
|
|
916
|
+
"severity": "warning",
|
|
917
|
+
"rationale": "A chat channel list without unread counts or badges silently drops an expected messaging affordance. The SDK exposes unread state on channels or user unread streams for global badges.",
|
|
918
|
+
"applies_when": {
|
|
919
|
+
"platforms": [
|
|
920
|
+
"react-native"
|
|
921
|
+
],
|
|
922
|
+
"outcomes": [
|
|
923
|
+
"add-feed",
|
|
924
|
+
"add-chat",
|
|
925
|
+
"validate-setup"
|
|
926
|
+
]
|
|
927
|
+
},
|
|
928
|
+
"enforcement": {
|
|
929
|
+
"deterministic": [
|
|
930
|
+
{
|
|
931
|
+
"check": "validator-finding-absent",
|
|
932
|
+
"finding_rule_id": "react-native.chat.unread-visible"
|
|
933
|
+
}
|
|
934
|
+
],
|
|
935
|
+
"attestation": {
|
|
936
|
+
"allowed": true,
|
|
937
|
+
"host_agent_min_confidence": "high",
|
|
938
|
+
"human_allowed": true,
|
|
939
|
+
"evidence_required": [
|
|
940
|
+
{
|
|
941
|
+
"field": "chat_unread_source",
|
|
942
|
+
"description": "Where unread counts are sourced from the SDK and rendered, or why the chat surface intentionally omits unread state.",
|
|
943
|
+
"upload_policy": "upload-with-consent"
|
|
944
|
+
}
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
"id": "flutter.chat.unread-visible",
|
|
951
|
+
"version": 1,
|
|
952
|
+
"title": "Flutter chat UI must render SDK unread counts",
|
|
953
|
+
"severity": "warning",
|
|
954
|
+
"rationale": "A chat channel list without unread counts or badges silently drops an expected messaging affordance. The SDK exposes unread state on channels or user unread streams for global badges.",
|
|
955
|
+
"applies_when": {
|
|
956
|
+
"platforms": [
|
|
957
|
+
"flutter"
|
|
958
|
+
],
|
|
959
|
+
"outcomes": [
|
|
960
|
+
"add-feed",
|
|
961
|
+
"add-chat",
|
|
962
|
+
"validate-setup"
|
|
963
|
+
]
|
|
964
|
+
},
|
|
965
|
+
"enforcement": {
|
|
966
|
+
"deterministic": [
|
|
967
|
+
{
|
|
968
|
+
"check": "validator-finding-absent",
|
|
969
|
+
"finding_rule_id": "flutter.chat.unread-visible"
|
|
970
|
+
}
|
|
971
|
+
],
|
|
972
|
+
"attestation": {
|
|
973
|
+
"allowed": true,
|
|
974
|
+
"host_agent_min_confidence": "high",
|
|
975
|
+
"human_allowed": true,
|
|
976
|
+
"evidence_required": [
|
|
977
|
+
{
|
|
978
|
+
"field": "chat_unread_source",
|
|
979
|
+
"description": "Where unread counts are sourced from the SDK and rendered, or why the chat surface intentionally omits unread state.",
|
|
980
|
+
"upload_policy": "upload-with-consent"
|
|
981
|
+
}
|
|
982
|
+
]
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
"id": "ios.chat.unread-visible",
|
|
988
|
+
"version": 1,
|
|
989
|
+
"title": "iOS chat UI must render SDK unread counts",
|
|
990
|
+
"severity": "warning",
|
|
991
|
+
"rationale": "A chat channel list without unread counts or badges silently drops an expected messaging affordance. The SDK exposes unread state on channels or user unread streams for global badges.",
|
|
992
|
+
"applies_when": {
|
|
993
|
+
"platforms": [
|
|
994
|
+
"ios"
|
|
995
|
+
],
|
|
996
|
+
"outcomes": [
|
|
997
|
+
"add-feed",
|
|
998
|
+
"add-chat",
|
|
999
|
+
"validate-setup"
|
|
1000
|
+
]
|
|
1001
|
+
},
|
|
1002
|
+
"enforcement": {
|
|
1003
|
+
"deterministic": [
|
|
1004
|
+
{
|
|
1005
|
+
"check": "validator-finding-absent",
|
|
1006
|
+
"finding_rule_id": "ios.chat.unread-visible"
|
|
1007
|
+
}
|
|
1008
|
+
],
|
|
1009
|
+
"attestation": {
|
|
1010
|
+
"allowed": true,
|
|
1011
|
+
"host_agent_min_confidence": "high",
|
|
1012
|
+
"human_allowed": true,
|
|
1013
|
+
"evidence_required": [
|
|
1014
|
+
{
|
|
1015
|
+
"field": "chat_unread_source",
|
|
1016
|
+
"description": "Where unread counts are sourced from the SDK and rendered, or why the chat surface intentionally omits unread state.",
|
|
1017
|
+
"upload_policy": "upload-with-consent"
|
|
1018
|
+
}
|
|
1019
|
+
]
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
875
1023
|
{
|
|
876
1024
|
"id": "android.chat.unread-visible",
|
|
877
1025
|
"version": 1,
|
|
@@ -909,6 +1057,150 @@
|
|
|
909
1057
|
}
|
|
910
1058
|
}
|
|
911
1059
|
},
|
|
1060
|
+
{
|
|
1061
|
+
"id": "typescript.chat.sort-explicit",
|
|
1062
|
+
"version": 1,
|
|
1063
|
+
"title": "TypeScript chat message order must be explicit",
|
|
1064
|
+
"severity": "warning",
|
|
1065
|
+
"rationale": "Relying on default SDK message ordering is easy to invert in a message list. The query or UI should declare whether messages render first-created or last-created order.",
|
|
1066
|
+
"applies_when": {
|
|
1067
|
+
"platforms": [
|
|
1068
|
+
"typescript"
|
|
1069
|
+
],
|
|
1070
|
+
"outcomes": [
|
|
1071
|
+
"validate-setup"
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
"enforcement": {
|
|
1075
|
+
"deterministic": [
|
|
1076
|
+
{
|
|
1077
|
+
"check": "validator-finding-absent",
|
|
1078
|
+
"finding_rule_id": "typescript.chat.sort-explicit"
|
|
1079
|
+
}
|
|
1080
|
+
],
|
|
1081
|
+
"attestation": {
|
|
1082
|
+
"allowed": true,
|
|
1083
|
+
"host_agent_min_confidence": "high",
|
|
1084
|
+
"human_allowed": true,
|
|
1085
|
+
"evidence_required": [
|
|
1086
|
+
{
|
|
1087
|
+
"field": "chat_message_order",
|
|
1088
|
+
"description": "The explicit message order used in SDK query or UI rendering, and why it matches the product UI.",
|
|
1089
|
+
"upload_policy": "upload-with-consent"
|
|
1090
|
+
}
|
|
1091
|
+
]
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"id": "react-native.chat.sort-explicit",
|
|
1097
|
+
"version": 1,
|
|
1098
|
+
"title": "React Native chat message order must be explicit",
|
|
1099
|
+
"severity": "warning",
|
|
1100
|
+
"rationale": "Relying on default SDK message ordering is easy to invert in a message list. The query or UI should declare whether messages render first-created or last-created order.",
|
|
1101
|
+
"applies_when": {
|
|
1102
|
+
"platforms": [
|
|
1103
|
+
"react-native"
|
|
1104
|
+
],
|
|
1105
|
+
"outcomes": [
|
|
1106
|
+
"validate-setup"
|
|
1107
|
+
]
|
|
1108
|
+
},
|
|
1109
|
+
"enforcement": {
|
|
1110
|
+
"deterministic": [
|
|
1111
|
+
{
|
|
1112
|
+
"check": "validator-finding-absent",
|
|
1113
|
+
"finding_rule_id": "react-native.chat.sort-explicit"
|
|
1114
|
+
}
|
|
1115
|
+
],
|
|
1116
|
+
"attestation": {
|
|
1117
|
+
"allowed": true,
|
|
1118
|
+
"host_agent_min_confidence": "high",
|
|
1119
|
+
"human_allowed": true,
|
|
1120
|
+
"evidence_required": [
|
|
1121
|
+
{
|
|
1122
|
+
"field": "chat_message_order",
|
|
1123
|
+
"description": "The explicit message order used in SDK query or UI rendering, and why it matches the product UI.",
|
|
1124
|
+
"upload_policy": "upload-with-consent"
|
|
1125
|
+
}
|
|
1126
|
+
]
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
"id": "flutter.chat.sort-explicit",
|
|
1132
|
+
"version": 1,
|
|
1133
|
+
"title": "Flutter chat message order must be explicit",
|
|
1134
|
+
"severity": "warning",
|
|
1135
|
+
"rationale": "Relying on default SDK message ordering is easy to invert in a message list. The query or UI should declare whether messages render first-created or last-created order.",
|
|
1136
|
+
"applies_when": {
|
|
1137
|
+
"platforms": [
|
|
1138
|
+
"flutter"
|
|
1139
|
+
],
|
|
1140
|
+
"outcomes": [
|
|
1141
|
+
"add-feed",
|
|
1142
|
+
"add-chat",
|
|
1143
|
+
"validate-setup"
|
|
1144
|
+
]
|
|
1145
|
+
},
|
|
1146
|
+
"enforcement": {
|
|
1147
|
+
"deterministic": [
|
|
1148
|
+
{
|
|
1149
|
+
"check": "validator-finding-absent",
|
|
1150
|
+
"finding_rule_id": "flutter.chat.sort-explicit"
|
|
1151
|
+
}
|
|
1152
|
+
],
|
|
1153
|
+
"attestation": {
|
|
1154
|
+
"allowed": true,
|
|
1155
|
+
"host_agent_min_confidence": "high",
|
|
1156
|
+
"human_allowed": true,
|
|
1157
|
+
"evidence_required": [
|
|
1158
|
+
{
|
|
1159
|
+
"field": "chat_message_order",
|
|
1160
|
+
"description": "The explicit message order used in SDK query or UI rendering, and why it matches the product UI.",
|
|
1161
|
+
"upload_policy": "upload-with-consent"
|
|
1162
|
+
}
|
|
1163
|
+
]
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
"id": "ios.chat.sort-explicit",
|
|
1169
|
+
"version": 1,
|
|
1170
|
+
"title": "iOS chat message order must be explicit",
|
|
1171
|
+
"severity": "warning",
|
|
1172
|
+
"rationale": "Relying on default SDK message ordering is easy to invert in a message list. The query or UI should declare whether messages render first-created or last-created order.",
|
|
1173
|
+
"applies_when": {
|
|
1174
|
+
"platforms": [
|
|
1175
|
+
"ios"
|
|
1176
|
+
],
|
|
1177
|
+
"outcomes": [
|
|
1178
|
+
"add-feed",
|
|
1179
|
+
"add-chat",
|
|
1180
|
+
"validate-setup"
|
|
1181
|
+
]
|
|
1182
|
+
},
|
|
1183
|
+
"enforcement": {
|
|
1184
|
+
"deterministic": [
|
|
1185
|
+
{
|
|
1186
|
+
"check": "validator-finding-absent",
|
|
1187
|
+
"finding_rule_id": "ios.chat.sort-explicit"
|
|
1188
|
+
}
|
|
1189
|
+
],
|
|
1190
|
+
"attestation": {
|
|
1191
|
+
"allowed": true,
|
|
1192
|
+
"host_agent_min_confidence": "high",
|
|
1193
|
+
"human_allowed": true,
|
|
1194
|
+
"evidence_required": [
|
|
1195
|
+
{
|
|
1196
|
+
"field": "chat_message_order",
|
|
1197
|
+
"description": "The explicit message order used in SDK query or UI rendering, and why it matches the product UI.",
|
|
1198
|
+
"upload_policy": "upload-with-consent"
|
|
1199
|
+
}
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
912
1204
|
{
|
|
913
1205
|
"id": "android.chat.sort-explicit",
|
|
914
1206
|
"version": 1,
|
package/rules/feed.yaml
CHANGED
|
@@ -3578,6 +3578,150 @@
|
|
|
3578
3578
|
}
|
|
3579
3579
|
}
|
|
3580
3580
|
},
|
|
3581
|
+
{
|
|
3582
|
+
"id": "typescript.feed.rich-post-composer-surfaced",
|
|
3583
|
+
"version": 1,
|
|
3584
|
+
"title": "TypeScript feed composer must surface rich post scope",
|
|
3585
|
+
"severity": "warning",
|
|
3586
|
+
"rationale": "A social.plus feed can contain rich post types. If the composer only creates text posts, users cannot create image, video, file, poll, clip, room, livestream, or mixed-attachment posts unless the product explicitly scopes those out.",
|
|
3587
|
+
"applies_when": {
|
|
3588
|
+
"platforms": [
|
|
3589
|
+
"typescript"
|
|
3590
|
+
],
|
|
3591
|
+
"outcomes": [
|
|
3592
|
+
"add-feed",
|
|
3593
|
+
"validate-setup"
|
|
3594
|
+
]
|
|
3595
|
+
},
|
|
3596
|
+
"enforcement": {
|
|
3597
|
+
"deterministic": [
|
|
3598
|
+
{
|
|
3599
|
+
"check": "validator-finding-absent",
|
|
3600
|
+
"finding_rule_id": "typescript.feed.rich-post-composer-surfaced"
|
|
3601
|
+
}
|
|
3602
|
+
],
|
|
3603
|
+
"attestation": {
|
|
3604
|
+
"allowed": true,
|
|
3605
|
+
"host_agent_min_confidence": "high",
|
|
3606
|
+
"human_allowed": true,
|
|
3607
|
+
"evidence_required": [
|
|
3608
|
+
{
|
|
3609
|
+
"field": "rich_post_composer_scope",
|
|
3610
|
+
"description": "Which rich post creation flows are implemented, or the explicit product reason for a text-only composer.",
|
|
3611
|
+
"upload_policy": "upload-with-consent"
|
|
3612
|
+
}
|
|
3613
|
+
]
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
},
|
|
3617
|
+
{
|
|
3618
|
+
"id": "react-native.feed.rich-post-composer-surfaced",
|
|
3619
|
+
"version": 1,
|
|
3620
|
+
"title": "React Native feed composer must surface rich post scope",
|
|
3621
|
+
"severity": "warning",
|
|
3622
|
+
"rationale": "A social.plus feed can contain rich post types. If the composer only creates text posts, users cannot create image, video, file, poll, clip, room, livestream, or mixed-attachment posts unless the product explicitly scopes those out.",
|
|
3623
|
+
"applies_when": {
|
|
3624
|
+
"platforms": [
|
|
3625
|
+
"react-native"
|
|
3626
|
+
],
|
|
3627
|
+
"outcomes": [
|
|
3628
|
+
"add-feed",
|
|
3629
|
+
"validate-setup"
|
|
3630
|
+
]
|
|
3631
|
+
},
|
|
3632
|
+
"enforcement": {
|
|
3633
|
+
"deterministic": [
|
|
3634
|
+
{
|
|
3635
|
+
"check": "validator-finding-absent",
|
|
3636
|
+
"finding_rule_id": "react-native.feed.rich-post-composer-surfaced"
|
|
3637
|
+
}
|
|
3638
|
+
],
|
|
3639
|
+
"attestation": {
|
|
3640
|
+
"allowed": true,
|
|
3641
|
+
"host_agent_min_confidence": "high",
|
|
3642
|
+
"human_allowed": true,
|
|
3643
|
+
"evidence_required": [
|
|
3644
|
+
{
|
|
3645
|
+
"field": "rich_post_composer_scope",
|
|
3646
|
+
"description": "Which rich post creation flows are implemented, or the explicit product reason for a text-only composer.",
|
|
3647
|
+
"upload_policy": "upload-with-consent"
|
|
3648
|
+
}
|
|
3649
|
+
]
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
},
|
|
3653
|
+
{
|
|
3654
|
+
"id": "flutter.feed.rich-post-composer-surfaced",
|
|
3655
|
+
"version": 1,
|
|
3656
|
+
"title": "Flutter feed composer must surface rich post scope",
|
|
3657
|
+
"severity": "warning",
|
|
3658
|
+
"rationale": "A social.plus feed can contain rich post types. If the composer only creates text posts, users cannot create image, video, file, poll, clip, room, livestream, or mixed-attachment posts unless the product explicitly scopes those out.",
|
|
3659
|
+
"applies_when": {
|
|
3660
|
+
"platforms": [
|
|
3661
|
+
"flutter"
|
|
3662
|
+
],
|
|
3663
|
+
"outcomes": [
|
|
3664
|
+
"add-feed",
|
|
3665
|
+
"validate-setup"
|
|
3666
|
+
]
|
|
3667
|
+
},
|
|
3668
|
+
"enforcement": {
|
|
3669
|
+
"deterministic": [
|
|
3670
|
+
{
|
|
3671
|
+
"check": "validator-finding-absent",
|
|
3672
|
+
"finding_rule_id": "flutter.feed.rich-post-composer-surfaced"
|
|
3673
|
+
}
|
|
3674
|
+
],
|
|
3675
|
+
"attestation": {
|
|
3676
|
+
"allowed": true,
|
|
3677
|
+
"host_agent_min_confidence": "high",
|
|
3678
|
+
"human_allowed": true,
|
|
3679
|
+
"evidence_required": [
|
|
3680
|
+
{
|
|
3681
|
+
"field": "rich_post_composer_scope",
|
|
3682
|
+
"description": "Which rich post creation flows are implemented, or the explicit product reason for a text-only composer.",
|
|
3683
|
+
"upload_policy": "upload-with-consent"
|
|
3684
|
+
}
|
|
3685
|
+
]
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
"id": "ios.feed.rich-post-composer-surfaced",
|
|
3691
|
+
"version": 1,
|
|
3692
|
+
"title": "iOS feed composer must surface rich post scope",
|
|
3693
|
+
"severity": "warning",
|
|
3694
|
+
"rationale": "A social.plus feed can contain rich post types. If the composer only creates text posts, users cannot create image, video, file, poll, clip, room, livestream, or mixed-attachment posts unless the product explicitly scopes those out.",
|
|
3695
|
+
"applies_when": {
|
|
3696
|
+
"platforms": [
|
|
3697
|
+
"ios"
|
|
3698
|
+
],
|
|
3699
|
+
"outcomes": [
|
|
3700
|
+
"add-feed",
|
|
3701
|
+
"validate-setup"
|
|
3702
|
+
]
|
|
3703
|
+
},
|
|
3704
|
+
"enforcement": {
|
|
3705
|
+
"deterministic": [
|
|
3706
|
+
{
|
|
3707
|
+
"check": "validator-finding-absent",
|
|
3708
|
+
"finding_rule_id": "ios.feed.rich-post-composer-surfaced"
|
|
3709
|
+
}
|
|
3710
|
+
],
|
|
3711
|
+
"attestation": {
|
|
3712
|
+
"allowed": true,
|
|
3713
|
+
"host_agent_min_confidence": "high",
|
|
3714
|
+
"human_allowed": true,
|
|
3715
|
+
"evidence_required": [
|
|
3716
|
+
{
|
|
3717
|
+
"field": "rich_post_composer_scope",
|
|
3718
|
+
"description": "Which rich post creation flows are implemented, or the explicit product reason for a text-only composer.",
|
|
3719
|
+
"upload_policy": "upload-with-consent"
|
|
3720
|
+
}
|
|
3721
|
+
]
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
},
|
|
3581
3725
|
{
|
|
3582
3726
|
"id": "android.feed.rich-post-composer-surfaced",
|
|
3583
3727
|
"version": 1,
|
|
@@ -3614,6 +3758,154 @@
|
|
|
3614
3758
|
}
|
|
3615
3759
|
}
|
|
3616
3760
|
},
|
|
3761
|
+
{
|
|
3762
|
+
"id": "typescript.profile.social-counts-from-sdk",
|
|
3763
|
+
"version": 1,
|
|
3764
|
+
"title": "TypeScript profile social counts must come from the SDK",
|
|
3765
|
+
"severity": "warning",
|
|
3766
|
+
"rationale": "Showing Followers/Following labels with placeholder values makes the profile look complete while silently dropping the social graph. social.plus user objects and relationship queries expose follower/following counts or lists.",
|
|
3767
|
+
"applies_when": {
|
|
3768
|
+
"platforms": [
|
|
3769
|
+
"typescript"
|
|
3770
|
+
],
|
|
3771
|
+
"outcomes": [
|
|
3772
|
+
"add-feed",
|
|
3773
|
+
"add-follow",
|
|
3774
|
+
"validate-setup"
|
|
3775
|
+
]
|
|
3776
|
+
},
|
|
3777
|
+
"enforcement": {
|
|
3778
|
+
"deterministic": [
|
|
3779
|
+
{
|
|
3780
|
+
"check": "validator-finding-absent",
|
|
3781
|
+
"finding_rule_id": "typescript.profile.social-counts-from-sdk"
|
|
3782
|
+
}
|
|
3783
|
+
],
|
|
3784
|
+
"attestation": {
|
|
3785
|
+
"allowed": true,
|
|
3786
|
+
"host_agent_min_confidence": "high",
|
|
3787
|
+
"human_allowed": true,
|
|
3788
|
+
"evidence_required": [
|
|
3789
|
+
{
|
|
3790
|
+
"field": "profile_social_counts",
|
|
3791
|
+
"description": "Where follower/following counts are read from AmityUser or relationship live collections, or why the profile intentionally omits them.",
|
|
3792
|
+
"upload_policy": "upload-with-consent"
|
|
3793
|
+
}
|
|
3794
|
+
]
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
{
|
|
3799
|
+
"id": "react-native.profile.social-counts-from-sdk",
|
|
3800
|
+
"version": 1,
|
|
3801
|
+
"title": "React Native profile social counts must come from the SDK",
|
|
3802
|
+
"severity": "warning",
|
|
3803
|
+
"rationale": "Showing Followers/Following labels with placeholder values makes the profile look complete while silently dropping the social graph. social.plus user objects and relationship queries expose follower/following counts or lists.",
|
|
3804
|
+
"applies_when": {
|
|
3805
|
+
"platforms": [
|
|
3806
|
+
"react-native"
|
|
3807
|
+
],
|
|
3808
|
+
"outcomes": [
|
|
3809
|
+
"add-feed",
|
|
3810
|
+
"add-follow",
|
|
3811
|
+
"validate-setup"
|
|
3812
|
+
]
|
|
3813
|
+
},
|
|
3814
|
+
"enforcement": {
|
|
3815
|
+
"deterministic": [
|
|
3816
|
+
{
|
|
3817
|
+
"check": "validator-finding-absent",
|
|
3818
|
+
"finding_rule_id": "react-native.profile.social-counts-from-sdk"
|
|
3819
|
+
}
|
|
3820
|
+
],
|
|
3821
|
+
"attestation": {
|
|
3822
|
+
"allowed": true,
|
|
3823
|
+
"host_agent_min_confidence": "high",
|
|
3824
|
+
"human_allowed": true,
|
|
3825
|
+
"evidence_required": [
|
|
3826
|
+
{
|
|
3827
|
+
"field": "profile_social_counts",
|
|
3828
|
+
"description": "Where follower/following counts are read from AmityUser or relationship live collections, or why the profile intentionally omits them.",
|
|
3829
|
+
"upload_policy": "upload-with-consent"
|
|
3830
|
+
}
|
|
3831
|
+
]
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
},
|
|
3835
|
+
{
|
|
3836
|
+
"id": "flutter.profile.social-counts-from-sdk",
|
|
3837
|
+
"version": 1,
|
|
3838
|
+
"title": "Flutter profile social counts must come from the SDK",
|
|
3839
|
+
"severity": "warning",
|
|
3840
|
+
"rationale": "Showing Followers/Following labels with placeholder values makes the profile look complete while silently dropping the social graph. social.plus user objects and relationship queries expose follower/following counts or lists.",
|
|
3841
|
+
"applies_when": {
|
|
3842
|
+
"platforms": [
|
|
3843
|
+
"flutter"
|
|
3844
|
+
],
|
|
3845
|
+
"outcomes": [
|
|
3846
|
+
"add-feed",
|
|
3847
|
+
"add-follow",
|
|
3848
|
+
"validate-setup"
|
|
3849
|
+
]
|
|
3850
|
+
},
|
|
3851
|
+
"enforcement": {
|
|
3852
|
+
"deterministic": [
|
|
3853
|
+
{
|
|
3854
|
+
"check": "validator-finding-absent",
|
|
3855
|
+
"finding_rule_id": "flutter.profile.social-counts-from-sdk"
|
|
3856
|
+
}
|
|
3857
|
+
],
|
|
3858
|
+
"attestation": {
|
|
3859
|
+
"allowed": true,
|
|
3860
|
+
"host_agent_min_confidence": "high",
|
|
3861
|
+
"human_allowed": true,
|
|
3862
|
+
"evidence_required": [
|
|
3863
|
+
{
|
|
3864
|
+
"field": "profile_social_counts",
|
|
3865
|
+
"description": "Where follower/following counts are read from AmityUser or relationship live collections, or why the profile intentionally omits them.",
|
|
3866
|
+
"upload_policy": "upload-with-consent"
|
|
3867
|
+
}
|
|
3868
|
+
]
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
},
|
|
3872
|
+
{
|
|
3873
|
+
"id": "ios.profile.social-counts-from-sdk",
|
|
3874
|
+
"version": 1,
|
|
3875
|
+
"title": "iOS profile social counts must come from the SDK",
|
|
3876
|
+
"severity": "warning",
|
|
3877
|
+
"rationale": "Showing Followers/Following labels with placeholder values makes the profile look complete while silently dropping the social graph. social.plus user objects and relationship queries expose follower/following counts or lists.",
|
|
3878
|
+
"applies_when": {
|
|
3879
|
+
"platforms": [
|
|
3880
|
+
"ios"
|
|
3881
|
+
],
|
|
3882
|
+
"outcomes": [
|
|
3883
|
+
"add-feed",
|
|
3884
|
+
"add-follow",
|
|
3885
|
+
"validate-setup"
|
|
3886
|
+
]
|
|
3887
|
+
},
|
|
3888
|
+
"enforcement": {
|
|
3889
|
+
"deterministic": [
|
|
3890
|
+
{
|
|
3891
|
+
"check": "validator-finding-absent",
|
|
3892
|
+
"finding_rule_id": "ios.profile.social-counts-from-sdk"
|
|
3893
|
+
}
|
|
3894
|
+
],
|
|
3895
|
+
"attestation": {
|
|
3896
|
+
"allowed": true,
|
|
3897
|
+
"host_agent_min_confidence": "high",
|
|
3898
|
+
"human_allowed": true,
|
|
3899
|
+
"evidence_required": [
|
|
3900
|
+
{
|
|
3901
|
+
"field": "profile_social_counts",
|
|
3902
|
+
"description": "Where follower/following counts are read from AmityUser or relationship live collections, or why the profile intentionally omits them.",
|
|
3903
|
+
"upload_policy": "upload-with-consent"
|
|
3904
|
+
}
|
|
3905
|
+
]
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
},
|
|
3617
3909
|
{
|
|
3618
3910
|
"id": "android.profile.social-counts-from-sdk",
|
|
3619
3911
|
"version": 1,
|