@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 +9 -0
- package/README.md +21 -45
- package/dist/outcomes.js +10 -0
- package/dist/tools/compliance.js +3 -0
- package/dist/tools/project.js +205 -0
- package/docs/vise-how-it-helps.html +52 -308
- package/package.json +1 -1
- package/rules/channel-archive.yaml +136 -0
- package/rules/for-you-feed.yaml +110 -0
- package/rules/message-search.yaml +136 -0
- package/sdk-surface/android.json +1305 -23
- package/sdk-surface/flutter.json +2542 -2582
- package/sdk-surface/ios.json +1182 -166
- package/sdk-surface/manifest.json +32 -32
- package/sdk-surface/models.android.json +14 -4
- package/sdk-surface/models.flutter.json +2 -2
- package/sdk-surface/models.ios.json +2 -2
- package/sdk-surface/models.typescript.json +145 -145
- package/sdk-surface/typescript.json +8337 -2443
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) —
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
Composite score, 0–100 shared scale; every control built and launched.
|
|
153
150
|
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
171
|
+
### Cross-check: OpenAI Codex GPT-5.4 (directional)
|
|
194
172
|
|
|
195
|
-
|
|
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%,
|
|
201
|
-
- **
|
|
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) &&
|
package/dist/tools/compliance.js
CHANGED
|
@@ -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",
|
package/dist/tools/project.js
CHANGED
|
@@ -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));
|