@amityco/social-plus-vise 1.4.6 → 1.6.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.
@@ -0,0 +1,136 @@
1
+ {
2
+ "domain": "channel-archive",
3
+ "schema_version": 1,
4
+ "rules": [
5
+ {
6
+ "id": "typescript.channel-archive.live-collection",
7
+ "version": 1,
8
+ "symbol_anchored": { "platform": "typescript", "types": ["ChannelRepository"], "members": ["getArchivedChannels"], "basis": "names-only" },
9
+ "title": "TypeScript archived-channels view should use the reactive getArchivedChannels LiveCollection",
10
+ "severity": "warning",
11
+ "rationale": "The archived-channels list is a Live Collection — it updates as channels are archived/unarchived. A one-shot getArchivedChannelIds fetch (or getArchivedChannels with no callback) renders a list that never updates. Pass the live callback: ChannelRepository.getArchivedChannels(params, callback).",
12
+ "applies_when": {
13
+ "platforms": ["typescript"],
14
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
15
+ },
16
+ "enforcement": {
17
+ "deterministic": [
18
+ { "check": "validator-finding-absent", "finding_rule_id": "typescript.channel-archive.live-collection" }
19
+ ],
20
+ "attestation": {
21
+ "allowed": true,
22
+ "host_agent_min_confidence": "high",
23
+ "human_allowed": true,
24
+ "evidence_required": [
25
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating archived-channels list is appropriate here.", "upload_policy": "upload-with-consent" }
26
+ ]
27
+ }
28
+ },
29
+ "symptoms": ["Archived channels list never updates", "Archive/unarchive not reflected"]
30
+ },
31
+ {
32
+ "id": "react-native.channel-archive.live-collection",
33
+ "version": 1,
34
+ "symbol_anchored": { "platform": "react-native", "types": ["ChannelRepository"], "members": ["getArchivedChannels"], "basis": "names-only" },
35
+ "title": "React Native archived-channels view should use the reactive getArchivedChannels LiveCollection",
36
+ "severity": "warning",
37
+ "rationale": "The archived-channels list is a Live Collection — it updates as channels are archived/unarchived. A one-shot getArchivedChannelIds fetch (or getArchivedChannels with no callback) renders a list that never updates. Pass the live callback: ChannelRepository.getArchivedChannels(params, callback).",
38
+ "applies_when": {
39
+ "platforms": ["react-native"],
40
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
41
+ },
42
+ "enforcement": {
43
+ "deterministic": [
44
+ { "check": "validator-finding-absent", "finding_rule_id": "react-native.channel-archive.live-collection" }
45
+ ],
46
+ "attestation": {
47
+ "allowed": true,
48
+ "host_agent_min_confidence": "high",
49
+ "human_allowed": true,
50
+ "evidence_required": [
51
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating archived-channels list is appropriate here.", "upload_policy": "upload-with-consent" }
52
+ ]
53
+ }
54
+ },
55
+ "symptoms": ["Archived channels list never updates", "Archive/unarchive not reflected"]
56
+ },
57
+ {
58
+ "id": "android.channel-archive.live-collection",
59
+ "version": 1,
60
+ "symbol_anchored": { "platform": "android", "types": ["AmityChannelRepository"], "members": ["getArchivedChannels"], "basis": "names-only" },
61
+ "title": "Android archived-channels view should use the reactive getArchivedChannels LiveCollection",
62
+ "severity": "warning",
63
+ "rationale": "The archived-channels list is a Live Collection — it updates as channels are archived/unarchived. A one-shot fetch renders a list that never updates. Observe the PagingData stream: AmityChannelRepository.getArchivedChannels().doOnNext { _: PagingData<AmityChannel> -> }.subscribe().",
64
+ "applies_when": {
65
+ "platforms": ["android"],
66
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
67
+ },
68
+ "enforcement": {
69
+ "deterministic": [
70
+ { "check": "validator-finding-absent", "finding_rule_id": "android.channel-archive.live-collection" }
71
+ ],
72
+ "attestation": {
73
+ "allowed": true,
74
+ "host_agent_min_confidence": "high",
75
+ "human_allowed": true,
76
+ "evidence_required": [
77
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating archived-channels list is appropriate here.", "upload_policy": "upload-with-consent" }
78
+ ]
79
+ }
80
+ },
81
+ "symptoms": ["Archived channels list never updates", "Archive/unarchive not reflected"]
82
+ },
83
+ {
84
+ "id": "ios.channel-archive.live-collection",
85
+ "version": 1,
86
+ "symbol_anchored": { "platform": "ios", "types": ["AmityChannelRepository"], "members": ["getArchivedChannels"], "basis": "names-only" },
87
+ "title": "iOS archived-channels view should use the reactive getArchivedChannels LiveCollection",
88
+ "severity": "warning",
89
+ "rationale": "The archived-channels list is a Live Collection — it updates as channels are archived/unarchived. A one-shot completion handler renders a list that never updates. Observe the collection: amityChannelRepository.getArchivedChannels() then .observe on the returned collection.",
90
+ "applies_when": {
91
+ "platforms": ["ios"],
92
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
93
+ },
94
+ "enforcement": {
95
+ "deterministic": [
96
+ { "check": "validator-finding-absent", "finding_rule_id": "ios.channel-archive.live-collection" }
97
+ ],
98
+ "attestation": {
99
+ "allowed": true,
100
+ "host_agent_min_confidence": "high",
101
+ "human_allowed": true,
102
+ "evidence_required": [
103
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating archived-channels list is appropriate here.", "upload_policy": "upload-with-consent" }
104
+ ]
105
+ }
106
+ },
107
+ "symptoms": ["Archived channels list never updates", "Archive/unarchive not reflected"]
108
+ },
109
+ {
110
+ "id": "flutter.channel-archive.live-collection",
111
+ "version": 1,
112
+ "symbol_anchored": { "platform": "flutter", "types": ["AmityChannelRepository"], "members": ["getArchivedChannels"], "basis": "names-only" },
113
+ "title": "Flutter archived-channels view should use the reactive getArchivedChannels LiveCollection",
114
+ "severity": "warning",
115
+ "rationale": "The archived-channels list is a Live Collection — it updates as channels are archived/unarchived. A one-shot fetch renders a list that never updates. Wire a PagingController over getArchivedChannels: PagingController(pageFuture: (token) => ...getArchivedChannels().getPagingData(token: token, limit: 20)).",
116
+ "applies_when": {
117
+ "platforms": ["flutter"],
118
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
119
+ },
120
+ "enforcement": {
121
+ "deterministic": [
122
+ { "check": "validator-finding-absent", "finding_rule_id": "flutter.channel-archive.live-collection" }
123
+ ],
124
+ "attestation": {
125
+ "allowed": true,
126
+ "host_agent_min_confidence": "high",
127
+ "human_allowed": true,
128
+ "evidence_required": [
129
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating archived-channels list is appropriate here.", "upload_policy": "upload-with-consent" }
130
+ ]
131
+ }
132
+ },
133
+ "symptoms": ["Archived channels list never updates", "Archive/unarchive not reflected"]
134
+ }
135
+ ]
136
+ }
package/rules/feed.yaml CHANGED
@@ -68,6 +68,28 @@
68
68
  }
69
69
  }
70
70
  },
71
+ {
72
+ "id": "ios.feed.source-used",
73
+ "version": 1,
74
+ "title": "iOS feed integration must use SDK post APIs",
75
+ "severity": "warning",
76
+ "rationale": "A community/profile-only SDK integration is not an add-feed implementation. Feed surfaces must query or create SDK posts and render SDK post data.",
77
+ "applies_when": {
78
+ "platforms": ["ios"],
79
+ "outcomes": ["add-feed", "validate-setup"]
80
+ },
81
+ "enforcement": {
82
+ "deterministic": [
83
+ { "check": "validator-finding-absent", "finding_rule_id": "ios.feed.source-used" }
84
+ ],
85
+ "attestation": {
86
+ "allowed": false,
87
+ "host_agent_min_confidence": "high",
88
+ "human_allowed": false,
89
+ "evidence_required": []
90
+ }
91
+ }
92
+ },
71
93
  {
72
94
  "id": "typescript.feed.target.literal",
73
95
  "version": 1,
@@ -1547,9 +1569,9 @@
1547
1569
  "id": "typescript.feed.target-type-explicit",
1548
1570
  "version": 1,
1549
1571
  "symbol_anchored": {"platform":"typescript","basis":"pattern"},
1550
- "title": "Explicit targetType for TypeScript createPost calls must not be hardcoded to COMMUNITY",
1572
+ "title": "TypeScript createPost targetType must be a valid, bound write target",
1551
1573
  "severity": "warning",
1552
- "rationale": "TypeScript feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1574
+ "rationale": "TypeScript createPost targets must resolve to a valid SDK write target (user or community) with a targetId from props/state/auth/route, never a global aggregate feed target.",
1553
1575
  "applies_when": {
1554
1576
  "platforms": [
1555
1577
  "typescript"
@@ -1573,7 +1595,7 @@
1573
1595
  "evidence_required": [
1574
1596
  {
1575
1597
  "field": "target_type_rationale",
1576
- "description": "Reason why this composer is hardcoded to a specific target type.",
1598
+ "description": "Reason why this composer uses this targetType/targetId binding.",
1577
1599
  "upload_policy": "upload-with-consent"
1578
1600
  }
1579
1601
  ]
@@ -1584,9 +1606,9 @@
1584
1606
  "id": "react-native.feed.target-type-explicit",
1585
1607
  "version": 1,
1586
1608
  "symbol_anchored": {"platform":"react-native","basis":"pattern"},
1587
- "title": "Explicit targetType for React Native createPost calls must not be hardcoded to COMMUNITY",
1609
+ "title": "React Native createPost targetType must be a valid, bound write target",
1588
1610
  "severity": "warning",
1589
- "rationale": "React Native feed targets must be passed dynamically (e.g. from props or state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1611
+ "rationale": "React Native createPost targets must resolve to a valid SDK write target (user or community) with a targetId from props/state/auth/route, never a global aggregate feed target.",
1590
1612
  "applies_when": {
1591
1613
  "platforms": [
1592
1614
  "react-native"
@@ -1610,7 +1632,7 @@
1610
1632
  "evidence_required": [
1611
1633
  {
1612
1634
  "field": "target_type_rationale",
1613
- "description": "Reason why this composer is hardcoded to a specific target type.",
1635
+ "description": "Reason why this composer uses this targetType/targetId binding.",
1614
1636
  "upload_policy": "upload-with-consent"
1615
1637
  }
1616
1638
  ]
@@ -1621,9 +1643,9 @@
1621
1643
  "id": "android.feed.target-type-explicit",
1622
1644
  "version": 1,
1623
1645
  "symbol_anchored": {"platform":"android","basis":"pattern"},
1624
- "title": "Explicit targetType for Android createPost calls must not be hardcoded to COMMUNITY",
1646
+ "title": "Android createPost targetType must be a valid, bound write target",
1625
1647
  "severity": "warning",
1626
- "rationale": "Android feed targets must be passed dynamically (e.g. from Intent extras or ViewModel state), not hardcoded as AmityPostTargetType.COMMUNITY.",
1648
+ "rationale": "Android createPost targets must resolve to a valid SDK write target (user or community) with a targetId from Intent extras/ViewModel/auth/route state, never a global aggregate feed target.",
1627
1649
  "applies_when": {
1628
1650
  "platforms": [
1629
1651
  "android"
@@ -1647,7 +1669,7 @@
1647
1669
  "evidence_required": [
1648
1670
  {
1649
1671
  "field": "target_type_rationale",
1650
- "description": "Reason why this composer is hardcoded to a specific target type.",
1672
+ "description": "Reason why this composer uses this targetType/targetId binding.",
1651
1673
  "upload_policy": "upload-with-consent"
1652
1674
  }
1653
1675
  ]
@@ -1658,9 +1680,9 @@
1658
1680
  "id": "flutter.feed.target-type-explicit",
1659
1681
  "version": 1,
1660
1682
  "symbol_anchored": {"platform":"flutter","types":["AmityPostTargetType"],"basis":"names-only"},
1661
- "title": "Explicit targetType for Flutter createPost calls must not be hardcoded to COMMUNITY",
1683
+ "title": "Flutter createPost targetType must be a valid, bound write target",
1662
1684
  "severity": "warning",
1663
- "rationale": "Flutter feed targets must be passed dynamically (e.g. from Widget properties), not hardcoded as AmityPostTargetType.COMMUNITY.",
1685
+ "rationale": "Flutter createPost targets must resolve to a valid SDK write target (user or community) with a targetId from Widget/auth/route state, never a global aggregate feed target.",
1664
1686
  "applies_when": {
1665
1687
  "platforms": [
1666
1688
  "flutter"
@@ -1684,7 +1706,7 @@
1684
1706
  "evidence_required": [
1685
1707
  {
1686
1708
  "field": "target_type_rationale",
1687
- "description": "Reason why this composer is hardcoded to a specific target type.",
1709
+ "description": "Reason why this composer uses this targetType/targetId binding.",
1688
1710
  "upload_policy": "upload-with-consent"
1689
1711
  }
1690
1712
  ]
@@ -1695,9 +1717,9 @@
1695
1717
  "id": "ios.feed.target-type-explicit",
1696
1718
  "version": 1,
1697
1719
  "symbol_anchored": {"platform":"ios","types":["AmityPostTargetType"],"basis":"names-only"},
1698
- "title": "Explicit targetType for iOS createPost calls must not be hardcoded to COMMUNITY",
1720
+ "title": "iOS createPost targetType must be a valid, bound write target",
1699
1721
  "severity": "warning",
1700
- "rationale": "iOS feed targets must be passed dynamically (e.g. from View state or initializer), not hardcoded as AmityPostTargetType.community.",
1722
+ "rationale": "iOS createPost targets must resolve to a valid SDK write target (user or community) with a targetId from view/auth/route state, never a global aggregate feed target.",
1701
1723
  "applies_when": {
1702
1724
  "platforms": [
1703
1725
  "ios"
@@ -1721,7 +1743,7 @@
1721
1743
  "evidence_required": [
1722
1744
  {
1723
1745
  "field": "target_type_rationale",
1724
- "description": "Reason why this composer is hardcoded to a specific target type.",
1746
+ "description": "Reason why this composer uses this targetType/targetId binding.",
1725
1747
  "upload_policy": "upload-with-consent"
1726
1748
  }
1727
1749
  ]
@@ -0,0 +1,110 @@
1
+ {
2
+ "domain": "for-you-feed",
3
+ "schema_version": 1,
4
+ "rules": [
5
+ {
6
+ "id": "typescript.for-you-feed.live-collection",
7
+ "version": 1,
8
+ "symbol_anchored": { "platform": "typescript", "types": ["FeedRepository"], "members": ["getForYouFeed"], "basis": "names-only" },
9
+ "title": "TypeScript For-You feed should use the reactive getForYouFeed LiveCollection",
10
+ "severity": "warning",
11
+ "rationale": "The personalized For-You feed is a Live Collection — it updates as ranking and freshness change. A one-shot fetch-and-map renders a stale feed. Pass the live callback: FeedRepository.getForYouFeed(params, callback). Gate on getForYouFeedSetting and fall back to the global feed when For-You is disabled (AmityForYouFeedDisabledError).",
12
+ "applies_when": {
13
+ "platforms": ["typescript"],
14
+ "outcomes": ["add-feed", "setup-live-data", "validate-setup"]
15
+ },
16
+ "enforcement": {
17
+ "deterministic": [
18
+ { "check": "validator-finding-absent", "finding_rule_id": "typescript.for-you-feed.live-collection" }
19
+ ],
20
+ "attestation": {
21
+ "allowed": true,
22
+ "host_agent_min_confidence": "high",
23
+ "human_allowed": true,
24
+ "evidence_required": [
25
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating For-You feed is appropriate here.", "upload_policy": "upload-with-consent" }
26
+ ]
27
+ }
28
+ },
29
+ "symptoms": ["For-You feed never updates", "Stale personalized ranking"]
30
+ },
31
+ {
32
+ "id": "react-native.for-you-feed.live-collection",
33
+ "version": 1,
34
+ "symbol_anchored": { "platform": "react-native", "types": ["FeedRepository"], "members": ["getForYouFeed"], "basis": "names-only" },
35
+ "title": "React Native For-You feed should use the reactive getForYouFeed LiveCollection",
36
+ "severity": "warning",
37
+ "rationale": "The personalized For-You feed is a Live Collection — it updates as ranking and freshness change. A one-shot fetch-and-map renders a stale feed. Pass the live callback: FeedRepository.getForYouFeed(params, callback). Gate on getForYouFeedSetting and fall back to the global feed when For-You is disabled (AmityForYouFeedDisabledError).",
38
+ "applies_when": {
39
+ "platforms": ["react-native"],
40
+ "outcomes": ["add-feed", "setup-live-data", "validate-setup"]
41
+ },
42
+ "enforcement": {
43
+ "deterministic": [
44
+ { "check": "validator-finding-absent", "finding_rule_id": "react-native.for-you-feed.live-collection" }
45
+ ],
46
+ "attestation": {
47
+ "allowed": true,
48
+ "host_agent_min_confidence": "high",
49
+ "human_allowed": true,
50
+ "evidence_required": [
51
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating For-You feed is appropriate here.", "upload_policy": "upload-with-consent" }
52
+ ]
53
+ }
54
+ },
55
+ "symptoms": ["For-You feed never updates", "Stale personalized ranking"]
56
+ },
57
+ {
58
+ "id": "android.for-you-feed.live-collection",
59
+ "version": 1,
60
+ "symbol_anchored": { "platform": "android", "types": ["AmityFeedRepository"], "members": ["getForYouFeed"], "basis": "names-only" },
61
+ "title": "Android For-You feed should use the reactive getForYouFeed LiveCollection",
62
+ "severity": "warning",
63
+ "rationale": "The personalized For-You feed is a Live Collection — it updates as ranking and freshness change. A one-shot fetch renders a stale feed. Observe the PagingData stream: AmityFeedRepository.getForYouFeed().doOnNext { _: PagingData<AmityPost> -> }.subscribe(). Gate on getForYouFeedSetting and fall back to the global feed when disabled.",
64
+ "applies_when": {
65
+ "platforms": ["android"],
66
+ "outcomes": ["add-feed", "setup-live-data", "validate-setup"]
67
+ },
68
+ "enforcement": {
69
+ "deterministic": [
70
+ { "check": "validator-finding-absent", "finding_rule_id": "android.for-you-feed.live-collection" }
71
+ ],
72
+ "attestation": {
73
+ "allowed": true,
74
+ "host_agent_min_confidence": "high",
75
+ "human_allowed": true,
76
+ "evidence_required": [
77
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating For-You feed is appropriate here.", "upload_policy": "upload-with-consent" }
78
+ ]
79
+ }
80
+ },
81
+ "symptoms": ["For-You feed never updates", "Stale personalized ranking"]
82
+ },
83
+ {
84
+ "id": "ios.for-you-feed.live-collection",
85
+ "version": 1,
86
+ "symbol_anchored": { "platform": "ios", "types": ["AmityFeedRepository"], "members": ["getForYouFeed"], "basis": "names-only" },
87
+ "title": "iOS For-You feed should use the reactive getForYouFeed LiveCollection",
88
+ "severity": "warning",
89
+ "rationale": "The personalized For-You feed is a Live Collection — it updates as ranking and freshness change. A one-shot completion handler renders a stale feed. Observe the collection: amityFeedRepository.getForYouFeed() then .observe. Gate on getForYouFeedSetting and fall back to the global feed when disabled (forYouFeedDisabled).",
90
+ "applies_when": {
91
+ "platforms": ["ios"],
92
+ "outcomes": ["add-feed", "setup-live-data", "validate-setup"]
93
+ },
94
+ "enforcement": {
95
+ "deterministic": [
96
+ { "check": "validator-finding-absent", "finding_rule_id": "ios.for-you-feed.live-collection" }
97
+ ],
98
+ "attestation": {
99
+ "allowed": true,
100
+ "host_agent_min_confidence": "high",
101
+ "human_allowed": true,
102
+ "evidence_required": [
103
+ { "field": "query_lifetime", "description": "Why a one-shot, non-updating For-You feed is appropriate here.", "upload_policy": "upload-with-consent" }
104
+ ]
105
+ }
106
+ },
107
+ "symptoms": ["For-You feed never updates", "Stale personalized ranking"]
108
+ }
109
+ ]
110
+ }
@@ -524,7 +524,8 @@
524
524
  "human_allowed": false,
525
525
  "evidence_required": []
526
526
  }
527
- }
527
+ },
528
+ "symptoms": ["notLoggedIn", "NOT_LOGGED_IN", "session is not established", "session not ready", "user is not logged in", "SessionState"]
528
529
  },
529
530
  {
530
531
  "id": "flutter.live-collection.session-ready-requery",
@@ -546,7 +547,8 @@
546
547
  "human_allowed": false,
547
548
  "evidence_required": []
548
549
  }
549
- }
550
+ },
551
+ "symptoms": ["notLoggedIn", "NOT_LOGGED_IN", "session is not established", "session not ready", "user is not logged in", "SessionState"]
550
552
  },
551
553
  {
552
554
  "id": "ios.live-collection.session-ready-requery",
@@ -568,7 +570,8 @@
568
570
  "human_allowed": false,
569
571
  "evidence_required": []
570
572
  }
571
- }
573
+ },
574
+ "symptoms": ["notLoggedIn", "NOT_LOGGED_IN", "session is not established", "session not ready", "user is not logged in", "SessionState"]
572
575
  },
573
576
  {
574
577
  "id": "typescript.live-collection.session-ready-requery",
@@ -590,7 +593,8 @@
590
593
  "human_allowed": false,
591
594
  "evidence_required": []
592
595
  }
593
- }
596
+ },
597
+ "symptoms": ["notLoggedIn", "NOT_LOGGED_IN", "session is not established", "session not ready", "user is not logged in", "SessionState"]
594
598
  },
595
599
  {
596
600
  "id": "react-native.live-collection.session-ready-requery",
@@ -612,7 +616,8 @@
612
616
  "human_allowed": false,
613
617
  "evidence_required": []
614
618
  }
615
- }
619
+ },
620
+ "symptoms": ["notLoggedIn", "NOT_LOGGED_IN", "session is not established", "session not ready", "user is not logged in", "SessionState"]
616
621
  }
617
622
  ]
618
623
  }
@@ -0,0 +1,136 @@
1
+ {
2
+ "domain": "message-search",
3
+ "schema_version": 1,
4
+ "rules": [
5
+ {
6
+ "id": "typescript.message-search.live-collection",
7
+ "version": 1,
8
+ "symbol_anchored": { "platform": "typescript", "types": ["MessageRepository"], "members": ["searchMessage"], "basis": "names-only" },
9
+ "title": "TypeScript message search should use the reactive, paginated Search Message LiveCollection",
10
+ "severity": "warning",
11
+ "rationale": "In-chat message search is a paginated Live Collection. A one-shot searchMessage (no callback) renders only the first page and never updates as messages arrive or sync. Pass the live callback: MessageRepository.searchMessage(params, callback).",
12
+ "applies_when": {
13
+ "platforms": ["typescript"],
14
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
15
+ },
16
+ "enforcement": {
17
+ "deterministic": [
18
+ { "check": "validator-finding-absent", "finding_rule_id": "typescript.message-search.live-collection" }
19
+ ],
20
+ "attestation": {
21
+ "allowed": true,
22
+ "host_agent_min_confidence": "high",
23
+ "human_allowed": true,
24
+ "evidence_required": [
25
+ { "field": "query_lifetime", "description": "Why a one-shot, single-page message search is appropriate here.", "upload_policy": "upload-with-consent" }
26
+ ]
27
+ }
28
+ },
29
+ "symptoms": ["Message search shows only first page", "Stale message-search results"]
30
+ },
31
+ {
32
+ "id": "react-native.message-search.live-collection",
33
+ "version": 1,
34
+ "symbol_anchored": { "platform": "react-native", "types": ["MessageRepository"], "members": ["searchMessage"], "basis": "names-only" },
35
+ "title": "React Native message search should use the reactive, paginated Search Message LiveCollection",
36
+ "severity": "warning",
37
+ "rationale": "In-chat message search is a paginated Live Collection. A one-shot searchMessage (no callback) renders only the first page and never updates as messages arrive or sync. Pass the live callback: MessageRepository.searchMessage(params, callback).",
38
+ "applies_when": {
39
+ "platforms": ["react-native"],
40
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
41
+ },
42
+ "enforcement": {
43
+ "deterministic": [
44
+ { "check": "validator-finding-absent", "finding_rule_id": "react-native.message-search.live-collection" }
45
+ ],
46
+ "attestation": {
47
+ "allowed": true,
48
+ "host_agent_min_confidence": "high",
49
+ "human_allowed": true,
50
+ "evidence_required": [
51
+ { "field": "query_lifetime", "description": "Why a one-shot, single-page message search is appropriate here.", "upload_policy": "upload-with-consent" }
52
+ ]
53
+ }
54
+ },
55
+ "symptoms": ["Message search shows only first page", "Stale message-search results"]
56
+ },
57
+ {
58
+ "id": "android.message-search.live-collection",
59
+ "version": 1,
60
+ "symbol_anchored": { "platform": "android", "types": ["AmityMessageRepository"], "members": ["searchMessages"], "basis": "names-only" },
61
+ "title": "Android message search should use the reactive, paginated Search Message LiveCollection",
62
+ "severity": "warning",
63
+ "rationale": "In-chat message search is a paginated Live Collection. A one-shot fetch renders only the first page and never updates. Observe the PagingData stream: AmityMessageRepository.searchMessages(...).doOnNext { _: PagingData<AmityMessage> -> }.subscribe().",
64
+ "applies_when": {
65
+ "platforms": ["android"],
66
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
67
+ },
68
+ "enforcement": {
69
+ "deterministic": [
70
+ { "check": "validator-finding-absent", "finding_rule_id": "android.message-search.live-collection" }
71
+ ],
72
+ "attestation": {
73
+ "allowed": true,
74
+ "host_agent_min_confidence": "high",
75
+ "human_allowed": true,
76
+ "evidence_required": [
77
+ { "field": "query_lifetime", "description": "Why a one-shot, single-page message search is appropriate here.", "upload_policy": "upload-with-consent" }
78
+ ]
79
+ }
80
+ },
81
+ "symptoms": ["Message search shows only first page", "Stale message-search results"]
82
+ },
83
+ {
84
+ "id": "ios.message-search.live-collection",
85
+ "version": 1,
86
+ "symbol_anchored": { "platform": "ios", "types": ["AmityMessageRepository"], "members": ["searchMessages"], "basis": "names-only" },
87
+ "title": "iOS message search should use the reactive, paginated Search Message LiveCollection",
88
+ "severity": "warning",
89
+ "rationale": "In-chat message search is a paginated Live Collection. A one-shot completion handler renders only the first page and never updates. Observe the collection: amityMessageRepository.searchMessages(options:) then .observe on the returned collection.",
90
+ "applies_when": {
91
+ "platforms": ["ios"],
92
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
93
+ },
94
+ "enforcement": {
95
+ "deterministic": [
96
+ { "check": "validator-finding-absent", "finding_rule_id": "ios.message-search.live-collection" }
97
+ ],
98
+ "attestation": {
99
+ "allowed": true,
100
+ "host_agent_min_confidence": "high",
101
+ "human_allowed": true,
102
+ "evidence_required": [
103
+ { "field": "query_lifetime", "description": "Why a one-shot, single-page message search is appropriate here.", "upload_policy": "upload-with-consent" }
104
+ ]
105
+ }
106
+ },
107
+ "symptoms": ["Message search shows only first page", "Stale message-search results"]
108
+ },
109
+ {
110
+ "id": "flutter.message-search.live-collection",
111
+ "version": 1,
112
+ "symbol_anchored": { "platform": "flutter", "types": ["MessageRepository"], "members": ["searchMessage"], "basis": "names-only" },
113
+ "title": "Flutter message search should use the reactive, paginated Search Message LiveCollection",
114
+ "severity": "warning",
115
+ "rationale": "In-chat message search is a paginated Live Collection. A one-shot fetch renders only the first page and never updates. Wire a PagingController over the SearchMessageLiveCollection: PagingController(pageFuture: (token) => messageRepository.searchMessage(...).getPagingData(token: token, limit: 20)).",
116
+ "applies_when": {
117
+ "platforms": ["flutter"],
118
+ "outcomes": ["add-chat", "setup-live-data", "validate-setup"]
119
+ },
120
+ "enforcement": {
121
+ "deterministic": [
122
+ { "check": "validator-finding-absent", "finding_rule_id": "flutter.message-search.live-collection" }
123
+ ],
124
+ "attestation": {
125
+ "allowed": true,
126
+ "host_agent_min_confidence": "high",
127
+ "human_allowed": true,
128
+ "evidence_required": [
129
+ { "field": "query_lifetime", "description": "Why a one-shot, single-page message search is appropriate here.", "upload_policy": "upload-with-consent" }
130
+ ]
131
+ }
132
+ },
133
+ "symptoms": ["Message search shows only first page", "Stale message-search results"]
134
+ }
135
+ ]
136
+ }
@@ -2323,6 +2323,29 @@
2323
2323
  },
2324
2324
  "symptoms": ["Dependency-only integration", "Static social shell", "No SDK-backed runtime data"]
2325
2325
  },
2326
+ {
2327
+ "id": "ios.sdk.source-used",
2328
+ "version": 1,
2329
+ "title": "iOS SDK dependency must be used by product source",
2330
+ "severity": "warning",
2331
+ "rationale": "Installing the iOS social.plus SDK is not an integration. Product Swift source must use the SDK or UIKit manager beyond static initialization: login/register a runtime user and query or create social.plus data.",
2332
+ "applies_when": {
2333
+ "platforms": ["ios"],
2334
+ "outcomes": ["setup-sdk", "setup-live-data", "add-feed", "add-comments", "add-chat", "add-community", "add-follow", "validate-setup"]
2335
+ },
2336
+ "enforcement": {
2337
+ "deterministic": [
2338
+ { "check": "validator-finding-absent", "finding_rule_id": "ios.sdk.source-used" }
2339
+ ],
2340
+ "attestation": {
2341
+ "allowed": false,
2342
+ "host_agent_min_confidence": "high",
2343
+ "human_allowed": false,
2344
+ "evidence_required": []
2345
+ }
2346
+ },
2347
+ "symptoms": ["Dependency-only integration", "Static social shell", "No SDK-backed runtime data"]
2348
+ },
2326
2349
  {
2327
2350
  "id": "react-native.ui.runtime-probe-copy",
2328
2351
  "version": 1,