@amityco/social-plus-vise 1.5.0 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.6.0 — 2026-07-06
8
+
9
+ ### Added
10
+ - **Reactivity rules for the latest SDK features.** Three new live-collection rules guard features added in the recent SDK release: in-chat **message search** (`message-search.live-collection`, all platforms), **archived channels** (`channel-archive.live-collection`, all platforms), and the personalized **For-You feed** (`for-you-feed.live-collection`, TypeScript/React Native/Android/iOS). Each flags a results list rendered from a one-shot query instead of the reactive, paginated Live Collection, and stays silent on a correct reactive integration. For-You is not part of the Flutter public API, so a For-You request on Flutter now surfaces a platform-availability notice pointing to a community- or user-scoped feed instead.
11
+
12
+ ### Changed
13
+ - **Refreshed the bundled SDK surface facts** for TypeScript, Android, iOS, and Flutter. `vise sdk-facts` and the symbol-anchored rules now reflect the current SDK symbols and models — including channel archiving, message search, For-You feed, and per-scope notification modes. The refresh is additive: no previously grounded symbol was removed, so existing rules keep resolving.
14
+ - **Portable provenance in `vise sdk-surface:import`.** The importer normalizes the source paths recorded in the shipped surface facts, so they stay machine-independent regardless of local checkout layout.
15
+
7
16
  ## 1.5.0 — 2026-07-05
8
17
 
9
18
  ### Added
package/README.md CHANGED
@@ -142,65 +142,41 @@ Vise can ingest your aesthetic from an HTML/CSS prototype (`vise design extract`
142
142
 
143
143
  ## Evidence
144
144
 
145
- We put Vise through the same brownfield social.plus integration twice per platform — once through the **governed Vise workflow**, once from a structured checklist with **Vise switched off** (the control) — and ran the whole thing with two coding agents: first **OpenAI Codex GPT-5.4**, then **Claude Sonnet 5** on a hardened protocol. Vise led on all five platform families with both agents. But the two headline gaps are **not** equally solid, and **neither raw average is the number to stand behind**.
145
+ We put Vise through the same brownfield social.plus integration twice per platform — once through the **governed Vise workflow**, once from a structured checklist with **Vise switched off** (the control) — across five platform families, on a live tenant with no mocking. The precise, uniform benchmark is **Claude Sonnet 5**; we cross-checked the direction with a second agent, **OpenAI Codex GPT-5.4**. Read the direction, not the decimals.
146
146
 
147
- > **Three different numbers appear below. They measure different things:**
148
- > - **+29.1** — Sonnet 5 *full weighted composite*. Includes the +90 definitional capability axis, so it is **not** the earned lift.
149
- > - **+12.1** — Sonnet 5 *credible-only* (capability axis removed, four fair dimensions re-weighted to 100%). **This is the number we stand behind.**
150
- > - **+23 to +31** — cross-model convergence deltas. **Still raw composites** (capability-inflated). They prove the lift points the **same direction** across agents — not its magnitude.
147
+ ### Anchor: Claude Sonnet 5, per platform
151
148
 
152
- > All deltas are computed on **unrounded** composites, so hand-subtracting the rounded scores in a table can differ by 0.1 (e.g. 83.0 70.8 reads as ~12.2, but the exact delta is +12.1).
149
+ Composite score, 0–100 shared scale; every control built and launched.
153
150
 
154
- ### Per-platform head-to-head
151
+ | Platform | Fixture | Vise | no-Vise | Δ |
152
+ |---|---|--:|--:|--:|
153
+ | Web | PULSE / pulse-studio | 88.3 | 56.8 | **+31.5** |
154
+ | React Native | Arena Fit | 89.7 | 62.1 | **+27.6** |
155
+ | iOS | Luminary Club | 77.5 | 48.0 | **+29.5** |
156
+ | Android | MarketPulse | 77.5 | 50.1 | **+27.4** |
157
+ | Flutter | Fieldhouse | 78.0 | 48.5 | **+29.5** |
158
+ | **Average** | 5 fixtures | **82.2** | **53.1** | **+29.1** |
155
159
 
156
- Composite score, 0–100 shared scale. Failed Codex controls (watchdog-killed before launch) are marked; those cells deflate the Codex control and inflate its gap.
157
-
158
- | Platform | Fixture | Sonnet 5 Vise | Sonnet 5 no-Vise | Sonnet Δ | Codex Vise | Codex no-Vise | Codex Δ |
159
- |---|---|--:|--:|--:|--:|--:|--:|
160
- | Web | PULSE / pulse-studio | 88.3 | 56.8 | **+31.5** | 91.4 | 65.6 | **+25.8** |
161
- | React Native | Arena Fit | 89.7 | 62.1 | **+27.6** | 90.3 | 67.3 | **+23.0** |
162
- | iOS | Luminary Club | 77.5 | 48.0 | +29.5 | 90.1 | 43.0 ⚠︎ | +47.1 ⚠︎ |
163
- | Android | MarketPulse / market-pulse | 77.5 | 50.1 | +27.4 | 94.3 | 31.2 ❌ | +63.1 ❌ |
164
- | Flutter | Fieldhouse | 78.0 | 48.5 | +29.5 | 92.8 | 33.0 ❌ | +59.8 ❌ |
165
- | **Average** | 5 fixtures | **82.2** | **53.1** | **+29.1** | 91.8 | 48.0 | ~~+43.8~~ |
166
-
167
- - ❌ **Control failed before launch.** Flutter's no-Vise control deleted `lib/main.dart` and was watchdog-terminated (33.0); Android's was depressed by an instrument failure (31.2). Both deflate the control and inflate the Codex gap.
168
- - ⚠︎ **Control ran low but launched.** Codex iOS no-Vise reached only 43.0 — the lowest launched control in either set — on the earlier harness.
169
-
170
- ### Averages: anchor on the clean run
171
-
172
- - **Claude Sonnet 5 — anchor (+29.1).** Hardened protocol; **all 5 controls built and launched**. This is the clean, conservative headline. It still carries the +90 definitional axis, so the earned lift is +12.1 credible-only.
173
- - **OpenAI Codex GPT-5.4 — caveated (~~+43.8~~).** Earlier, less-hardened harness; 2 of 5 controls failed before launch and iOS ran low. That larger gap is **inflated** and is not comparable to the Sonnet number.
174
-
175
- ### The cross-model signal: where both controls launched, the agents agree
176
-
177
- On the only two platforms where the Codex control actually built and launched — **Web** and **React Native** — the two agents' deltas land within ~5 points of each other:
178
-
179
- - **Web:** Codex **+25.8** ≈ Sonnet 5 **+31.5** (agree within 5.7 pts)
180
- - **React Native:** Codex **+23.0** ≈ Sonnet 5 **+27.6** (agree within 4.6 pts)
181
-
182
- **Read these as raw composites, not the real-world lift.** Each Vise delta here still embeds the +90 definitional capability axis. So the convergence proves the lift points the **same direction** across two independent agents — it does **not** say the earned magnitude is +23–31. For the earned magnitude, use the credible-only number below. (iOS is left out of this window because the Codex iOS control ran low — the same reason its average is caveated.)
183
-
184
- ### The credible-only lift: +12.1
185
-
186
- Drop the Vise-definitional capability axis (the control scores 0 there **by construction**) and re-weight the remaining four dimensions to 100%:
160
+ That **+29.1** full composite still carries the **+90 definitional capability axis** (the control scores 0 there **by construction**). Drop it and re-weight the four fair dimensions to 100%:
187
161
 
188
162
  | | Vise | no-Vise | Δ |
189
163
  |---|--:|--:|--:|
190
164
  | Full weighted composite (incl. capability axis) | 82.2 | 53.1 | +29.1 |
191
- | **Credible-only composite (capability axis removed)** | **83.0** | **70.8** | **+12.1** |
165
+ | **Credible-only (capability axis removed)** | **83.0** | **70.8** | **+12.1** |
166
+
167
+ **+12.1 is the number to stand behind.** It comes from **Product outcome +17.6** and **Design integration +15.8** (the credible wins); **Runtime hygiene is a near-tie (+1.9** — build/launch cleanliness, which a capable agent clears either way; on Android the checklist control actually built cleaner — this is *not* SDK-rule compliance, see below); **Efficiency +11.7 is a scorer *budget* metric** (the governed arm is **slower** in real time, ~2.7× on web); **Capability activation +90 is definitional**, kept out of the headline. (Deltas use unrounded composites, so hand-subtracting rounded cells can differ by 0.1.)
168
+
169
+ > **Why the hygiene dimension is near-parity — and why that isn't the whole story.** The scorer's `correctness` dimension measures *runtime hygiene* (build, launch, no crashes/secrets/hardcoded IDs), which strong agents clear with or without Vise. It is **not** SDK-rule compliance — the thing the 400+ deterministic rules govern. That is measured directly by a separate **compliance-findings** metric (`vise validate` on both arms' produced source): an ungoverned control carries real rule violations a smoke test never surfaces, and its runtime symptoms already show up as the product-outcome gap above.
192
170
 
193
- (The +12.1 is the delta of the unrounded composites; hand-subtracting the rounded 83.0 − 70.8 reads as ~12.2.)
171
+ ### Cross-check: OpenAI Codex GPT-5.4 (directional)
194
172
 
195
- Where the +12.1 comes from (Sonnet 5): **Product outcome +17.6** and **Design integration +15.8** are the credible wins; **Correctness is a near-tie (+1.9** — on Android the checklist control actually built cleaner); **Efficiency +11.7 is a scorer *budget* metric, not wall-clock** the governed arm is **slower** in real time (~2. the control on web); **Capability activation +90 is definitional**, kept out of the headline.
173
+ The same A/B run with Codex points the same way **Vise ahead on every platform**. Codex's exact figures are **directional, not precise**: its no-Vise controls are high-variance — on Android a single control has read anywhere from **+63** (an unlucky, instrument-affected run on the earlier harness) down to **+27** (clean). Run cleanly on the hardened harness (Android and iOS N=3; Flutter one clean launch, the rest watchdog-blocked), Codex's controls land ~65 and its lift settles around **+25–30**, converging with Sonnet's. Honest cross-model read: **both agents show Vise winning by ~+25–30 once controls are run cleanly.** We anchor on Sonnet because it is the uniform, current-harness run; Codex is cross-model confirmation of direction.
196
174
 
197
175
  ### Methodology & caveats
198
176
 
199
177
  - **Vise is the deterministic CLI under test, not an AI model.** Each arm runs the same coding agent on the same brownfield fixture, live tenant, no mocking.
200
- - **Scoring weights:** capability activation 25% (definitional), product outcome 25%, design integration 20%, correctness 20%, efficiency 10%. The credible-only composite drops the capability axis and re-weights the other four to 100%.
201
- - **Deltas use unrounded scores.** Displayed scores are rounded to one decimal, so a hand-subtraction of two table cells can differ from the stated delta by 0.1.
202
- - **Two runs, protocol hardened between them.** Codex GPT-5.4 (Medium) ran late June 2026 on the first, less-hardened harness; Claude Sonnet 5 ran early July 2026 on the current hardened protocol. Because the harness changed, the two agents' controls are **not directly comparable** — which is exactly why the cross-model view is limited to the two platforms where both controls launched.
203
- - **n = 1 per arm, directional.** July 2026 product-evidence runs on representative fixtures — **not a third-party audit**. For a formal comparison, keep each Vise / no-Vise pair on the same model and reasoning setting.
178
+ - **Scoring weights:** capability activation 25% (definitional), product outcome 25%, design integration 20%, runtime hygiene 20% (the scorer's `correctness` key — build/launch cleanliness, not SDK-rule compliance), efficiency 10%. A separate **compliance-findings** metric (`vise validate` on both arms) measures SDK-rule adherence directly. The credible-only composite drops the capability axis and re-weights the other four to 100%.
179
+ - **Small-N, directional.** Controls are n = 1–3 per arm, so neither model is a precise second measurement, and the two are not directly comparable (Codex ran on an earlier harness). The runtime-signal scorer that could over-credit a rendered-but-broken shell is now gated. July 2026 product-evidence runs on representative fixtures — **not a third-party audit**. For a formal comparison, keep each Vise / no-Vise pair on the same model and reasoning setting.
204
180
 
205
181
  <sub>Cursor, Claude, Codex, GitHub Copilot, VS Code, and other product names are trademarks of their respective owners; social.plus is not affiliated with or endorsed by them.</sub>
206
182
 
package/dist/outcomes.js CHANGED
@@ -63,6 +63,16 @@ export const PLATFORM_FEATURE_GAPS = [
63
63
  sdkSource: "use-cases/social/livestream/go-live-and-room-management",
64
64
  guidance: "Room-based livestreaming — broadcasting, going live, and room management (RoomRepository.createRoom / getRoom / getRooms / stopRoom) — is NOT in the social.plus Flutter SDK; it is available on the iOS, Android, and TypeScript/web SDKs. The Flutter SDK exposes only the DEPRECATED legacy Stream API (StreamRepository.getStream) for VIEWING existing stream posts — there is no Room API in Dart, so do not invent RoomRepository / createRoom. Options: (a) build the broadcast/host livestream experience on a supported platform (iOS, Android, or TypeScript/web), (b) on Flutter, render an existing legacy stream post read-only via StreamRepository.getStream(streamId) (deprecated — exposes title/status/isLive only), or (c) embed a web livestream view.",
65
65
  },
66
+ {
67
+ feature: "For-You (personalized) feed",
68
+ match: /\bfor[\s-]?you\b|personali[sz]ed\s+feed|curated\s+feed|recommend\w*\s+feed/i,
69
+ exclude: /\brecommend\w*\s+(?:communit|user|friend|people|member)\w*/i,
70
+ unavailableOn: ["flutter"],
71
+ availableOn: ["typescript", "ios", "android"],
72
+ objectIds: ["for-you"],
73
+ sdkSource: "social-plus-sdk/social/content-management/posts/retrieve/for-you-feed",
74
+ guidance: "The social.plus Flutter SDK does not expose the For-You (personalized) feed — getForYouFeed / getForYouFeedSetting are available on the iOS, Android, and TypeScript/web SDKs only. Do not invent a getForYouFeed in Dart. Options: (a) build the For-You surface on a supported platform (iOS, Android, or TypeScript/web), (b) on Flutter, render a community-scoped or user-scoped feed (getCommunityFeed / getUserFeed) or the global feed instead, or (c) call a server-side recommendation API from your backend and surface results through your own endpoint.",
75
+ },
66
76
  ];
67
77
  export function platformFeatureGapsFor(request, platform) {
68
78
  return PLATFORM_FEATURE_GAPS.filter((gap) => gap.unavailableOn.includes(platform) &&
@@ -2300,6 +2300,9 @@ export const FILE_SCOPABLE_SENSOR_SUFFIXES = new Set([
2300
2300
  "live-collection.api-mismatch",
2301
2301
  "story.live-collection",
2302
2302
  "search.live-collection",
2303
+ "message-search.live-collection",
2304
+ "channel-archive.live-collection",
2305
+ "for-you-feed.live-collection",
2303
2306
  "notification-tray.live-collection",
2304
2307
  "event.live-collection",
2305
2308
  "blocked-users.live-collection",
@@ -472,6 +472,9 @@ async function validateAndroid(root) {
472
472
  findings.push(...validateLiveCollectionApiMismatch(root, "android", sourceContent));
473
473
  findings.push(...validateStories(root, "android", sourceContent));
474
474
  findings.push(...validateSearch(root, "android", sourceContent));
475
+ findings.push(...validateMessageSearch(root, "android", sourceContent));
476
+ findings.push(...validateChannelArchive(root, "android", sourceContent));
477
+ findings.push(...validateForYouFeed(root, "android", sourceContent));
475
478
  findings.push(...validateNotificationTray(root, "android", sourceContent));
476
479
  findings.push(...validateMembershipList(root, "android", sourceContent));
477
480
  findings.push(...validateEvents(root, "android", sourceContent));
@@ -601,6 +604,8 @@ async function validateFlutter(root) {
601
604
  findings.push(...validateLiveCollectionApiMismatch(root, "flutter", dartContent));
602
605
  findings.push(...validateStories(root, "flutter", dartContent));
603
606
  findings.push(...validateSearch(root, "flutter", dartContent));
607
+ findings.push(...validateMessageSearch(root, "flutter", dartContent));
608
+ findings.push(...validateChannelArchive(root, "flutter", dartContent));
604
609
  findings.push(...validateMembershipList(root, "flutter", dartContent));
605
610
  findings.push(...validateBlockedUsers(root, "flutter", dartContent));
606
611
  findings.push(...validatePollVoteStatusGuard(root, "flutter", dartContent));
@@ -732,6 +737,9 @@ async function validateTypeScript(root, platform) {
732
737
  findings.push(...validateLiveCollectionApiMismatch(root, platform, sourceContent));
733
738
  findings.push(...validateStories(root, platform, sourceContent));
734
739
  findings.push(...validateSearch(root, platform, sourceContent));
740
+ findings.push(...validateMessageSearch(root, platform, sourceContent));
741
+ findings.push(...validateChannelArchive(root, platform, sourceContent));
742
+ findings.push(...validateForYouFeed(root, platform, sourceContent));
735
743
  findings.push(...validateNotificationTray(root, platform, sourceContent));
736
744
  findings.push(...validateMembershipList(root, platform, sourceContent));
737
745
  findings.push(...validateEvents(root, platform, sourceContent));
@@ -1862,6 +1870,200 @@ function validateSearch(root, platform, sourceContent) {
1862
1870
  }
1863
1871
  return findings;
1864
1872
  }
1873
+ function validateMessageSearch(root, platform, sourceContent) {
1874
+ const findings = [];
1875
+ const ruleId = `${platform}.message-search.live-collection`;
1876
+ const MESSAGE_SEARCH_QUERY_PATTERNS = [
1877
+ /\bsearchMessages?\b/,
1878
+ /\bAmityMessageSearchQuery\b/,
1879
+ /\bAmityMessageSearchOptions\b/,
1880
+ /\bSearchMessageBuilder\b/,
1881
+ /\bSearchMessageLiveCollection\b/,
1882
+ ];
1883
+ const LIST_UI_PATTERNS = [
1884
+ /\bmap\s*\(/,
1885
+ /\bFlatList\b/,
1886
+ /\bListView\b/,
1887
+ /\bRecyclerView\b/,
1888
+ /\bLazyColumn\b/,
1889
+ /\bLazyRow\b/,
1890
+ /\bLazyVerticalGrid\b/,
1891
+ /\bLazyVerticalStaggeredGrid\b/,
1892
+ /\bForEach\b/,
1893
+ /\bList\s*\(/,
1894
+ /\bsubmitList\b/,
1895
+ /\btableView\b/,
1896
+ /\bcollectionView\b/
1897
+ ];
1898
+ const REACTIVE_MARKERS = [
1899
+ /\bgetLiveCollection\b/,
1900
+ /\bobserve\b/,
1901
+ /\bobserveOnce\b/,
1902
+ /\blisten\b/,
1903
+ /\bonData\b/,
1904
+ /\bStreamBuilder\b/,
1905
+ /\bLiveData\b/,
1906
+ /\bMutableLiveData\b/,
1907
+ /\bMediatorLiveData\b/,
1908
+ /collectAsState/,
1909
+ /observeAsState/,
1910
+ /Amity\w*Flow\b/,
1911
+ /\bPagingData\b/,
1912
+ /LazyPagingItems/,
1913
+ /collectAsLazyPagingItems/,
1914
+ /\.on\s*\(\s*['"]data[A-Z]/,
1915
+ /\$snapshots\b/,
1916
+ /\bPagingController\b/,
1917
+ /\bgetPagingData\b/,
1918
+ /\bsearchMessages?\s*\(\s*\{[^{}]*\}\s*,\s*(?:\w+|\([^)]*\)\s*=>|function\b|async\b)/,
1919
+ /\/\/\s*vise:\s*one-shot query/i
1920
+ ];
1921
+ for (const [file, content] of sourceContent) {
1922
+ const rel = relativeFile(root, file);
1923
+ if (path.basename(file).endsWith('.d.ts'))
1924
+ continue;
1925
+ const hasSearchQuery = MESSAGE_SEARCH_QUERY_PATTERNS.some((p) => p.test(content));
1926
+ if (hasSearchQuery) {
1927
+ const hasListUI = LIST_UI_PATTERNS.some((p) => p.test(content));
1928
+ if (hasListUI) {
1929
+ const reactiveSurface = commentStripped(file, platform, content);
1930
+ const hasReactivity = /\/\/\s*vise:\s*one-shot query/i.test(content) || REACTIVE_MARKERS.some((p) => p.test(reactiveSurface));
1931
+ if (!hasReactivity) {
1932
+ findings.push(finding(ruleId, "warning", `A message search (searchMessage/searchMessages) renders a list but no reactive/paginated markers (observe, the (params, callback) live form, PagingData, PagingController) were found in the same file.`, rel, "Render message-search results from the reactive, paginated Search Message LiveCollection so the list live-updates and pages. Use the live callback (TS/RN: searchMessage(params, callback)), observe the collection (iOS searchMessages(options:) + .observe / Android AmityMessageSearchQuery PagingData), or wire a PagingController with SearchMessageLiveCollection (Flutter). If a one-shot single-page search is truly intended, add comment // vise: one-shot query."));
1933
+ }
1934
+ }
1935
+ }
1936
+ }
1937
+ return findings;
1938
+ }
1939
+ function validateChannelArchive(root, platform, sourceContent) {
1940
+ const findings = [];
1941
+ const ruleId = `${platform}.channel-archive.live-collection`;
1942
+ const ARCHIVE_QUERY_PATTERNS = [
1943
+ /\bgetArchivedChannels\b/,
1944
+ /\bgetArchivedChannelIds\b/,
1945
+ ];
1946
+ const LIST_UI_PATTERNS = [
1947
+ /\bmap\s*\(/,
1948
+ /\bFlatList\b/,
1949
+ /\bListView\b/,
1950
+ /\bRecyclerView\b/,
1951
+ /\bLazyColumn\b/,
1952
+ /\bLazyRow\b/,
1953
+ /\bLazyVerticalGrid\b/,
1954
+ /\bLazyVerticalStaggeredGrid\b/,
1955
+ /\bForEach\b/,
1956
+ /\bList\s*\(/,
1957
+ /\bsubmitList\b/,
1958
+ /\btableView\b/,
1959
+ /\bcollectionView\b/
1960
+ ];
1961
+ const REACTIVE_MARKERS = [
1962
+ /\bgetLiveCollection\b/,
1963
+ /\bobserve\b/,
1964
+ /\bobserveOnce\b/,
1965
+ /\blisten\b/,
1966
+ /\bonData\b/,
1967
+ /\bStreamBuilder\b/,
1968
+ /\bLiveData\b/,
1969
+ /\bMutableLiveData\b/,
1970
+ /\bMediatorLiveData\b/,
1971
+ /collectAsState/,
1972
+ /observeAsState/,
1973
+ /Amity\w*Flow\b/,
1974
+ /\bPagingData\b/,
1975
+ /LazyPagingItems/,
1976
+ /collectAsLazyPagingItems/,
1977
+ /\.on\s*\(\s*['"]data[A-Z]/,
1978
+ /\$snapshots\b/,
1979
+ /\bPagingController\b/,
1980
+ /\bgetPagingData\b/,
1981
+ /\bgetArchivedChannels\s*\(\s*\{[^{}]*\}\s*,\s*(?:\w+|\([^)]*\)\s*=>|function\b|async\b)/,
1982
+ /\/\/\s*vise:\s*one-shot query/i
1983
+ ];
1984
+ for (const [file, content] of sourceContent) {
1985
+ const rel = relativeFile(root, file);
1986
+ if (path.basename(file).endsWith('.d.ts'))
1987
+ continue;
1988
+ const hasArchiveQuery = ARCHIVE_QUERY_PATTERNS.some((p) => p.test(content));
1989
+ if (hasArchiveQuery) {
1990
+ const hasListUI = LIST_UI_PATTERNS.some((p) => p.test(content));
1991
+ if (hasListUI) {
1992
+ const reactiveSurface = commentStripped(file, platform, content);
1993
+ const hasReactivity = /\/\/\s*vise:\s*one-shot query/i.test(content) || REACTIVE_MARKERS.some((p) => p.test(reactiveSurface));
1994
+ if (!hasReactivity) {
1995
+ findings.push(finding(ruleId, "warning", `An archived-channels view (getArchivedChannels / getArchivedChannelIds) renders a list but no reactive markers (observe, the (params, callback) live form, PagingData, PagingController) were found in the same file.`, rel, "Render the archived-channels view from the getArchivedChannels LiveCollection (subscribe via its callback / observe and re-render on updates, dispose on unmount) rather than a one-shot getArchivedChannelIds fetch, so the list updates as channels are archived or unarchived. If a one-shot snapshot is truly intended, add comment // vise: one-shot query."));
1996
+ }
1997
+ }
1998
+ }
1999
+ }
2000
+ return findings;
2001
+ }
2002
+ function validateForYouFeed(root, platform, sourceContent) {
2003
+ const findings = [];
2004
+ if (platform === "flutter")
2005
+ return findings;
2006
+ const ruleId = `${platform}.for-you-feed.live-collection`;
2007
+ const FORYOU_QUERY_PATTERNS = [
2008
+ /\bgetForYouFeed\b/,
2009
+ ];
2010
+ const LIST_UI_PATTERNS = [
2011
+ /\bmap\s*\(/,
2012
+ /\bFlatList\b/,
2013
+ /\bListView\b/,
2014
+ /\bRecyclerView\b/,
2015
+ /\bLazyColumn\b/,
2016
+ /\bLazyRow\b/,
2017
+ /\bLazyVerticalGrid\b/,
2018
+ /\bLazyVerticalStaggeredGrid\b/,
2019
+ /\bForEach\b/,
2020
+ /\bList\s*\(/,
2021
+ /\bsubmitList\b/,
2022
+ /\btableView\b/,
2023
+ /\bcollectionView\b/
2024
+ ];
2025
+ const REACTIVE_MARKERS = [
2026
+ /\bgetLiveCollection\b/,
2027
+ /\bobserve\b/,
2028
+ /\bobserveOnce\b/,
2029
+ /\blisten\b/,
2030
+ /\bonData\b/,
2031
+ /\bStreamBuilder\b/,
2032
+ /\bLiveData\b/,
2033
+ /\bMutableLiveData\b/,
2034
+ /\bMediatorLiveData\b/,
2035
+ /collectAsState/,
2036
+ /observeAsState/,
2037
+ /Amity\w*Flow\b/,
2038
+ /\bPagingData\b/,
2039
+ /LazyPagingItems/,
2040
+ /collectAsLazyPagingItems/,
2041
+ /\.on\s*\(\s*['"]data[A-Z]/,
2042
+ /\$snapshots\b/,
2043
+ /\bPagingController\b/,
2044
+ /\bgetPagingData\b/,
2045
+ /\bgetForYouFeed\s*\(\s*\{[^{}]*\}\s*,\s*(?:\w+|\([^)]*\)\s*=>|function\b|async\b)/,
2046
+ /\bgetForYouFeed\s*\(\s*(?:\([^)]*\)\s*=>|\w+\s*=>|function\b|async\b)/,
2047
+ /\/\/\s*vise:\s*one-shot query/i
2048
+ ];
2049
+ for (const [file, content] of sourceContent) {
2050
+ const rel = relativeFile(root, file);
2051
+ if (path.basename(file).endsWith('.d.ts'))
2052
+ continue;
2053
+ const hasForYouQuery = FORYOU_QUERY_PATTERNS.some((p) => p.test(content));
2054
+ if (hasForYouQuery) {
2055
+ const hasListUI = LIST_UI_PATTERNS.some((p) => p.test(content));
2056
+ if (hasListUI) {
2057
+ const reactiveSurface = commentStripped(file, platform, content);
2058
+ const hasReactivity = /\/\/\s*vise:\s*one-shot query/i.test(content) || REACTIVE_MARKERS.some((p) => p.test(reactiveSurface));
2059
+ if (!hasReactivity) {
2060
+ findings.push(finding(ruleId, "warning", `A For-You feed (getForYouFeed) renders a list but no reactive markers (observe, the (params, callback) live form, PagingData, PagingController) were found in the same file.`, rel, "Query the personalized feed via getForYouFeed and OBSERVE it as a live collection (subscribe / .observe / PagingData — never a one-shot fetch-and-map), so the feed updates as ranking and freshness change. Gate on getForYouFeedSetting and, when disabled, fall back to the global feed rather than rendering empty. If a one-shot snapshot is truly intended, add comment // vise: one-shot query."));
2061
+ }
2062
+ }
2063
+ }
2064
+ }
2065
+ return findings;
2066
+ }
1865
2067
  function validateNotificationTray(root, platform, sourceContent) {
1866
2068
  const findings = [];
1867
2069
  const ruleId = `${platform}.notification-tray.live-collection`;
@@ -3162,6 +3364,9 @@ async function validateIos(root) {
3162
3364
  findings.push(...validateLiveCollectionApiMismatch(root, "ios", swiftContent));
3163
3365
  findings.push(...validateStories(root, "ios", swiftContent));
3164
3366
  findings.push(...validateSearch(root, "ios", swiftContent));
3367
+ findings.push(...validateMessageSearch(root, "ios", swiftContent));
3368
+ findings.push(...validateChannelArchive(root, "ios", swiftContent));
3369
+ findings.push(...validateForYouFeed(root, "ios", swiftContent));
3165
3370
  findings.push(...validateNotificationTray(root, "ios", swiftContent));
3166
3371
  findings.push(...validateMembershipList(root, "ios", swiftContent));
3167
3372
  findings.push(...validateEvents(root, "ios", swiftContent));