@amityco/social-plus-vise 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/rules/feed.yaml CHANGED
@@ -8,10 +8,34 @@
8
8
  "title": "TypeScript feed targets must not be hardcoded literals",
9
9
  "severity": "warning",
10
10
  "rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
11
- "applies_when": { "platforms": ["typescript"], "outcomes": ["add-feed", "validate-setup"] },
11
+ "applies_when": {
12
+ "platforms": [
13
+ "typescript"
14
+ ],
15
+ "outcomes": [
16
+ "add-feed",
17
+ "validate-setup"
18
+ ]
19
+ },
12
20
  "enforcement": {
13
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.feed.target.literal" }],
14
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
21
+ "deterministic": [
22
+ {
23
+ "check": "validator-finding-absent",
24
+ "finding_rule_id": "typescript.feed.target.literal"
25
+ }
26
+ ],
27
+ "attestation": {
28
+ "allowed": true,
29
+ "host_agent_min_confidence": "high",
30
+ "human_allowed": true,
31
+ "evidence_required": [
32
+ {
33
+ "field": "target_source",
34
+ "description": "Where the runtime feed target comes from.",
35
+ "upload_policy": "upload-with-consent"
36
+ }
37
+ ]
38
+ }
15
39
  }
16
40
  },
17
41
  {
@@ -20,10 +44,34 @@
20
44
  "title": "React Native feed targets must not be hardcoded literals",
21
45
  "severity": "warning",
22
46
  "rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
23
- "applies_when": { "platforms": ["react-native"], "outcomes": ["add-feed", "validate-setup"] },
47
+ "applies_when": {
48
+ "platforms": [
49
+ "react-native"
50
+ ],
51
+ "outcomes": [
52
+ "add-feed",
53
+ "validate-setup"
54
+ ]
55
+ },
24
56
  "enforcement": {
25
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.feed.target.literal" }],
26
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
57
+ "deterministic": [
58
+ {
59
+ "check": "validator-finding-absent",
60
+ "finding_rule_id": "react-native.feed.target.literal"
61
+ }
62
+ ],
63
+ "attestation": {
64
+ "allowed": true,
65
+ "host_agent_min_confidence": "high",
66
+ "human_allowed": true,
67
+ "evidence_required": [
68
+ {
69
+ "field": "target_source",
70
+ "description": "Where the runtime feed target comes from.",
71
+ "upload_policy": "upload-with-consent"
72
+ }
73
+ ]
74
+ }
27
75
  }
28
76
  },
29
77
  {
@@ -32,10 +80,34 @@
32
80
  "title": "Android feed targets must not be hardcoded literals",
33
81
  "severity": "warning",
34
82
  "rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
35
- "applies_when": { "platforms": ["android"], "outcomes": ["add-feed", "validate-setup"] },
83
+ "applies_when": {
84
+ "platforms": [
85
+ "android"
86
+ ],
87
+ "outcomes": [
88
+ "add-feed",
89
+ "validate-setup"
90
+ ]
91
+ },
36
92
  "enforcement": {
37
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.feed.target.literal" }],
38
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
93
+ "deterministic": [
94
+ {
95
+ "check": "validator-finding-absent",
96
+ "finding_rule_id": "android.feed.target.literal"
97
+ }
98
+ ],
99
+ "attestation": {
100
+ "allowed": true,
101
+ "host_agent_min_confidence": "high",
102
+ "human_allowed": true,
103
+ "evidence_required": [
104
+ {
105
+ "field": "target_source",
106
+ "description": "Where the runtime feed target comes from.",
107
+ "upload_policy": "upload-with-consent"
108
+ }
109
+ ]
110
+ }
39
111
  }
40
112
  },
41
113
  {
@@ -44,10 +116,34 @@
44
116
  "title": "Flutter feed targets must not be hardcoded literals",
45
117
  "severity": "warning",
46
118
  "rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
47
- "applies_when": { "platforms": ["flutter"], "outcomes": ["add-feed", "validate-setup"] },
119
+ "applies_when": {
120
+ "platforms": [
121
+ "flutter"
122
+ ],
123
+ "outcomes": [
124
+ "add-feed",
125
+ "validate-setup"
126
+ ]
127
+ },
48
128
  "enforcement": {
49
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.feed.target.literal" }],
50
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
129
+ "deterministic": [
130
+ {
131
+ "check": "validator-finding-absent",
132
+ "finding_rule_id": "flutter.feed.target.literal"
133
+ }
134
+ ],
135
+ "attestation": {
136
+ "allowed": true,
137
+ "host_agent_min_confidence": "high",
138
+ "human_allowed": true,
139
+ "evidence_required": [
140
+ {
141
+ "field": "target_source",
142
+ "description": "Where the runtime feed target comes from.",
143
+ "upload_policy": "upload-with-consent"
144
+ }
145
+ ]
146
+ }
51
147
  }
52
148
  },
53
149
  {
@@ -56,10 +152,34 @@
56
152
  "title": "iOS feed targets must not be hardcoded literals",
57
153
  "severity": "warning",
58
154
  "rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
59
- "applies_when": { "platforms": ["ios"], "outcomes": ["add-feed", "validate-setup"] },
155
+ "applies_when": {
156
+ "platforms": [
157
+ "ios"
158
+ ],
159
+ "outcomes": [
160
+ "add-feed",
161
+ "validate-setup"
162
+ ]
163
+ },
60
164
  "enforcement": {
61
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.feed.target.literal" }],
62
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
165
+ "deterministic": [
166
+ {
167
+ "check": "validator-finding-absent",
168
+ "finding_rule_id": "ios.feed.target.literal"
169
+ }
170
+ ],
171
+ "attestation": {
172
+ "allowed": true,
173
+ "host_agent_min_confidence": "high",
174
+ "human_allowed": true,
175
+ "evidence_required": [
176
+ {
177
+ "field": "target_source",
178
+ "description": "Where the runtime feed target comes from.",
179
+ "upload_policy": "upload-with-consent"
180
+ }
181
+ ]
182
+ }
63
183
  }
64
184
  },
65
185
  {
@@ -68,10 +188,35 @@
68
188
  "title": "Android feed UI must render loading, empty, and error states",
69
189
  "severity": "warning",
70
190
  "rationale": "Live collections take time to load, can be empty, and can error. A feed that only renders the success path looks broken to users on slow networks or fresh accounts.",
71
- "applies_when": { "platforms": ["android"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
191
+ "applies_when": {
192
+ "platforms": [
193
+ "android"
194
+ ],
195
+ "outcomes": [
196
+ "add-feed",
197
+ "setup-live-data",
198
+ "validate-setup"
199
+ ]
200
+ },
72
201
  "enforcement": {
73
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.feed.ui-states-present" }],
74
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading, empty, and error states are rendered (or why they aren't needed).", "upload_policy": "upload-with-consent" }] }
202
+ "deterministic": [
203
+ {
204
+ "check": "validator-finding-absent",
205
+ "finding_rule_id": "android.feed.ui-states-present"
206
+ }
207
+ ],
208
+ "attestation": {
209
+ "allowed": true,
210
+ "host_agent_min_confidence": "high",
211
+ "human_allowed": true,
212
+ "evidence_required": [
213
+ {
214
+ "field": "state_handling",
215
+ "description": "Where loading, empty, and error states are rendered (or why they aren't needed).",
216
+ "upload_policy": "upload-with-consent"
217
+ }
218
+ ]
219
+ }
75
220
  }
76
221
  },
77
222
  {
@@ -80,10 +225,35 @@
80
225
  "title": "Flutter feed UI must render loading, empty, and error states",
81
226
  "severity": "warning",
82
227
  "rationale": "Live streams take time to load, can be empty, and can error. A feed widget that only renders posts looks broken on slow networks or fresh accounts.",
83
- "applies_when": { "platforms": ["flutter"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
228
+ "applies_when": {
229
+ "platforms": [
230
+ "flutter"
231
+ ],
232
+ "outcomes": [
233
+ "add-feed",
234
+ "setup-live-data",
235
+ "validate-setup"
236
+ ]
237
+ },
84
238
  "enforcement": {
85
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.feed.ui-states-present" }],
86
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (CircularProgressIndicator), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
239
+ "deterministic": [
240
+ {
241
+ "check": "validator-finding-absent",
242
+ "finding_rule_id": "flutter.feed.ui-states-present"
243
+ }
244
+ ],
245
+ "attestation": {
246
+ "allowed": true,
247
+ "host_agent_min_confidence": "high",
248
+ "human_allowed": true,
249
+ "evidence_required": [
250
+ {
251
+ "field": "state_handling",
252
+ "description": "Where loading (CircularProgressIndicator), empty, and error states are rendered.",
253
+ "upload_policy": "upload-with-consent"
254
+ }
255
+ ]
256
+ }
87
257
  }
88
258
  },
89
259
  {
@@ -92,10 +262,35 @@
92
262
  "title": "iOS feed UI must render loading, empty, and error states",
93
263
  "severity": "warning",
94
264
  "rationale": "Live collections take time to load, can be empty, and can error. A feed view that only renders the success path looks broken on slow networks or fresh accounts.",
95
- "applies_when": { "platforms": ["ios"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
265
+ "applies_when": {
266
+ "platforms": [
267
+ "ios"
268
+ ],
269
+ "outcomes": [
270
+ "add-feed",
271
+ "setup-live-data",
272
+ "validate-setup"
273
+ ]
274
+ },
96
275
  "enforcement": {
97
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.feed.ui-states-present" }],
98
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (ProgressView), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
276
+ "deterministic": [
277
+ {
278
+ "check": "validator-finding-absent",
279
+ "finding_rule_id": "ios.feed.ui-states-present"
280
+ }
281
+ ],
282
+ "attestation": {
283
+ "allowed": true,
284
+ "host_agent_min_confidence": "high",
285
+ "human_allowed": true,
286
+ "evidence_required": [
287
+ {
288
+ "field": "state_handling",
289
+ "description": "Where loading (ProgressView), empty, and error states are rendered.",
290
+ "upload_policy": "upload-with-consent"
291
+ }
292
+ ]
293
+ }
99
294
  }
100
295
  },
101
296
  {
@@ -104,10 +299,35 @@
104
299
  "title": "TypeScript feed UI must render loading, empty, and error states",
105
300
  "severity": "warning",
106
301
  "rationale": "Live subscriptions take time to load, can be empty, and can error. A component that only renders posts looks broken on slow networks or fresh accounts.",
107
- "applies_when": { "platforms": ["typescript"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
302
+ "applies_when": {
303
+ "platforms": [
304
+ "typescript"
305
+ ],
306
+ "outcomes": [
307
+ "add-feed",
308
+ "setup-live-data",
309
+ "validate-setup"
310
+ ]
311
+ },
108
312
  "enforcement": {
109
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.feed.ui-states-present" }],
110
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading, empty, and error states are rendered (e.g., isLoading state, empty fallback, error boundary).", "upload_policy": "upload-with-consent" }] }
313
+ "deterministic": [
314
+ {
315
+ "check": "validator-finding-absent",
316
+ "finding_rule_id": "typescript.feed.ui-states-present"
317
+ }
318
+ ],
319
+ "attestation": {
320
+ "allowed": true,
321
+ "host_agent_min_confidence": "high",
322
+ "human_allowed": true,
323
+ "evidence_required": [
324
+ {
325
+ "field": "state_handling",
326
+ "description": "Where loading, empty, and error states are rendered (e.g., isLoading state, empty fallback, error boundary).",
327
+ "upload_policy": "upload-with-consent"
328
+ }
329
+ ]
330
+ }
111
331
  }
112
332
  },
113
333
  {
@@ -116,10 +336,2045 @@
116
336
  "title": "React Native feed UI must render loading, empty, and error states",
117
337
  "severity": "warning",
118
338
  "rationale": "Live subscriptions take time to load, can be empty, and can error. A component that only renders posts looks broken on slow networks or fresh accounts.",
119
- "applies_when": { "platforms": ["react-native"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
339
+ "applies_when": {
340
+ "platforms": [
341
+ "react-native"
342
+ ],
343
+ "outcomes": [
344
+ "add-feed",
345
+ "setup-live-data",
346
+ "validate-setup"
347
+ ]
348
+ },
120
349
  "enforcement": {
121
- "deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.feed.ui-states-present" }],
122
- "attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (ActivityIndicator), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
350
+ "deterministic": [
351
+ {
352
+ "check": "validator-finding-absent",
353
+ "finding_rule_id": "react-native.feed.ui-states-present"
354
+ }
355
+ ],
356
+ "attestation": {
357
+ "allowed": true,
358
+ "host_agent_min_confidence": "high",
359
+ "human_allowed": true,
360
+ "evidence_required": [
361
+ {
362
+ "field": "state_handling",
363
+ "description": "Where loading (ActivityIndicator), empty, and error states are rendered.",
364
+ "upload_policy": "upload-with-consent"
365
+ }
366
+ ]
367
+ }
368
+ }
369
+ },
370
+ {
371
+ "id": "typescript.feed.pagination-wired",
372
+ "version": 1,
373
+ "title": "TypeScript feed must wire pagination",
374
+ "severity": "warning",
375
+ "rationale": "A live feed without pagination loads all content on mount, which freezes screens and wastes bandwidth in large communities. Wire hasMore/loadMore, useInfiniteQuery, or equivalent.",
376
+ "applies_when": {
377
+ "platforms": [
378
+ "typescript"
379
+ ],
380
+ "outcomes": [
381
+ "add-feed",
382
+ "setup-live-data",
383
+ "validate-setup"
384
+ ]
385
+ },
386
+ "enforcement": {
387
+ "deterministic": [
388
+ {
389
+ "check": "validator-finding-absent",
390
+ "finding_rule_id": "typescript.feed.pagination-wired"
391
+ }
392
+ ],
393
+ "attestation": {
394
+ "allowed": true,
395
+ "host_agent_min_confidence": "high",
396
+ "human_allowed": true,
397
+ "evidence_required": [
398
+ {
399
+ "field": "pagination_owner",
400
+ "description": "Where pagination is handled (hook, utility, or SDK-managed infinite query).",
401
+ "upload_policy": "upload-with-consent"
402
+ }
403
+ ]
404
+ }
405
+ }
406
+ },
407
+ {
408
+ "id": "react-native.feed.pagination-wired",
409
+ "version": 1,
410
+ "title": "React Native feed must wire pagination",
411
+ "severity": "warning",
412
+ "rationale": "A live feed without pagination loads all content on mount, which freezes mobile screens. Wire FlatList.onEndReached, hasMore/loadMore, or useInfiniteQuery.",
413
+ "applies_when": {
414
+ "platforms": [
415
+ "react-native"
416
+ ],
417
+ "outcomes": [
418
+ "add-feed",
419
+ "setup-live-data",
420
+ "validate-setup"
421
+ ]
422
+ },
423
+ "enforcement": {
424
+ "deterministic": [
425
+ {
426
+ "check": "validator-finding-absent",
427
+ "finding_rule_id": "react-native.feed.pagination-wired"
428
+ }
429
+ ],
430
+ "attestation": {
431
+ "allowed": true,
432
+ "host_agent_min_confidence": "high",
433
+ "human_allowed": true,
434
+ "evidence_required": [
435
+ {
436
+ "field": "pagination_owner",
437
+ "description": "Where pagination is handled (FlatList onEndReached, hook, or SDK-managed query).",
438
+ "upload_policy": "upload-with-consent"
439
+ }
440
+ ]
441
+ }
442
+ }
443
+ },
444
+ {
445
+ "id": "android.feed.pagination-wired",
446
+ "version": 1,
447
+ "title": "Android feed must wire pagination",
448
+ "severity": "warning",
449
+ "rationale": "A live feed without pagination loads all content on mount, which freezes mobile screens. Wire PagingData, LazyColumn scroll trigger, or loadMore/nextPage.",
450
+ "applies_when": {
451
+ "platforms": [
452
+ "android"
453
+ ],
454
+ "outcomes": [
455
+ "add-feed",
456
+ "setup-live-data",
457
+ "validate-setup"
458
+ ]
459
+ },
460
+ "enforcement": {
461
+ "deterministic": [
462
+ {
463
+ "check": "validator-finding-absent",
464
+ "finding_rule_id": "android.feed.pagination-wired"
465
+ }
466
+ ],
467
+ "attestation": {
468
+ "allowed": true,
469
+ "host_agent_min_confidence": "high",
470
+ "human_allowed": true,
471
+ "evidence_required": [
472
+ {
473
+ "field": "pagination_owner",
474
+ "description": "Where pagination is handled (PagingData, LazyColumn trigger, or manual loadMore).",
475
+ "upload_policy": "upload-with-consent"
476
+ }
477
+ ]
478
+ }
479
+ }
480
+ },
481
+ {
482
+ "id": "flutter.feed.pagination-wired",
483
+ "version": 1,
484
+ "title": "Flutter feed must wire pagination",
485
+ "severity": "warning",
486
+ "rationale": "A live feed without pagination loads all content on mount, which freezes mobile screens. Wire ScrollController, ListView.builder with loadMore, or PagingController.",
487
+ "applies_when": {
488
+ "platforms": [
489
+ "flutter"
490
+ ],
491
+ "outcomes": [
492
+ "add-feed",
493
+ "setup-live-data",
494
+ "validate-setup"
495
+ ]
496
+ },
497
+ "enforcement": {
498
+ "deterministic": [
499
+ {
500
+ "check": "validator-finding-absent",
501
+ "finding_rule_id": "flutter.feed.pagination-wired"
502
+ }
503
+ ],
504
+ "attestation": {
505
+ "allowed": true,
506
+ "host_agent_min_confidence": "high",
507
+ "human_allowed": true,
508
+ "evidence_required": [
509
+ {
510
+ "field": "pagination_owner",
511
+ "description": "Where pagination is handled (ScrollController, PagingController, or manual loadMore).",
512
+ "upload_policy": "upload-with-consent"
513
+ }
514
+ ]
515
+ }
516
+ }
517
+ },
518
+ {
519
+ "id": "ios.feed.pagination-wired",
520
+ "version": 1,
521
+ "title": "iOS feed must wire pagination",
522
+ "severity": "warning",
523
+ "rationale": "A live feed without pagination loads all content on mount, which freezes mobile screens. Wire loadMore/nextPage, onAppear trigger, or UITableViewDataSourcePrefetching.",
524
+ "applies_when": {
525
+ "platforms": [
526
+ "ios"
527
+ ],
528
+ "outcomes": [
529
+ "add-feed",
530
+ "setup-live-data",
531
+ "validate-setup"
532
+ ]
533
+ },
534
+ "enforcement": {
535
+ "deterministic": [
536
+ {
537
+ "check": "validator-finding-absent",
538
+ "finding_rule_id": "ios.feed.pagination-wired"
539
+ }
540
+ ],
541
+ "attestation": {
542
+ "allowed": true,
543
+ "host_agent_min_confidence": "high",
544
+ "human_allowed": true,
545
+ "evidence_required": [
546
+ {
547
+ "field": "pagination_owner",
548
+ "description": "Where pagination is handled (onAppear, willDisplay, or manual loadMore/nextPage).",
549
+ "upload_policy": "upload-with-consent"
550
+ }
551
+ ]
552
+ }
553
+ }
554
+ },
555
+ {
556
+ "id": "typescript.feed.moderation-affordance-present",
557
+ "version": 1,
558
+ "title": "TypeScript feed must include a moderation affordance",
559
+ "severity": "warning",
560
+ "rationale": "Production UGC needs at least one user-visible moderation affordance: report, block, mute, or hide. A feed with posting but no moderation path is a legal and trust risk.",
561
+ "applies_when": {
562
+ "platforms": [
563
+ "typescript"
564
+ ],
565
+ "outcomes": [
566
+ "add-feed",
567
+ "validate-setup"
568
+ ]
569
+ },
570
+ "enforcement": {
571
+ "deterministic": [
572
+ {
573
+ "check": "validator-finding-absent",
574
+ "finding_rule_id": "typescript.feed.moderation-affordance-present"
575
+ }
576
+ ],
577
+ "attestation": {
578
+ "allowed": true,
579
+ "host_agent_min_confidence": "medium",
580
+ "human_allowed": true,
581
+ "evidence_required": [
582
+ {
583
+ "field": "moderation_surface",
584
+ "description": "Where the moderation flow lives (report button, admin dashboard, or centralized module).",
585
+ "upload_policy": "upload-with-consent"
586
+ }
587
+ ]
588
+ }
589
+ }
590
+ },
591
+ {
592
+ "id": "react-native.feed.moderation-affordance-present",
593
+ "version": 1,
594
+ "title": "React Native feed must include a moderation affordance",
595
+ "severity": "warning",
596
+ "rationale": "Production UGC needs at least one user-visible moderation affordance: report, block, mute, or hide. A feed with posting but no moderation path is a legal and trust risk.",
597
+ "applies_when": {
598
+ "platforms": [
599
+ "react-native"
600
+ ],
601
+ "outcomes": [
602
+ "add-feed",
603
+ "validate-setup"
604
+ ]
605
+ },
606
+ "enforcement": {
607
+ "deterministic": [
608
+ {
609
+ "check": "validator-finding-absent",
610
+ "finding_rule_id": "react-native.feed.moderation-affordance-present"
611
+ }
612
+ ],
613
+ "attestation": {
614
+ "allowed": true,
615
+ "host_agent_min_confidence": "medium",
616
+ "human_allowed": true,
617
+ "evidence_required": [
618
+ {
619
+ "field": "moderation_surface",
620
+ "description": "Where the moderation flow lives (report button, admin dashboard, or centralized module).",
621
+ "upload_policy": "upload-with-consent"
622
+ }
623
+ ]
624
+ }
625
+ }
626
+ },
627
+ {
628
+ "id": "android.feed.moderation-affordance-present",
629
+ "version": 1,
630
+ "title": "Android feed must include a moderation affordance",
631
+ "severity": "warning",
632
+ "rationale": "Production UGC needs at least one user-visible moderation affordance: report, block, mute, or hide. A feed with posting but no moderation path is a legal and trust risk.",
633
+ "applies_when": {
634
+ "platforms": [
635
+ "android"
636
+ ],
637
+ "outcomes": [
638
+ "add-feed",
639
+ "validate-setup"
640
+ ]
641
+ },
642
+ "enforcement": {
643
+ "deterministic": [
644
+ {
645
+ "check": "validator-finding-absent",
646
+ "finding_rule_id": "android.feed.moderation-affordance-present"
647
+ }
648
+ ],
649
+ "attestation": {
650
+ "allowed": true,
651
+ "host_agent_min_confidence": "medium",
652
+ "human_allowed": true,
653
+ "evidence_required": [
654
+ {
655
+ "field": "moderation_surface",
656
+ "description": "Where the moderation flow lives (report button, admin dashboard, or centralized module).",
657
+ "upload_policy": "upload-with-consent"
658
+ }
659
+ ]
660
+ }
661
+ }
662
+ },
663
+ {
664
+ "id": "flutter.feed.moderation-affordance-present",
665
+ "version": 1,
666
+ "title": "Flutter feed must include a moderation affordance",
667
+ "severity": "warning",
668
+ "rationale": "Production UGC needs at least one user-visible moderation affordance: report, block, mute, or hide. A feed with posting but no moderation path is a legal and trust risk.",
669
+ "applies_when": {
670
+ "platforms": [
671
+ "flutter"
672
+ ],
673
+ "outcomes": [
674
+ "add-feed",
675
+ "validate-setup"
676
+ ]
677
+ },
678
+ "enforcement": {
679
+ "deterministic": [
680
+ {
681
+ "check": "validator-finding-absent",
682
+ "finding_rule_id": "flutter.feed.moderation-affordance-present"
683
+ }
684
+ ],
685
+ "attestation": {
686
+ "allowed": true,
687
+ "host_agent_min_confidence": "medium",
688
+ "human_allowed": true,
689
+ "evidence_required": [
690
+ {
691
+ "field": "moderation_surface",
692
+ "description": "Where the moderation flow lives (report button, admin dashboard, or centralized module).",
693
+ "upload_policy": "upload-with-consent"
694
+ }
695
+ ]
696
+ }
697
+ }
698
+ },
699
+ {
700
+ "id": "ios.feed.moderation-affordance-present",
701
+ "version": 1,
702
+ "title": "iOS feed must include a moderation affordance",
703
+ "severity": "warning",
704
+ "rationale": "Production UGC needs at least one user-visible moderation affordance: report, block, mute, or hide. A feed with posting but no moderation path is a legal and trust risk.",
705
+ "applies_when": {
706
+ "platforms": [
707
+ "ios"
708
+ ],
709
+ "outcomes": [
710
+ "add-feed",
711
+ "validate-setup"
712
+ ]
713
+ },
714
+ "enforcement": {
715
+ "deterministic": [
716
+ {
717
+ "check": "validator-finding-absent",
718
+ "finding_rule_id": "ios.feed.moderation-affordance-present"
719
+ }
720
+ ],
721
+ "attestation": {
722
+ "allowed": true,
723
+ "host_agent_min_confidence": "medium",
724
+ "human_allowed": true,
725
+ "evidence_required": [
726
+ {
727
+ "field": "moderation_surface",
728
+ "description": "Where the moderation flow lives (report button, admin dashboard, or centralized module).",
729
+ "upload_policy": "upload-with-consent"
730
+ }
731
+ ]
732
+ }
733
+ }
734
+ },
735
+ {
736
+ "id": "typescript.posts.status-filter-applied",
737
+ "version": 1,
738
+ "title": "TypeScript post queries must filter by status",
739
+ "severity": "warning",
740
+ "rationale": "TypeScript queries must set isDeleted: false or feedType: 'published' in the query payload to filter out deleted content. Unfiltered queries may expose moderated content.",
741
+ "applies_when": {
742
+ "platforms": [
743
+ "typescript"
744
+ ],
745
+ "outcomes": [
746
+ "add-feed",
747
+ "add-moderation",
748
+ "validate-setup"
749
+ ]
750
+ },
751
+ "enforcement": {
752
+ "deterministic": [
753
+ {
754
+ "check": "validator-finding-absent",
755
+ "finding_rule_id": "typescript.posts.status-filter-applied"
756
+ }
757
+ ],
758
+ "attestation": {
759
+ "allowed": true,
760
+ "host_agent_min_confidence": "high",
761
+ "human_allowed": true,
762
+ "evidence_required": [
763
+ {
764
+ "field": "status_filter",
765
+ "description": "Why unfiltered is safe or what filter is applied.",
766
+ "upload_policy": "upload-with-consent"
767
+ }
768
+ ]
769
+ }
770
+ }
771
+ },
772
+ {
773
+ "id": "react-native.posts.status-filter-applied",
774
+ "version": 1,
775
+ "title": "React Native post queries must filter by status",
776
+ "severity": "warning",
777
+ "rationale": "React Native queries must set isDeleted: false or feedType: 'published' in the query payload to filter out deleted content. Unfiltered queries may expose moderated content.",
778
+ "applies_when": {
779
+ "platforms": [
780
+ "react-native"
781
+ ],
782
+ "outcomes": [
783
+ "add-feed",
784
+ "add-moderation",
785
+ "validate-setup"
786
+ ]
787
+ },
788
+ "enforcement": {
789
+ "deterministic": [
790
+ {
791
+ "check": "validator-finding-absent",
792
+ "finding_rule_id": "react-native.posts.status-filter-applied"
793
+ }
794
+ ],
795
+ "attestation": {
796
+ "allowed": true,
797
+ "host_agent_min_confidence": "high",
798
+ "human_allowed": true,
799
+ "evidence_required": [
800
+ {
801
+ "field": "status_filter",
802
+ "description": "Why unfiltered is safe or what filter is applied.",
803
+ "upload_policy": "upload-with-consent"
804
+ }
805
+ ]
806
+ }
807
+ }
808
+ },
809
+ {
810
+ "id": "android.posts.status-filter-applied",
811
+ "version": 1,
812
+ "title": "Android post queries must filter by status",
813
+ "severity": "warning",
814
+ "rationale": "Android queries must use feedType(AmityFeedType.PUBLISHED) or includeDeleted(false) to filter out deleted/flagged content. Unfiltered queries may expose moderated content.",
815
+ "applies_when": {
816
+ "platforms": [
817
+ "android"
818
+ ],
819
+ "outcomes": [
820
+ "add-feed",
821
+ "add-moderation",
822
+ "validate-setup"
823
+ ]
824
+ },
825
+ "enforcement": {
826
+ "deterministic": [
827
+ {
828
+ "check": "validator-finding-absent",
829
+ "finding_rule_id": "android.posts.status-filter-applied"
830
+ }
831
+ ],
832
+ "attestation": {
833
+ "allowed": true,
834
+ "host_agent_min_confidence": "high",
835
+ "human_allowed": true,
836
+ "evidence_required": [
837
+ {
838
+ "field": "status_filter",
839
+ "description": "Why unfiltered is safe or what filter is applied.",
840
+ "upload_policy": "upload-with-consent"
841
+ }
842
+ ]
843
+ }
844
+ }
845
+ },
846
+ {
847
+ "id": "flutter.posts.status-filter-applied",
848
+ "version": 1,
849
+ "title": "Flutter post queries must filter by status",
850
+ "severity": "warning",
851
+ "rationale": "Flutter queries must use feedType: AmityFeedType.PUBLISHED or includeDeleted(false) to ensure deleted content is filtered. Unfiltered queries may expose moderated content.",
852
+ "applies_when": {
853
+ "platforms": [
854
+ "flutter"
855
+ ],
856
+ "outcomes": [
857
+ "add-feed",
858
+ "add-moderation",
859
+ "validate-setup"
860
+ ]
861
+ },
862
+ "enforcement": {
863
+ "deterministic": [
864
+ {
865
+ "check": "validator-finding-absent",
866
+ "finding_rule_id": "flutter.posts.status-filter-applied"
867
+ }
868
+ ],
869
+ "attestation": {
870
+ "allowed": true,
871
+ "host_agent_min_confidence": "high",
872
+ "human_allowed": true,
873
+ "evidence_required": [
874
+ {
875
+ "field": "status_filter",
876
+ "description": "Why unfiltered is safe or what filter is applied.",
877
+ "upload_policy": "upload-with-consent"
878
+ }
879
+ ]
880
+ }
881
+ }
882
+ },
883
+ {
884
+ "id": "ios.posts.status-filter-applied",
885
+ "version": 1,
886
+ "title": "iOS post queries must filter by status",
887
+ "severity": "warning",
888
+ "rationale": "iOS queries must specify feedType: .published or includeDeleted: false to ensure deleted/flagged content is hidden. Unfiltered queries may expose moderated content.",
889
+ "applies_when": {
890
+ "platforms": [
891
+ "ios"
892
+ ],
893
+ "outcomes": [
894
+ "add-feed",
895
+ "add-moderation",
896
+ "validate-setup"
897
+ ]
898
+ },
899
+ "enforcement": {
900
+ "deterministic": [
901
+ {
902
+ "check": "validator-finding-absent",
903
+ "finding_rule_id": "ios.posts.status-filter-applied"
904
+ }
905
+ ],
906
+ "attestation": {
907
+ "allowed": true,
908
+ "host_agent_min_confidence": "high",
909
+ "human_allowed": true,
910
+ "evidence_required": [
911
+ {
912
+ "field": "status_filter",
913
+ "description": "Why unfiltered is safe or what filter is applied.",
914
+ "upload_policy": "upload-with-consent"
915
+ }
916
+ ]
917
+ }
918
+ }
919
+ },
920
+ {
921
+ "id": "typescript.pagination.cursor-opaque",
922
+ "version": 1,
923
+ "title": "Pagination cursors for TypeScript must be opaque tokens, not numeric offsets",
924
+ "severity": "warning",
925
+ "rationale": "TypeScript apps must pass the exact opaque string returned by nextPageToken or use the provided nextPage() method instead of computing numeric offsets. Computing page * size will result in API errors.",
926
+ "applies_when": {
927
+ "platforms": [
928
+ "typescript"
929
+ ],
930
+ "outcomes": [
931
+ "add-feed",
932
+ "add-comments",
933
+ "add-chat",
934
+ "validate-setup"
935
+ ]
936
+ },
937
+ "enforcement": {
938
+ "deterministic": [
939
+ {
940
+ "check": "validator-finding-absent",
941
+ "finding_rule_id": "typescript.pagination.cursor-opaque"
942
+ }
943
+ ],
944
+ "attestation": {
945
+ "allowed": true,
946
+ "host_agent_min_confidence": "high",
947
+ "human_allowed": true,
948
+ "evidence_required": [
949
+ {
950
+ "field": "cursor_source",
951
+ "description": "Where the pagination cursor comes from.",
952
+ "upload_policy": "upload-with-consent"
953
+ }
954
+ ]
955
+ }
956
+ }
957
+ },
958
+ {
959
+ "id": "react-native.pagination.cursor-opaque",
960
+ "version": 1,
961
+ "title": "Pagination cursors for React Native must be opaque tokens, not numeric offsets",
962
+ "severity": "warning",
963
+ "rationale": "React Native apps must pass the exact opaque string returned by nextPageToken or use the provided nextPage() method instead of computing numeric offsets. Computing page * size will result in API errors.",
964
+ "applies_when": {
965
+ "platforms": [
966
+ "react-native"
967
+ ],
968
+ "outcomes": [
969
+ "add-feed",
970
+ "add-comments",
971
+ "add-chat",
972
+ "validate-setup"
973
+ ]
974
+ },
975
+ "enforcement": {
976
+ "deterministic": [
977
+ {
978
+ "check": "validator-finding-absent",
979
+ "finding_rule_id": "react-native.pagination.cursor-opaque"
980
+ }
981
+ ],
982
+ "attestation": {
983
+ "allowed": true,
984
+ "host_agent_min_confidence": "high",
985
+ "human_allowed": true,
986
+ "evidence_required": [
987
+ {
988
+ "field": "cursor_source",
989
+ "description": "Where the pagination cursor comes from.",
990
+ "upload_policy": "upload-with-consent"
991
+ }
992
+ ]
993
+ }
994
+ }
995
+ },
996
+ {
997
+ "id": "android.pagination.cursor-opaque",
998
+ "version": 1,
999
+ "title": "Pagination cursors for Android must be opaque tokens, not numeric offsets",
1000
+ "severity": "warning",
1001
+ "rationale": "Android apps must pass the exact opaque string returned by Amity's nextPageToken rather than computing numeric offsets. Constructing page * size will result in 400 errors or silent failures.",
1002
+ "applies_when": {
1003
+ "platforms": [
1004
+ "android"
1005
+ ],
1006
+ "outcomes": [
1007
+ "add-feed",
1008
+ "add-comments",
1009
+ "add-chat",
1010
+ "validate-setup"
1011
+ ]
1012
+ },
1013
+ "enforcement": {
1014
+ "deterministic": [
1015
+ {
1016
+ "check": "validator-finding-absent",
1017
+ "finding_rule_id": "android.pagination.cursor-opaque"
1018
+ }
1019
+ ],
1020
+ "attestation": {
1021
+ "allowed": true,
1022
+ "host_agent_min_confidence": "high",
1023
+ "human_allowed": true,
1024
+ "evidence_required": [
1025
+ {
1026
+ "field": "cursor_source",
1027
+ "description": "Where the pagination cursor comes from.",
1028
+ "upload_policy": "upload-with-consent"
1029
+ }
1030
+ ]
1031
+ }
1032
+ }
1033
+ },
1034
+ {
1035
+ "id": "flutter.pagination.cursor-opaque",
1036
+ "version": 1,
1037
+ "title": "Pagination cursors for Flutter must be opaque tokens, not numeric offsets",
1038
+ "severity": "warning",
1039
+ "rationale": "Flutter apps must pass the exact opaque token returned by Amity's nextPageToken or use .loadNext() instead of computing numeric offsets. Computing page * size will result in API errors.",
1040
+ "applies_when": {
1041
+ "platforms": [
1042
+ "flutter"
1043
+ ],
1044
+ "outcomes": [
1045
+ "add-feed",
1046
+ "add-comments",
1047
+ "add-chat",
1048
+ "validate-setup"
1049
+ ]
1050
+ },
1051
+ "enforcement": {
1052
+ "deterministic": [
1053
+ {
1054
+ "check": "validator-finding-absent",
1055
+ "finding_rule_id": "flutter.pagination.cursor-opaque"
1056
+ }
1057
+ ],
1058
+ "attestation": {
1059
+ "allowed": true,
1060
+ "host_agent_min_confidence": "high",
1061
+ "human_allowed": true,
1062
+ "evidence_required": [
1063
+ {
1064
+ "field": "cursor_source",
1065
+ "description": "Where the pagination cursor comes from.",
1066
+ "upload_policy": "upload-with-consent"
1067
+ }
1068
+ ]
1069
+ }
1070
+ }
1071
+ },
1072
+ {
1073
+ "id": "ios.pagination.cursor-opaque",
1074
+ "version": 1,
1075
+ "title": "Pagination cursors for iOS must be opaque tokens, not numeric offsets",
1076
+ "severity": "warning",
1077
+ "rationale": "iOS apps must pass the exact opaque String returned by Amity's nextPageToken rather than computing numeric offsets. Constructing offsets will result in API errors or silent failures.",
1078
+ "applies_when": {
1079
+ "platforms": [
1080
+ "ios"
1081
+ ],
1082
+ "outcomes": [
1083
+ "add-feed",
1084
+ "add-comments",
1085
+ "add-chat",
1086
+ "validate-setup"
1087
+ ]
1088
+ },
1089
+ "enforcement": {
1090
+ "deterministic": [
1091
+ {
1092
+ "check": "validator-finding-absent",
1093
+ "finding_rule_id": "ios.pagination.cursor-opaque"
1094
+ }
1095
+ ],
1096
+ "attestation": {
1097
+ "allowed": true,
1098
+ "host_agent_min_confidence": "high",
1099
+ "human_allowed": true,
1100
+ "evidence_required": [
1101
+ {
1102
+ "field": "cursor_source",
1103
+ "description": "Where the pagination cursor comes from.",
1104
+ "upload_policy": "upload-with-consent"
1105
+ }
1106
+ ]
1107
+ }
1108
+ }
1109
+ },
1110
+ {
1111
+ "id": "typescript.posts.parent-child-rendered",
1112
+ "version": 1,
1113
+ "title": "Post rendering for TypeScript must account for child posts (images/videos)",
1114
+ "severity": "warning",
1115
+ "rationale": "TypeScript UI components for Posts must recursively render or inspect post.children. If a component only renders the parent post's data.text, it will silently drop attached images and videos.",
1116
+ "applies_when": {
1117
+ "platforms": [
1118
+ "typescript"
1119
+ ],
1120
+ "outcomes": [
1121
+ "add-feed",
1122
+ "validate-setup"
1123
+ ]
1124
+ },
1125
+ "enforcement": {
1126
+ "deterministic": [
1127
+ {
1128
+ "check": "validator-finding-absent",
1129
+ "finding_rule_id": "typescript.posts.parent-child-rendered"
1130
+ }
1131
+ ],
1132
+ "attestation": {
1133
+ "allowed": true,
1134
+ "host_agent_min_confidence": "high",
1135
+ "human_allowed": true,
1136
+ "evidence_required": [
1137
+ {
1138
+ "field": "media_resolution",
1139
+ "description": "How image/video posts are surfaced.",
1140
+ "upload_policy": "upload-with-consent"
1141
+ }
1142
+ ]
1143
+ }
1144
+ }
1145
+ },
1146
+ {
1147
+ "id": "react-native.posts.parent-child-rendered",
1148
+ "version": 1,
1149
+ "title": "Post rendering for React Native must account for child posts (images/videos)",
1150
+ "severity": "warning",
1151
+ "rationale": "React Native components for Posts must recursively render or inspect post.children. If a component only renders the parent post's data.text, it will silently drop attached images and videos.",
1152
+ "applies_when": {
1153
+ "platforms": [
1154
+ "react-native"
1155
+ ],
1156
+ "outcomes": [
1157
+ "add-feed",
1158
+ "validate-setup"
1159
+ ]
1160
+ },
1161
+ "enforcement": {
1162
+ "deterministic": [
1163
+ {
1164
+ "check": "validator-finding-absent",
1165
+ "finding_rule_id": "react-native.posts.parent-child-rendered"
1166
+ }
1167
+ ],
1168
+ "attestation": {
1169
+ "allowed": true,
1170
+ "host_agent_min_confidence": "high",
1171
+ "human_allowed": true,
1172
+ "evidence_required": [
1173
+ {
1174
+ "field": "media_resolution",
1175
+ "description": "How image/video posts are surfaced.",
1176
+ "upload_policy": "upload-with-consent"
1177
+ }
1178
+ ]
1179
+ }
1180
+ }
1181
+ },
1182
+ {
1183
+ "id": "android.posts.parent-child-rendered",
1184
+ "version": 1,
1185
+ "title": "Post rendering for Android must account for child posts (images/videos)",
1186
+ "severity": "warning",
1187
+ "rationale": "Android Compose/RecyclerView components for Posts must render or inspect the child items (post.children). If a component only renders the parent post's text, it will silently drop attached images and videos.",
1188
+ "applies_when": {
1189
+ "platforms": [
1190
+ "android"
1191
+ ],
1192
+ "outcomes": [
1193
+ "add-feed",
1194
+ "validate-setup"
1195
+ ]
1196
+ },
1197
+ "enforcement": {
1198
+ "deterministic": [
1199
+ {
1200
+ "check": "validator-finding-absent",
1201
+ "finding_rule_id": "android.posts.parent-child-rendered"
1202
+ }
1203
+ ],
1204
+ "attestation": {
1205
+ "allowed": true,
1206
+ "host_agent_min_confidence": "high",
1207
+ "human_allowed": true,
1208
+ "evidence_required": [
1209
+ {
1210
+ "field": "media_resolution",
1211
+ "description": "How image/video posts are surfaced.",
1212
+ "upload_policy": "upload-with-consent"
1213
+ }
1214
+ ]
1215
+ }
1216
+ }
1217
+ },
1218
+ {
1219
+ "id": "flutter.posts.parent-child-rendered",
1220
+ "version": 1,
1221
+ "title": "Post rendering for Flutter must account for child posts (images/videos)",
1222
+ "severity": "warning",
1223
+ "rationale": "Flutter Widgets for Posts must render or inspect the child posts (post.children). If a widget only renders the parent post's data.text, it will silently drop attached images and videos.",
1224
+ "applies_when": {
1225
+ "platforms": [
1226
+ "flutter"
1227
+ ],
1228
+ "outcomes": [
1229
+ "add-feed",
1230
+ "validate-setup"
1231
+ ]
1232
+ },
1233
+ "enforcement": {
1234
+ "deterministic": [
1235
+ {
1236
+ "check": "validator-finding-absent",
1237
+ "finding_rule_id": "flutter.posts.parent-child-rendered"
1238
+ }
1239
+ ],
1240
+ "attestation": {
1241
+ "allowed": true,
1242
+ "host_agent_min_confidence": "high",
1243
+ "human_allowed": true,
1244
+ "evidence_required": [
1245
+ {
1246
+ "field": "media_resolution",
1247
+ "description": "How image/video posts are surfaced.",
1248
+ "upload_policy": "upload-with-consent"
1249
+ }
1250
+ ]
1251
+ }
1252
+ }
1253
+ },
1254
+ {
1255
+ "id": "ios.posts.parent-child-rendered",
1256
+ "version": 1,
1257
+ "title": "Post rendering for iOS must account for child posts (images/videos)",
1258
+ "severity": "warning",
1259
+ "rationale": "iOS SwiftUI/UIKit views for Posts must inspect the child posts (post.children). If a view only renders the parent post's text, it will silently drop attached images and videos.",
1260
+ "applies_when": {
1261
+ "platforms": [
1262
+ "ios"
1263
+ ],
1264
+ "outcomes": [
1265
+ "add-feed",
1266
+ "validate-setup"
1267
+ ]
1268
+ },
1269
+ "enforcement": {
1270
+ "deterministic": [
1271
+ {
1272
+ "check": "validator-finding-absent",
1273
+ "finding_rule_id": "ios.posts.parent-child-rendered"
1274
+ }
1275
+ ],
1276
+ "attestation": {
1277
+ "allowed": true,
1278
+ "host_agent_min_confidence": "high",
1279
+ "human_allowed": true,
1280
+ "evidence_required": [
1281
+ {
1282
+ "field": "media_resolution",
1283
+ "description": "How image/video posts are surfaced.",
1284
+ "upload_policy": "upload-with-consent"
1285
+ }
1286
+ ]
1287
+ }
1288
+ }
1289
+ },
1290
+ {
1291
+ "id": "typescript.feed.target-type-explicit",
1292
+ "version": 1,
1293
+ "title": "Explicit targetType for TypeScript createPost calls must not be hardcoded to COMMUNITY",
1294
+ "severity": "warning",
1295
+ "rationale": "TypeScript feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1296
+ "applies_when": {
1297
+ "platforms": [
1298
+ "typescript"
1299
+ ],
1300
+ "outcomes": [
1301
+ "add-feed",
1302
+ "validate-setup"
1303
+ ]
1304
+ },
1305
+ "enforcement": {
1306
+ "deterministic": [
1307
+ {
1308
+ "check": "validator-finding-absent",
1309
+ "finding_rule_id": "typescript.feed.target-type-explicit"
1310
+ }
1311
+ ],
1312
+ "attestation": {
1313
+ "allowed": true,
1314
+ "host_agent_min_confidence": "high",
1315
+ "human_allowed": true,
1316
+ "evidence_required": [
1317
+ {
1318
+ "field": "target_type_rationale",
1319
+ "description": "Reason why this composer is hardcoded to a specific target type.",
1320
+ "upload_policy": "upload-with-consent"
1321
+ }
1322
+ ]
1323
+ }
1324
+ }
1325
+ },
1326
+ {
1327
+ "id": "react-native.feed.target-type-explicit",
1328
+ "version": 1,
1329
+ "title": "Explicit targetType for React Native createPost calls must not be hardcoded to COMMUNITY",
1330
+ "severity": "warning",
1331
+ "rationale": "React Native feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1332
+ "applies_when": {
1333
+ "platforms": [
1334
+ "react-native"
1335
+ ],
1336
+ "outcomes": [
1337
+ "add-feed",
1338
+ "validate-setup"
1339
+ ]
1340
+ },
1341
+ "enforcement": {
1342
+ "deterministic": [
1343
+ {
1344
+ "check": "validator-finding-absent",
1345
+ "finding_rule_id": "react-native.feed.target-type-explicit"
1346
+ }
1347
+ ],
1348
+ "attestation": {
1349
+ "allowed": true,
1350
+ "host_agent_min_confidence": "high",
1351
+ "human_allowed": true,
1352
+ "evidence_required": [
1353
+ {
1354
+ "field": "target_type_rationale",
1355
+ "description": "Reason why this composer is hardcoded to a specific target type.",
1356
+ "upload_policy": "upload-with-consent"
1357
+ }
1358
+ ]
1359
+ }
1360
+ }
1361
+ },
1362
+ {
1363
+ "id": "android.feed.target-type-explicit",
1364
+ "version": 1,
1365
+ "title": "Explicit targetType for Android createPost calls must not be hardcoded to COMMUNITY",
1366
+ "severity": "warning",
1367
+ "rationale": "Android feed targets must be passed dynamically (e.g. from Intent extras or ViewModel state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1368
+ "applies_when": {
1369
+ "platforms": [
1370
+ "android"
1371
+ ],
1372
+ "outcomes": [
1373
+ "add-feed",
1374
+ "validate-setup"
1375
+ ]
1376
+ },
1377
+ "enforcement": {
1378
+ "deterministic": [
1379
+ {
1380
+ "check": "validator-finding-absent",
1381
+ "finding_rule_id": "android.feed.target-type-explicit"
1382
+ }
1383
+ ],
1384
+ "attestation": {
1385
+ "allowed": true,
1386
+ "host_agent_min_confidence": "high",
1387
+ "human_allowed": true,
1388
+ "evidence_required": [
1389
+ {
1390
+ "field": "target_type_rationale",
1391
+ "description": "Reason why this composer is hardcoded to a specific target type.",
1392
+ "upload_policy": "upload-with-consent"
1393
+ }
1394
+ ]
1395
+ }
1396
+ }
1397
+ },
1398
+ {
1399
+ "id": "flutter.feed.target-type-explicit",
1400
+ "version": 1,
1401
+ "title": "Explicit targetType for Flutter createPost calls must not be hardcoded to COMMUNITY",
1402
+ "severity": "warning",
1403
+ "rationale": "Flutter feed targets must be passed dynamically (e.g. from Widget properties), not hardcoded as AmityPostTargetType.COMMUNITY.",
1404
+ "applies_when": {
1405
+ "platforms": [
1406
+ "flutter"
1407
+ ],
1408
+ "outcomes": [
1409
+ "add-feed",
1410
+ "validate-setup"
1411
+ ]
1412
+ },
1413
+ "enforcement": {
1414
+ "deterministic": [
1415
+ {
1416
+ "check": "validator-finding-absent",
1417
+ "finding_rule_id": "flutter.feed.target-type-explicit"
1418
+ }
1419
+ ],
1420
+ "attestation": {
1421
+ "allowed": true,
1422
+ "host_agent_min_confidence": "high",
1423
+ "human_allowed": true,
1424
+ "evidence_required": [
1425
+ {
1426
+ "field": "target_type_rationale",
1427
+ "description": "Reason why this composer is hardcoded to a specific target type.",
1428
+ "upload_policy": "upload-with-consent"
1429
+ }
1430
+ ]
1431
+ }
1432
+ }
1433
+ },
1434
+ {
1435
+ "id": "ios.feed.target-type-explicit",
1436
+ "version": 1,
1437
+ "title": "Explicit targetType for iOS createPost calls must not be hardcoded to COMMUNITY",
1438
+ "severity": "warning",
1439
+ "rationale": "iOS feed targets must be passed dynamically (e.g. from View state or initializer), not hardcoded as AmityPostTargetType.community.",
1440
+ "applies_when": {
1441
+ "platforms": [
1442
+ "ios"
1443
+ ],
1444
+ "outcomes": [
1445
+ "add-feed",
1446
+ "validate-setup"
1447
+ ]
1448
+ },
1449
+ "enforcement": {
1450
+ "deterministic": [
1451
+ {
1452
+ "check": "validator-finding-absent",
1453
+ "finding_rule_id": "ios.feed.target-type-explicit"
1454
+ }
1455
+ ],
1456
+ "attestation": {
1457
+ "allowed": true,
1458
+ "host_agent_min_confidence": "high",
1459
+ "human_allowed": true,
1460
+ "evidence_required": [
1461
+ {
1462
+ "field": "target_type_rationale",
1463
+ "description": "Reason why this composer is hardcoded to a specific target type.",
1464
+ "upload_policy": "upload-with-consent"
1465
+ }
1466
+ ]
1467
+ }
1468
+ }
1469
+ },
1470
+ {
1471
+ "id": "typescript.reactions.configured-name-used",
1472
+ "version": 1,
1473
+ "title": "TypeScript reaction name matches console config",
1474
+ "severity": "warning",
1475
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1476
+ "applies_when": {
1477
+ "platforms": [
1478
+ "typescript"
1479
+ ],
1480
+ "outcomes": [
1481
+ "add-feed",
1482
+ "add-comments",
1483
+ "validate-setup"
1484
+ ]
1485
+ },
1486
+ "enforcement": {
1487
+ "deterministic": [
1488
+ {
1489
+ "check": "validator-finding-absent",
1490
+ "finding_rule_id": "typescript.reactions.configured-name-used"
1491
+ }
1492
+ ],
1493
+ "attestation": {
1494
+ "allowed": true,
1495
+ "host_agent_min_confidence": "high",
1496
+ "human_allowed": true,
1497
+ "evidence_required": [
1498
+ {
1499
+ "field": "reaction_config_source",
1500
+ "description": "How the reaction name is passed dynamically.",
1501
+ "upload_policy": "upload-with-consent"
1502
+ }
1503
+ ]
1504
+ }
1505
+ }
1506
+ },
1507
+ {
1508
+ "id": "react-native.reactions.configured-name-used",
1509
+ "version": 1,
1510
+ "title": "React Native reaction name matches console config",
1511
+ "severity": "warning",
1512
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1513
+ "applies_when": {
1514
+ "platforms": [
1515
+ "react-native"
1516
+ ],
1517
+ "outcomes": [
1518
+ "add-feed",
1519
+ "add-comments",
1520
+ "validate-setup"
1521
+ ]
1522
+ },
1523
+ "enforcement": {
1524
+ "deterministic": [
1525
+ {
1526
+ "check": "validator-finding-absent",
1527
+ "finding_rule_id": "react-native.reactions.configured-name-used"
1528
+ }
1529
+ ],
1530
+ "attestation": {
1531
+ "allowed": true,
1532
+ "host_agent_min_confidence": "high",
1533
+ "human_allowed": true,
1534
+ "evidence_required": [
1535
+ {
1536
+ "field": "reaction_config_source",
1537
+ "description": "How the reaction name is passed dynamically.",
1538
+ "upload_policy": "upload-with-consent"
1539
+ }
1540
+ ]
1541
+ }
1542
+ }
1543
+ },
1544
+ {
1545
+ "id": "android.reactions.configured-name-used",
1546
+ "version": 1,
1547
+ "title": "Android reaction name matches console config",
1548
+ "severity": "warning",
1549
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1550
+ "applies_when": {
1551
+ "platforms": [
1552
+ "android"
1553
+ ],
1554
+ "outcomes": [
1555
+ "add-feed",
1556
+ "add-comments",
1557
+ "validate-setup"
1558
+ ]
1559
+ },
1560
+ "enforcement": {
1561
+ "deterministic": [
1562
+ {
1563
+ "check": "validator-finding-absent",
1564
+ "finding_rule_id": "android.reactions.configured-name-used"
1565
+ }
1566
+ ],
1567
+ "attestation": {
1568
+ "allowed": true,
1569
+ "host_agent_min_confidence": "high",
1570
+ "human_allowed": true,
1571
+ "evidence_required": [
1572
+ {
1573
+ "field": "reaction_config_source",
1574
+ "description": "How the reaction name is passed dynamically.",
1575
+ "upload_policy": "upload-with-consent"
1576
+ }
1577
+ ]
1578
+ }
1579
+ }
1580
+ },
1581
+ {
1582
+ "id": "flutter.reactions.configured-name-used",
1583
+ "version": 1,
1584
+ "title": "Flutter reaction name matches console config",
1585
+ "severity": "warning",
1586
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1587
+ "applies_when": {
1588
+ "platforms": [
1589
+ "flutter"
1590
+ ],
1591
+ "outcomes": [
1592
+ "add-feed",
1593
+ "add-comments",
1594
+ "validate-setup"
1595
+ ]
1596
+ },
1597
+ "enforcement": {
1598
+ "deterministic": [
1599
+ {
1600
+ "check": "validator-finding-absent",
1601
+ "finding_rule_id": "flutter.reactions.configured-name-used"
1602
+ }
1603
+ ],
1604
+ "attestation": {
1605
+ "allowed": true,
1606
+ "host_agent_min_confidence": "high",
1607
+ "human_allowed": true,
1608
+ "evidence_required": [
1609
+ {
1610
+ "field": "reaction_config_source",
1611
+ "description": "How the reaction name is passed dynamically.",
1612
+ "upload_policy": "upload-with-consent"
1613
+ }
1614
+ ]
1615
+ }
1616
+ }
1617
+ },
1618
+ {
1619
+ "id": "ios.reactions.configured-name-used",
1620
+ "version": 1,
1621
+ "title": "iOS reaction name matches console config",
1622
+ "severity": "warning",
1623
+ "rationale": "Reaction names are configurable per-tenant. Hardcoding 'like' or another specific name prevents apps from dynamically matching the tenant's actual configuration, leading to silent failures or API errors.",
1624
+ "applies_when": {
1625
+ "platforms": [
1626
+ "ios"
1627
+ ],
1628
+ "outcomes": [
1629
+ "add-feed",
1630
+ "add-comments",
1631
+ "validate-setup"
1632
+ ]
1633
+ },
1634
+ "enforcement": {
1635
+ "deterministic": [
1636
+ {
1637
+ "check": "validator-finding-absent",
1638
+ "finding_rule_id": "ios.reactions.configured-name-used"
1639
+ }
1640
+ ],
1641
+ "attestation": {
1642
+ "allowed": true,
1643
+ "host_agent_min_confidence": "high",
1644
+ "human_allowed": true,
1645
+ "evidence_required": [
1646
+ {
1647
+ "field": "reaction_config_source",
1648
+ "description": "How the reaction name is passed dynamically.",
1649
+ "upload_policy": "upload-with-consent"
1650
+ }
1651
+ ]
1652
+ }
1653
+ }
1654
+ },
1655
+ {
1656
+ "id": "typescript.custom-post-type.dataType-declared",
1657
+ "version": 1,
1658
+ "title": "TypeScript custom post must declare dataType",
1659
+ "severity": "warning",
1660
+ "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
1661
+ "applies_when": {
1662
+ "platforms": [
1663
+ "typescript"
1664
+ ],
1665
+ "outcomes": [
1666
+ "add-feed",
1667
+ "validate-setup"
1668
+ ]
1669
+ },
1670
+ "enforcement": {
1671
+ "deterministic": [
1672
+ {
1673
+ "check": "validator-finding-absent",
1674
+ "finding_rule_id": "typescript.custom-post-type.dataType-declared"
1675
+ }
1676
+ ],
1677
+ "attestation": {
1678
+ "allowed": true,
1679
+ "host_agent_min_confidence": "high",
1680
+ "human_allowed": true,
1681
+ "evidence_required": [
1682
+ {
1683
+ "field": "post_type",
1684
+ "description": "The custom data type or why the payload doesn't need it.",
1685
+ "upload_policy": "upload-with-consent"
1686
+ }
1687
+ ]
1688
+ }
1689
+ }
1690
+ },
1691
+ {
1692
+ "id": "react-native.custom-post-type.dataType-declared",
1693
+ "version": 1,
1694
+ "title": "React Native custom post must declare dataType",
1695
+ "severity": "warning",
1696
+ "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
1697
+ "applies_when": {
1698
+ "platforms": [
1699
+ "react-native"
1700
+ ],
1701
+ "outcomes": [
1702
+ "add-feed",
1703
+ "validate-setup"
1704
+ ]
1705
+ },
1706
+ "enforcement": {
1707
+ "deterministic": [
1708
+ {
1709
+ "check": "validator-finding-absent",
1710
+ "finding_rule_id": "react-native.custom-post-type.dataType-declared"
1711
+ }
1712
+ ],
1713
+ "attestation": {
1714
+ "allowed": true,
1715
+ "host_agent_min_confidence": "high",
1716
+ "human_allowed": true,
1717
+ "evidence_required": [
1718
+ {
1719
+ "field": "post_type",
1720
+ "description": "The custom data type or why the payload doesn't need it.",
1721
+ "upload_policy": "upload-with-consent"
1722
+ }
1723
+ ]
1724
+ }
1725
+ }
1726
+ },
1727
+ {
1728
+ "id": "android.custom-post-type.dataType-declared",
1729
+ "version": 1,
1730
+ "title": "Android custom post must declare dataType",
1731
+ "severity": "warning",
1732
+ "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
1733
+ "applies_when": {
1734
+ "platforms": [
1735
+ "android"
1736
+ ],
1737
+ "outcomes": [
1738
+ "add-feed",
1739
+ "validate-setup"
1740
+ ]
1741
+ },
1742
+ "enforcement": {
1743
+ "deterministic": [
1744
+ {
1745
+ "check": "validator-finding-absent",
1746
+ "finding_rule_id": "android.custom-post-type.dataType-declared"
1747
+ }
1748
+ ],
1749
+ "attestation": {
1750
+ "allowed": true,
1751
+ "host_agent_min_confidence": "high",
1752
+ "human_allowed": true,
1753
+ "evidence_required": [
1754
+ {
1755
+ "field": "post_type",
1756
+ "description": "The custom data type or why the payload doesn't need it.",
1757
+ "upload_policy": "upload-with-consent"
1758
+ }
1759
+ ]
1760
+ }
1761
+ }
1762
+ },
1763
+ {
1764
+ "id": "flutter.custom-post-type.dataType-declared",
1765
+ "version": 1,
1766
+ "title": "Flutter custom post must declare dataType",
1767
+ "severity": "warning",
1768
+ "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
1769
+ "applies_when": {
1770
+ "platforms": [
1771
+ "flutter"
1772
+ ],
1773
+ "outcomes": [
1774
+ "add-feed",
1775
+ "validate-setup"
1776
+ ]
1777
+ },
1778
+ "enforcement": {
1779
+ "deterministic": [
1780
+ {
1781
+ "check": "validator-finding-absent",
1782
+ "finding_rule_id": "flutter.custom-post-type.dataType-declared"
1783
+ }
1784
+ ],
1785
+ "attestation": {
1786
+ "allowed": true,
1787
+ "host_agent_min_confidence": "high",
1788
+ "human_allowed": true,
1789
+ "evidence_required": [
1790
+ {
1791
+ "field": "post_type",
1792
+ "description": "The custom data type or why the payload doesn't need it.",
1793
+ "upload_policy": "upload-with-consent"
1794
+ }
1795
+ ]
1796
+ }
1797
+ }
1798
+ },
1799
+ {
1800
+ "id": "ios.custom-post-type.dataType-declared",
1801
+ "version": 1,
1802
+ "title": "iOS custom post must declare dataType",
1803
+ "severity": "warning",
1804
+ "rationale": "When creating a post with a custom data payload, the 'dataType' tag must be explicitly provided so the SDK can route it correctly. If missing, it defaults to a text post, which stringifies the payload.",
1805
+ "applies_when": {
1806
+ "platforms": [
1807
+ "ios"
1808
+ ],
1809
+ "outcomes": [
1810
+ "add-feed",
1811
+ "validate-setup"
1812
+ ]
1813
+ },
1814
+ "enforcement": {
1815
+ "deterministic": [
1816
+ {
1817
+ "check": "validator-finding-absent",
1818
+ "finding_rule_id": "ios.custom-post-type.dataType-declared"
1819
+ }
1820
+ ],
1821
+ "attestation": {
1822
+ "allowed": true,
1823
+ "host_agent_min_confidence": "high",
1824
+ "human_allowed": true,
1825
+ "evidence_required": [
1826
+ {
1827
+ "field": "post_type",
1828
+ "description": "The custom data type or why the payload doesn't need it.",
1829
+ "upload_policy": "upload-with-consent"
1830
+ }
1831
+ ]
1832
+ }
1833
+ }
1834
+ },
1835
+ {
1836
+ "id": "typescript.unread.subscribed-not-counted",
1837
+ "version": 1,
1838
+ "title": "TypeScript unread count must use subscribed stream",
1839
+ "severity": "warning",
1840
+ "rationale": "Amity exposes a server-synced unreadCount stream. Manual JS arithmetic like posts.filter(p => !p.isRead).length goes stale because server-side read events from other devices never push back to the client array. Subscribe via AmityCoreClient.unreadCount() and bind the result with useState/useEffect instead.",
1841
+ "applies_when": {
1842
+ "platforms": [
1843
+ "typescript"
1844
+ ],
1845
+ "outcomes": [
1846
+ "add-feed",
1847
+ "add-chat",
1848
+ "validate-setup"
1849
+ ]
1850
+ },
1851
+ "enforcement": {
1852
+ "deterministic": [
1853
+ {
1854
+ "check": "validator-finding-absent",
1855
+ "finding_rule_id": "typescript.unread.subscribed-not-counted"
1856
+ }
1857
+ ],
1858
+ "attestation": {
1859
+ "allowed": true,
1860
+ "host_agent_min_confidence": "high",
1861
+ "human_allowed": true,
1862
+ "evidence_required": [
1863
+ {
1864
+ "field": "unread_source",
1865
+ "description": "How the component sources and updates the unread count from the Amity SDK.",
1866
+ "upload_policy": "upload-with-consent"
1867
+ }
1868
+ ]
1869
+ }
1870
+ }
1871
+ },
1872
+ {
1873
+ "id": "react-native.unread.subscribed-not-counted",
1874
+ "version": 1,
1875
+ "title": "React Native unread count must use subscribed stream",
1876
+ "severity": "warning",
1877
+ "rationale": "Amity exposes a server-synced unreadCount stream. Hand-rolling messages.filter(m => !m.isRead).length in a hook drifts when the user reads on another device — the bridge never pushes back to your local array. Subscribe via AmityCoreClient.unreadCount() and bind it to a useState in the badge component.",
1878
+ "applies_when": {
1879
+ "platforms": [
1880
+ "react-native"
1881
+ ],
1882
+ "outcomes": [
1883
+ "add-feed",
1884
+ "add-chat",
1885
+ "validate-setup"
1886
+ ]
1887
+ },
1888
+ "enforcement": {
1889
+ "deterministic": [
1890
+ {
1891
+ "check": "validator-finding-absent",
1892
+ "finding_rule_id": "react-native.unread.subscribed-not-counted"
1893
+ }
1894
+ ],
1895
+ "attestation": {
1896
+ "allowed": true,
1897
+ "host_agent_min_confidence": "high",
1898
+ "human_allowed": true,
1899
+ "evidence_required": [
1900
+ {
1901
+ "field": "unread_source",
1902
+ "description": "How the component sources and updates the unread count from the Amity SDK.",
1903
+ "upload_policy": "upload-with-consent"
1904
+ }
1905
+ ]
1906
+ }
1907
+ }
1908
+ },
1909
+ {
1910
+ "id": "android.unread.subscribed-not-counted",
1911
+ "version": 1,
1912
+ "title": "Android unread count must use subscribed stream",
1913
+ "severity": "warning",
1914
+ "rationale": "Amity exposes a server-synced unreadCount stream. Kotlin code that counts via posts.count { !it.isRead } in a ViewModel drifts when the user reads from another device — Room/Flow caches never get the server-side ack. Collect AmityCoreClient.unreadCount() as a Flow<Int> and expose it as StateFlow to the UI.",
1915
+ "applies_when": {
1916
+ "platforms": [
1917
+ "android"
1918
+ ],
1919
+ "outcomes": [
1920
+ "add-feed",
1921
+ "add-chat",
1922
+ "validate-setup"
1923
+ ]
1924
+ },
1925
+ "enforcement": {
1926
+ "deterministic": [
1927
+ {
1928
+ "check": "validator-finding-absent",
1929
+ "finding_rule_id": "android.unread.subscribed-not-counted"
1930
+ }
1931
+ ],
1932
+ "attestation": {
1933
+ "allowed": true,
1934
+ "host_agent_min_confidence": "high",
1935
+ "human_allowed": true,
1936
+ "evidence_required": [
1937
+ {
1938
+ "field": "unread_source",
1939
+ "description": "How the component sources and updates the unread count from the Amity SDK.",
1940
+ "upload_policy": "upload-with-consent"
1941
+ }
1942
+ ]
1943
+ }
1944
+ }
1945
+ },
1946
+ {
1947
+ "id": "flutter.unread.subscribed-not-counted",
1948
+ "version": 1,
1949
+ "title": "Flutter unread count must use subscribed stream",
1950
+ "severity": "warning",
1951
+ "rationale": "Amity exposes a server-synced unreadCount stream. Dart code that counts via posts.where((p) => !p.isRead).length inside a setState drifts when the user reads from another device — local List<AmityPost> never receives the server ack. Use a StreamBuilder bound to AmityCoreClient.unreadCount() for the badge.",
1952
+ "applies_when": {
1953
+ "platforms": [
1954
+ "flutter"
1955
+ ],
1956
+ "outcomes": [
1957
+ "add-feed",
1958
+ "add-chat",
1959
+ "validate-setup"
1960
+ ]
1961
+ },
1962
+ "enforcement": {
1963
+ "deterministic": [
1964
+ {
1965
+ "check": "validator-finding-absent",
1966
+ "finding_rule_id": "flutter.unread.subscribed-not-counted"
1967
+ }
1968
+ ],
1969
+ "attestation": {
1970
+ "allowed": true,
1971
+ "host_agent_min_confidence": "high",
1972
+ "human_allowed": true,
1973
+ "evidence_required": [
1974
+ {
1975
+ "field": "unread_source",
1976
+ "description": "How the component sources and updates the unread count from the Amity SDK.",
1977
+ "upload_policy": "upload-with-consent"
1978
+ }
1979
+ ]
1980
+ }
1981
+ }
1982
+ },
1983
+ {
1984
+ "id": "ios.unread.subscribed-not-counted",
1985
+ "version": 1,
1986
+ "title": "iOS unread count must use subscribed stream",
1987
+ "severity": "warning",
1988
+ "rationale": "Amity exposes a server-synced unreadCount stream. Swift code that counts via posts.filter { !$0.isRead }.count inside an @State drifts when the user reads from another device — the local snapshot never receives the server ack. Bind the SwiftUI badge to client.unreadCount() via @ObservedObject or Combine.",
1989
+ "applies_when": {
1990
+ "platforms": [
1991
+ "ios"
1992
+ ],
1993
+ "outcomes": [
1994
+ "add-feed",
1995
+ "add-chat",
1996
+ "validate-setup"
1997
+ ]
1998
+ },
1999
+ "enforcement": {
2000
+ "deterministic": [
2001
+ {
2002
+ "check": "validator-finding-absent",
2003
+ "finding_rule_id": "ios.unread.subscribed-not-counted"
2004
+ }
2005
+ ],
2006
+ "attestation": {
2007
+ "allowed": true,
2008
+ "host_agent_min_confidence": "high",
2009
+ "human_allowed": true,
2010
+ "evidence_required": [
2011
+ {
2012
+ "field": "unread_source",
2013
+ "description": "How the component sources and updates the unread count from the Amity SDK.",
2014
+ "upload_policy": "upload-with-consent"
2015
+ }
2016
+ ]
2017
+ }
2018
+ }
2019
+ },
2020
+ {
2021
+ "id": "typescript.file-upload.via-amity-file-client",
2022
+ "version": 1,
2023
+ "title": "TypeScript media must be uploaded via Amity file client",
2024
+ "severity": "warning",
2025
+ "rationale": "Pushing a File or Blob to S3/Cloudinary and passing the resulting URL into createPost({ attachments: [{ url }] }) fails — the SDK only recognizes AmityFileIds from its own pipeline. Upload via AmityFileRepository.uploadFile(file) (or the upload hook in your Amity wrapper) and attach { fileId } from that response instead.",
2026
+ "applies_when": {
2027
+ "platforms": [
2028
+ "typescript"
2029
+ ],
2030
+ "outcomes": [
2031
+ "add-feed",
2032
+ "validate-setup"
2033
+ ]
2034
+ },
2035
+ "enforcement": {
2036
+ "deterministic": [
2037
+ {
2038
+ "check": "validator-finding-absent",
2039
+ "finding_rule_id": "typescript.file-upload.via-amity-file-client"
2040
+ }
2041
+ ],
2042
+ "attestation": {
2043
+ "allowed": true,
2044
+ "host_agent_min_confidence": "high",
2045
+ "human_allowed": true,
2046
+ "evidence_required": [
2047
+ {
2048
+ "field": "media_upload_flow",
2049
+ "description": "How the component uploads media to Amity's infrastructure.",
2050
+ "upload_policy": "upload-with-consent"
2051
+ }
2052
+ ]
2053
+ }
2054
+ }
2055
+ },
2056
+ {
2057
+ "id": "react-native.file-upload.via-amity-file-client",
2058
+ "version": 1,
2059
+ "title": "React Native media must be uploaded via Amity file client",
2060
+ "severity": "warning",
2061
+ "rationale": "Picking an image via react-native-image-picker, uploading the local URI to S3, and passing the S3 URL into createPost fails — the SDK only recognizes AmityFileIds from its own pipeline. Upload via AmityFileRepository.uploadFile(localUri) (typically inside a useMutation) and attach { fileId } from that response.",
2062
+ "applies_when": {
2063
+ "platforms": [
2064
+ "react-native"
2065
+ ],
2066
+ "outcomes": [
2067
+ "add-feed",
2068
+ "validate-setup"
2069
+ ]
2070
+ },
2071
+ "enforcement": {
2072
+ "deterministic": [
2073
+ {
2074
+ "check": "validator-finding-absent",
2075
+ "finding_rule_id": "react-native.file-upload.via-amity-file-client"
2076
+ }
2077
+ ],
2078
+ "attestation": {
2079
+ "allowed": true,
2080
+ "host_agent_min_confidence": "high",
2081
+ "human_allowed": true,
2082
+ "evidence_required": [
2083
+ {
2084
+ "field": "media_upload_flow",
2085
+ "description": "How the component uploads media to Amity's infrastructure.",
2086
+ "upload_policy": "upload-with-consent"
2087
+ }
2088
+ ]
2089
+ }
2090
+ }
2091
+ },
2092
+ {
2093
+ "id": "android.file-upload.via-amity-file-client",
2094
+ "version": 1,
2095
+ "title": "Android media must be uploaded via Amity file client",
2096
+ "severity": "warning",
2097
+ "rationale": "Picking a Uri from MediaStore, uploading via OkHttp to a custom bucket, and passing the bucket URL into createPost fails — the SDK only recognizes AmityFileIds from AmityFileRepository.uploadFile(uri). Run the upload in viewModelScope, await the resulting AmityFile, then attach its fileId to the post builder.",
2098
+ "applies_when": {
2099
+ "platforms": [
2100
+ "android"
2101
+ ],
2102
+ "outcomes": [
2103
+ "add-feed",
2104
+ "validate-setup"
2105
+ ]
2106
+ },
2107
+ "enforcement": {
2108
+ "deterministic": [
2109
+ {
2110
+ "check": "validator-finding-absent",
2111
+ "finding_rule_id": "android.file-upload.via-amity-file-client"
2112
+ }
2113
+ ],
2114
+ "attestation": {
2115
+ "allowed": true,
2116
+ "host_agent_min_confidence": "high",
2117
+ "human_allowed": true,
2118
+ "evidence_required": [
2119
+ {
2120
+ "field": "media_upload_flow",
2121
+ "description": "How the component uploads media to Amity's infrastructure.",
2122
+ "upload_policy": "upload-with-consent"
2123
+ }
2124
+ ]
2125
+ }
2126
+ }
2127
+ },
2128
+ {
2129
+ "id": "flutter.file-upload.via-amity-file-client",
2130
+ "version": 1,
2131
+ "title": "Flutter media must be uploaded via Amity file client",
2132
+ "severity": "warning",
2133
+ "rationale": "Picking via image_picker, posting the File to a custom bucket, and passing the resulting URL into createPost fails — the SDK only recognizes AmityFileIds from AmityFileRepository.uploadFile(File). Await the upload Future, then attach the returned AmityImageData/AmityVideoData by fileId.",
2134
+ "applies_when": {
2135
+ "platforms": [
2136
+ "flutter"
2137
+ ],
2138
+ "outcomes": [
2139
+ "add-feed",
2140
+ "validate-setup"
2141
+ ]
2142
+ },
2143
+ "enforcement": {
2144
+ "deterministic": [
2145
+ {
2146
+ "check": "validator-finding-absent",
2147
+ "finding_rule_id": "flutter.file-upload.via-amity-file-client"
2148
+ }
2149
+ ],
2150
+ "attestation": {
2151
+ "allowed": true,
2152
+ "host_agent_min_confidence": "high",
2153
+ "human_allowed": true,
2154
+ "evidence_required": [
2155
+ {
2156
+ "field": "media_upload_flow",
2157
+ "description": "How the component uploads media to Amity's infrastructure.",
2158
+ "upload_policy": "upload-with-consent"
2159
+ }
2160
+ ]
2161
+ }
2162
+ }
2163
+ },
2164
+ {
2165
+ "id": "ios.file-upload.via-amity-file-client",
2166
+ "version": 1,
2167
+ "title": "iOS media must be uploaded via Amity file client",
2168
+ "severity": "warning",
2169
+ "rationale": "Picking a PHAsset/UIImage, uploading via URLSession to a custom bucket, and passing the resulting URL into createPost fails — the SDK only recognizes AmityFileIds from AmityFileRepository.uploadFile(_:). Use the async/await upload variant, then attach AmityImageData(fileId:) or AmityVideoData(fileId:) from the result.",
2170
+ "applies_when": {
2171
+ "platforms": [
2172
+ "ios"
2173
+ ],
2174
+ "outcomes": [
2175
+ "add-feed",
2176
+ "validate-setup"
2177
+ ]
2178
+ },
2179
+ "enforcement": {
2180
+ "deterministic": [
2181
+ {
2182
+ "check": "validator-finding-absent",
2183
+ "finding_rule_id": "ios.file-upload.via-amity-file-client"
2184
+ }
2185
+ ],
2186
+ "attestation": {
2187
+ "allowed": true,
2188
+ "host_agent_min_confidence": "high",
2189
+ "human_allowed": true,
2190
+ "evidence_required": [
2191
+ {
2192
+ "field": "media_upload_flow",
2193
+ "description": "How the component uploads media to Amity's infrastructure.",
2194
+ "upload_policy": "upload-with-consent"
2195
+ }
2196
+ ]
2197
+ }
2198
+ }
2199
+ },
2200
+ {
2201
+ "id": "typescript.image-post.child-resolution-awaited",
2202
+ "version": 1,
2203
+ "title": "TypeScript image post must await child resolution",
2204
+ "severity": "warning",
2205
+ "rationale": "createPost with an image returns the parent post before the image child finishes server-side processing. Rendering the result synchronously (e.g. setPost(result) in the same useEffect) gives an empty thumbnail. Await the child via result.children?.[0]?.whenReady or re-fetch with observePost(result.postId) before binding to state.",
2206
+ "applies_when": {
2207
+ "platforms": [
2208
+ "typescript"
2209
+ ],
2210
+ "outcomes": [
2211
+ "add-feed",
2212
+ "validate-setup"
2213
+ ]
2214
+ },
2215
+ "enforcement": {
2216
+ "deterministic": [
2217
+ {
2218
+ "check": "validator-finding-absent",
2219
+ "finding_rule_id": "typescript.image-post.child-resolution-awaited"
2220
+ }
2221
+ ],
2222
+ "attestation": {
2223
+ "allowed": true,
2224
+ "host_agent_min_confidence": "high",
2225
+ "human_allowed": true,
2226
+ "evidence_required": [
2227
+ {
2228
+ "field": "child_resolution_strategy",
2229
+ "description": "How the component ensures child posts (images/videos) are resolved before rendering.",
2230
+ "upload_policy": "upload-with-consent"
2231
+ }
2232
+ ]
2233
+ }
2234
+ }
2235
+ },
2236
+ {
2237
+ "id": "react-native.image-post.child-resolution-awaited",
2238
+ "version": 1,
2239
+ "title": "React Native image post must await child resolution",
2240
+ "severity": "warning",
2241
+ "rationale": "createPost with an image returns the parent post before the image child finishes server-side processing. Calling navigation.navigate('Feed', { post: result }) immediately renders a broken <Image source={{ uri: ... }}>. Await result.children?.[0]?.whenReady or subscribe via observePost(result.postId) before navigating.",
2242
+ "applies_when": {
2243
+ "platforms": [
2244
+ "react-native"
2245
+ ],
2246
+ "outcomes": [
2247
+ "add-feed",
2248
+ "validate-setup"
2249
+ ]
2250
+ },
2251
+ "enforcement": {
2252
+ "deterministic": [
2253
+ {
2254
+ "check": "validator-finding-absent",
2255
+ "finding_rule_id": "react-native.image-post.child-resolution-awaited"
2256
+ }
2257
+ ],
2258
+ "attestation": {
2259
+ "allowed": true,
2260
+ "host_agent_min_confidence": "high",
2261
+ "human_allowed": true,
2262
+ "evidence_required": [
2263
+ {
2264
+ "field": "child_resolution_strategy",
2265
+ "description": "How the component ensures child posts (images/videos) are resolved before rendering.",
2266
+ "upload_policy": "upload-with-consent"
2267
+ }
2268
+ ]
2269
+ }
2270
+ }
2271
+ },
2272
+ {
2273
+ "id": "android.image-post.child-resolution-awaited",
2274
+ "version": 1,
2275
+ "title": "Android image post must await child resolution",
2276
+ "severity": "warning",
2277
+ "rationale": "createPost with an image returns the parent post before the image child finishes server-side processing. Binding it straight to a Glide/Coil load in the same Activity yields a broken thumbnail. In Kotlin, suspend on AmityPost.children.first().whenReady() or re-observe via getPost(postId).asFlow() before binding the ImageView.",
2278
+ "applies_when": {
2279
+ "platforms": [
2280
+ "android"
2281
+ ],
2282
+ "outcomes": [
2283
+ "add-feed",
2284
+ "validate-setup"
2285
+ ]
2286
+ },
2287
+ "enforcement": {
2288
+ "deterministic": [
2289
+ {
2290
+ "check": "validator-finding-absent",
2291
+ "finding_rule_id": "android.image-post.child-resolution-awaited"
2292
+ }
2293
+ ],
2294
+ "attestation": {
2295
+ "allowed": true,
2296
+ "host_agent_min_confidence": "high",
2297
+ "human_allowed": true,
2298
+ "evidence_required": [
2299
+ {
2300
+ "field": "child_resolution_strategy",
2301
+ "description": "How the component ensures child posts (images/videos) are resolved before rendering.",
2302
+ "upload_policy": "upload-with-consent"
2303
+ }
2304
+ ]
2305
+ }
2306
+ }
2307
+ },
2308
+ {
2309
+ "id": "flutter.image-post.child-resolution-awaited",
2310
+ "version": 1,
2311
+ "title": "Flutter image post must await child resolution",
2312
+ "severity": "warning",
2313
+ "rationale": "createPost with an image returns the parent post before the image child finishes server-side processing. Calling setState((){ _post = result; }) immediately renders a placeholder where the Image.network should be. await result.children.first.whenReady or wrap the render in a StreamBuilder bound to observePost(result.postId).",
2314
+ "applies_when": {
2315
+ "platforms": [
2316
+ "flutter"
2317
+ ],
2318
+ "outcomes": [
2319
+ "add-feed",
2320
+ "validate-setup"
2321
+ ]
2322
+ },
2323
+ "enforcement": {
2324
+ "deterministic": [
2325
+ {
2326
+ "check": "validator-finding-absent",
2327
+ "finding_rule_id": "flutter.image-post.child-resolution-awaited"
2328
+ }
2329
+ ],
2330
+ "attestation": {
2331
+ "allowed": true,
2332
+ "host_agent_min_confidence": "high",
2333
+ "human_allowed": true,
2334
+ "evidence_required": [
2335
+ {
2336
+ "field": "child_resolution_strategy",
2337
+ "description": "How the component ensures child posts (images/videos) are resolved before rendering.",
2338
+ "upload_policy": "upload-with-consent"
2339
+ }
2340
+ ]
2341
+ }
2342
+ }
2343
+ },
2344
+ {
2345
+ "id": "ios.image-post.child-resolution-awaited",
2346
+ "version": 1,
2347
+ "title": "iOS image post must await child resolution",
2348
+ "severity": "warning",
2349
+ "rationale": "createPost with an image returns the parent post before the image child finishes server-side processing. Setting @State var post = result inside SwiftUI immediately renders an empty AsyncImage. Use try await result.children.first?.whenReady or observe via client.observePost(result.postId) (Combine) before assigning to the @State.",
2350
+ "applies_when": {
2351
+ "platforms": [
2352
+ "ios"
2353
+ ],
2354
+ "outcomes": [
2355
+ "add-feed",
2356
+ "validate-setup"
2357
+ ]
2358
+ },
2359
+ "enforcement": {
2360
+ "deterministic": [
2361
+ {
2362
+ "check": "validator-finding-absent",
2363
+ "finding_rule_id": "ios.image-post.child-resolution-awaited"
2364
+ }
2365
+ ],
2366
+ "attestation": {
2367
+ "allowed": true,
2368
+ "host_agent_min_confidence": "high",
2369
+ "human_allowed": true,
2370
+ "evidence_required": [
2371
+ {
2372
+ "field": "child_resolution_strategy",
2373
+ "description": "How the component ensures child posts (images/videos) are resolved before rendering.",
2374
+ "upload_policy": "upload-with-consent"
2375
+ }
2376
+ ]
2377
+ }
123
2378
  }
124
2379
  }
125
2380
  ]