@amityco/social-plus-vise 1.1.0 → 1.2.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.
@@ -112,7 +112,7 @@ async function runSensor(cwd, sensor, timeoutMs) {
112
112
  durationMs: Date.now() - startedAt,
113
113
  stdout: truncate(stdout),
114
114
  stderr: truncate(stderr),
115
- reason: `Timed out after ${timeoutMs}ms.`,
115
+ reason: sensor.timeoutReason ?? `Timed out after ${timeoutMs}ms.`,
116
116
  });
117
117
  }, timeoutMs);
118
118
  child.stdout?.on("data", (chunk) => {
@@ -0,0 +1,370 @@
1
+ const SIGNALS = [
2
+ {
3
+ id: "dynamic-ui",
4
+ level: "dynamic-ui",
5
+ label: "Minimal, server-driven brand or theme updates",
6
+ strength: "strong",
7
+ pattern: /\b(dynamic ui|remote config|network config|syncNetworkConfig|without (?:an? )?(?:app )?(?:release|update|rebuild)|real[-\s]?time theme|brand colors?|theme tokens?|palette|console[-\s]?driven)\b/i,
8
+ },
9
+ {
10
+ id: "component-styling",
11
+ level: "component-styling",
12
+ label: "Config-driven page/component/element styling",
13
+ strength: "strong",
14
+ pattern: /\b(component styling|config\.json|uikit\.config|customizations?\s+(?:map|key|path|object)|page\/component\/element|page id|component id|element id|preferred_theme|primary_color|exclu(?:de|des|ded|ding|sion|sions)|(?:hide|remove)(?!\s+(?:\w+\s+){0,2}?(?:members?|users?|people|participants?|admins?|accounts?|followers?))|button text|icons?|dark mode|light mode|feature flags?|reactions?)\b/i,
15
+ },
16
+ {
17
+ id: "localization",
18
+ level: "localization",
19
+ label: "Localized strings, copy, or terminology",
20
+ strength: "strong",
21
+ pattern: /\b(locali[sz](?:e|es|ed|ing|ation|ations)?|translat(?:e|es|ed|ing|ion|ions)|locale|(?<!\b(?:swift|kotlin|java|javascript|typescript|dart|objective[-\s]?c|programming|coding|markup|query|design|visual|brand|domain|ubiquitous)\s)language|strings?|copywriting|copy text|copy changes|wording|rename|setLocale|setOverrides|locale bundle|resource overlay|StringProvider|AmityStringProvider)\b/i,
22
+ },
23
+ {
24
+ id: "behavior-overrides",
25
+ level: "behavior-overrides",
26
+ label: "Navigation, route, click, or tap behavior override",
27
+ strength: "strong",
28
+ pattern: /\b(behavio[u]?r|navigation|route|deep link|on click|onclick|on tap|tap action|click action|clicks?|taps?|presses?|run custom code|custom code|custom handler|analytics (?:hook|callback|event|events|tracking|integration)|track(?:ing)? events?|permission gate|custom action|pageBehavior|BehaviourProvider|AmityPageRenderer)\b/i,
29
+ },
30
+ {
31
+ id: "fork-and-extend",
32
+ level: "fork-and-extend",
33
+ label: "Source-level UIKit extension",
34
+ strength: "strong",
35
+ pattern: /\b(fork(?: and extend)?|forked ui\s?kit|source code|own version|modify ui\s?kit source|custom layout|new component|internal view composition|custom data flow|logic injection|maintain upstream)\b|\b(?:restructure|rebuild|re[-\s]?architect|rework|overhaul)\s+(?:the\s+)?(?:\w+\s+){0,3}?(?:layout|structure|hierarchy|composition|thread)\b|\b(?:ui\s?kit|uikit)\s+(?:(?:doesn'?t|does not|can'?t|cannot|won'?t)\s+(?:have|provide|support|offer|include)|lacks?|is missing|has no)\b/i,
36
+ },
37
+ {
38
+ id: "sdk-custom-ui",
39
+ level: "sdk-custom-ui",
40
+ label: "Custom UI beyond UIKit's customization model",
41
+ strength: "strong",
42
+ pattern: /\b(totally different ui|brand[-\s]?new ui|bespoke ui|unique ui|complete design freedom|build from scratch|hand[-\s]?rolled)\b/i,
43
+ },
44
+ {
45
+ id: "basic-theme",
46
+ level: "dynamic-ui",
47
+ label: "Basic brand styling",
48
+ strength: "medium",
49
+ pattern: /\b(theme|theming|brand|colors?|style|styling|look and feel)\b/i,
50
+ },
51
+ {
52
+ id: "specific-element",
53
+ level: "component-styling",
54
+ label: "Specific UIKit element/page/component tweak",
55
+ strength: "medium",
56
+ pattern: /\b(feed header|composer button|story tab|comment tray|community card|profile header|share button|close button|target page|story_page|community_profile_page|global_feed_component)\b/i,
57
+ },
58
+ ];
59
+ const DOCS = [
60
+ "https://learn.social.plus/uikit/overview",
61
+ "https://learn.social.plus/uikit/customization/dynamic-ui",
62
+ "https://learn.social.plus/uikit/customization/component-styling",
63
+ "https://learn.social.plus/uikit/customization/localization",
64
+ "https://learn.social.plus/uikit/getting-started/installation",
65
+ ];
66
+ const SOURCE_EVIDENCE = [
67
+ "social.plus UIKit overview: minimal customization uses Dynamic UI, moderate customization uses Component Styling, advanced customization uses Fork and Extend, and totally different UI should use SDK/API directly.",
68
+ "social.plus Dynamic UI docs: use Console-managed configuration for low-risk brand/theme changes and sync network config in the client.",
69
+ "social.plus Component Styling docs: use config path overrides at page/component/element level, with exclusions and behavior overrides for supported navigation/action changes.",
70
+ "social.plus Localization docs: override strings or provide locale bundles while preserving English fallback behavior.",
71
+ "Local UIKit source study: Web provider exposes configs, pageBehavior, syncNetworkConfig, and localization props; React Native, Android, and Flutter repos use config files plus behavior managers/providers.",
72
+ ];
73
+ const FEATURE_CAPABILITY = /\b(livestream(?:ing)?|live[-\s]?stream(?:ing)?|video call(?:ing|s)?|voice call(?:ing|s)?|video chat|broadcast(?:ing)?|go live)\b/i;
74
+ export function recommendUIKitCustomization(args) {
75
+ const answers = args.answers ?? {};
76
+ const request = typeof args.request === "string" ? args.request : "";
77
+ const rawCustomizationAnswer = typeof answers.uikit_customization === "string" ? answers.uikit_customization.trim() : "";
78
+ const explicitAnswer = normalizeCustomizationAnswer(rawCustomizationAnswer);
79
+ const unrecognizedExplicitAnswer = rawCustomizationAnswer.length > 0 && !explicitAnswer;
80
+ const matches = collectSignals(request);
81
+ const grouped = groupedSignals(matches);
82
+ const hasCustomizationSignal = matches.length > 0;
83
+ const sdkCustomUiOnly = hasCustomizationSignal && matches.every((match) => match.level === "sdk-custom-ui");
84
+ const solutionNeedsUIKitAttention = args.solutionPath?.recommendation === "uikit" ||
85
+ args.solutionPath?.recommendation === "hybrid" ||
86
+ args.solutionPath?.recommendation === "needs-decision";
87
+ if (!explicitAnswer && args.solutionPath?.recommendation === "sdk") {
88
+ return undefined;
89
+ }
90
+ if (!explicitAnswer && !unrecognizedExplicitAnswer && (!hasCustomizationSignal || sdkCustomUiOnly) && !solutionNeedsUIKitAttention) {
91
+ return undefined;
92
+ }
93
+ const featureCapabilityOnly = !explicitAnswer && !unrecognizedExplicitAnswer && !hasCustomizationSignal && FEATURE_CAPABILITY.test(request);
94
+ const matchedLevels = levelsFromMatches(matches);
95
+ const answeredLevel = explicitAnswer;
96
+ const recommendedLevel = answeredLevel
97
+ ?? (unrecognizedExplicitAnswer || featureCapabilityOnly ? "needs-decision" : chooseRecommendedLevel(matches, args.solutionPath));
98
+ const additionalLevels = matchedLevels.filter((level) => level !== recommendedLevel);
99
+ const status = statusFor(recommendedLevel, matchedLevels, args.solutionPath, explicitAnswer, unrecognizedExplicitAnswer);
100
+ const confidence = confidenceFor({ explicitAnswer, matches, recommendedLevel, status, solutionPath: args.solutionPath, unrecognizedExplicitAnswer });
101
+ const platformGuidance = platformGuidanceFor(args.platform);
102
+ return {
103
+ status,
104
+ recommendedLevel,
105
+ confidence,
106
+ summary: summaryFor(recommendedLevel, status, explicitAnswer, additionalLevels),
107
+ answerId: "uikit_customization",
108
+ signals: grouped,
109
+ additionalLevels,
110
+ decision: {
111
+ requiredBeforeCustomization: status === "needs-decision" || recommendedLevel === "fork-and-extend" || recommendedLevel === "sdk-custom-ui",
112
+ question: "Which UIKit customization route should this customer use?",
113
+ options: [
114
+ "dynamic-ui: server-driven brand/theme changes without app releases",
115
+ "component-styling: config.json/uikit.config.json page, component, element, theme, exclude, and feature-flag overrides",
116
+ "localization: locale bundles, string overrides, or resource overlays for copy/language changes",
117
+ "behavior-overrides: supported navigation, route, click, tap, analytics, or permission behavior hooks",
118
+ "fork-and-extend: modify UIKit source while owning an upstream update strategy",
119
+ "sdk-custom-ui: leave UIKit for direct SDK/API when the desired UI is materially different",
120
+ "hybrid: combine UIKit configuration with SDK/custom code for differentiated app-layer behavior",
121
+ ],
122
+ },
123
+ implementationGuidance: implementationGuidanceFor(recommendedLevel, additionalLevels),
124
+ platformGuidance,
125
+ docs: DOCS,
126
+ evidence: SOURCE_EVIDENCE,
127
+ advisoryOnly: "This is advisory UIKit customization guidance. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
128
+ };
129
+ }
130
+ function collectSignals(request) {
131
+ return SIGNALS.flatMap((signal) => {
132
+ const match = request.match(signal.pattern);
133
+ if (!match?.[0]) {
134
+ return [];
135
+ }
136
+ return [{
137
+ id: signal.id,
138
+ level: signal.level,
139
+ label: signal.label,
140
+ strength: signal.strength,
141
+ matched: match[0],
142
+ }];
143
+ });
144
+ }
145
+ function groupedSignals(matches) {
146
+ return {
147
+ dynamicUi: matches.filter((match) => match.level === "dynamic-ui"),
148
+ componentStyling: matches.filter((match) => match.level === "component-styling"),
149
+ localization: matches.filter((match) => match.level === "localization"),
150
+ behaviorOverrides: matches.filter((match) => match.level === "behavior-overrides"),
151
+ forkAndExtend: matches.filter((match) => match.level === "fork-and-extend"),
152
+ sdkCustomUi: matches.filter((match) => match.level === "sdk-custom-ui"),
153
+ };
154
+ }
155
+ function levelsFromMatches(matches) {
156
+ return [
157
+ "dynamic-ui",
158
+ "component-styling",
159
+ "localization",
160
+ "behavior-overrides",
161
+ "fork-and-extend",
162
+ "sdk-custom-ui",
163
+ ].filter((level) => matches.some((match) => match.level === level));
164
+ }
165
+ function chooseRecommendedLevel(matches, solutionPath) {
166
+ const matchedLevels = levelsFromMatches(matches);
167
+ const strongLevels = levelsFromMatches(matches.filter((match) => match.strength === "strong"));
168
+ const pool = strongLevels.length > 0 ? strongLevels : matchedLevels;
169
+ if (pool.includes("sdk-custom-ui")) {
170
+ return "sdk-custom-ui";
171
+ }
172
+ if (pool.includes("fork-and-extend")) {
173
+ return "fork-and-extend";
174
+ }
175
+ if (pool.includes("behavior-overrides")) {
176
+ return "behavior-overrides";
177
+ }
178
+ if (pool.includes("component-styling")) {
179
+ return "component-styling";
180
+ }
181
+ if (pool.includes("dynamic-ui")) {
182
+ return "dynamic-ui";
183
+ }
184
+ if (pool.includes("localization")) {
185
+ return "localization";
186
+ }
187
+ if (solutionPath?.recommendation === "needs-decision") {
188
+ return "needs-decision";
189
+ }
190
+ return "dynamic-ui";
191
+ }
192
+ function statusFor(recommendedLevel, matchedLevels, solutionPath, explicitAnswer, unrecognizedExplicitAnswer) {
193
+ if (explicitAnswer) {
194
+ return "ready";
195
+ }
196
+ if (unrecognizedExplicitAnswer) {
197
+ return "needs-decision";
198
+ }
199
+ if (solutionPath?.recommendation === "needs-decision") {
200
+ return "needs-decision";
201
+ }
202
+ if (recommendedLevel === "needs-decision") {
203
+ return "needs-decision";
204
+ }
205
+ if (matchedLevels.includes("sdk-custom-ui") && matchedLevels.some((level) => level !== "sdk-custom-ui")) {
206
+ return "needs-decision";
207
+ }
208
+ return "ready";
209
+ }
210
+ function confidenceFor(args) {
211
+ if (args.explicitAnswer) {
212
+ return "high";
213
+ }
214
+ if (args.unrecognizedExplicitAnswer) {
215
+ return "low";
216
+ }
217
+ if (args.matches.some((match) => match.strength === "strong")) {
218
+ return args.status === "needs-decision" ? "medium" : "high";
219
+ }
220
+ if (args.solutionPath?.recommendation === "uikit" && args.recommendedLevel === "dynamic-ui") {
221
+ return "medium";
222
+ }
223
+ return "low";
224
+ }
225
+ function summaryFor(recommendedLevel, status, explicitAnswer, additionalLevels) {
226
+ if (explicitAnswer === "hybrid") {
227
+ return "The customer selected a hybrid UIKit customization path. Combine UIKit configuration or behavior hooks for standard surfaces with SDK/custom code for differentiated app-layer behavior.";
228
+ }
229
+ if (status === "needs-decision") {
230
+ return "The request crosses UIKit customization boundaries. Decide whether to stay within UIKit configuration/behavior hooks, fork UIKit, or build the custom surface directly with SDK/API.";
231
+ }
232
+ const suffix = additionalLevels.length > 0
233
+ ? ` Also account for ${additionalLevels.map(labelForLevel).join(", ")}.`
234
+ : "";
235
+ switch (recommendedLevel) {
236
+ case "dynamic-ui":
237
+ return `Use Dynamic UI for low-risk brand/theme changes that should be updated through configuration rather than app releases.${suffix}`;
238
+ case "component-styling":
239
+ return `Use Component Styling for config-driven page, component, element, theme, exclude, reaction, or feature-flag overrides while staying inside UIKit.${suffix}`;
240
+ case "localization":
241
+ return `Use UIKit localization for copy, terminology, locale, and string override goals while preserving default fallback behavior.${suffix}`;
242
+ case "behavior-overrides":
243
+ return `Use UIKit behavior overrides for supported navigation, route, click, tap, analytics, or permission behavior changes.${suffix}`;
244
+ case "fork-and-extend":
245
+ return `Use Fork and Extend only when the customer accepts source ownership and an upstream update strategy.${suffix}`;
246
+ case "sdk-custom-ui":
247
+ return `Use direct SDK/API custom UI when the desired experience is materially outside UIKit's configuration, behavior, and fork-friendly model.${suffix}`;
248
+ case "hybrid":
249
+ return `Use a hybrid UIKit + SDK route when standard surfaces can stay in UIKit but differentiated behavior belongs in customer-owned SDK/custom code.${suffix}`;
250
+ case "needs-decision":
251
+ return "Decide the UIKit customization route before implementation.";
252
+ }
253
+ }
254
+ function implementationGuidanceFor(recommendedLevel, additionalLevels) {
255
+ const shared = [
256
+ "Start with the lowest UIKit customization level that satisfies the customer goal; escalate only when the requested change cannot be expressed there.",
257
+ "Keep customer-owned configuration, behavior overrides, locale bundles, and fork deltas explicit so future UIKit upgrades are reviewable.",
258
+ "Validate the host app build and UX manually after UIKit customization; Vise compliance still covers customer wiring and deterministic SDK/config evidence, not hidden UIKit internals.",
259
+ ];
260
+ const levelGuidance = {
261
+ "dynamic-ui": [
262
+ "For Dynamic UI, configure brand/theme values in Console or network config, enable the client sync hook where supported, test launch/resume refresh, and verify cached fallback behavior offline.",
263
+ ],
264
+ "component-styling": [
265
+ "For Component Styling, edit the UIKit config file with page/component/element paths, keep wildcard overrides broad only when intentional, avoid structural exclusions that break usability, and test light and dark themes.",
266
+ ],
267
+ localization: [
268
+ "For Localization, use locale bundles, resource overlays, or string override APIs; keep placeholders intact and rely on default English fallback for untranslated keys.",
269
+ ],
270
+ "behavior-overrides": [
271
+ "For Behavior Overrides, override only the specific navigation/action method needed and leave default behavior in place for everything else.",
272
+ ],
273
+ "fork-and-extend": [
274
+ "For Fork and Extend, fork the UIKit source, document every customer delta, keep upstream merge strategy visible, and run the UIKit sample/build before integrating the fork into the customer app.",
275
+ ],
276
+ "sdk-custom-ui": [
277
+ "For SDK Custom UI, switch back to the SDK-first Vise flow and use UIKit only as reference material unless the customer explicitly chooses a hybrid path.",
278
+ ],
279
+ hybrid: [
280
+ "For Hybrid, use UIKit configuration or behavior hooks for standard social surfaces and direct SDK/custom code only for differentiated app-layer behavior.",
281
+ ],
282
+ "needs-decision": [
283
+ "For mixed requests, resolve whether the team accepts UIKit constraints before editing UI code.",
284
+ ],
285
+ };
286
+ return [
287
+ ...shared,
288
+ ...levelGuidance[recommendedLevel],
289
+ ...additionalLevels.flatMap((level) => levelGuidance[level]),
290
+ ];
291
+ }
292
+ function platformGuidanceFor(platform) {
293
+ const normalized = (platform ?? "").toLowerCase();
294
+ if (normalized.includes("react-native")) {
295
+ return [
296
+ "React Native UIKit: pass `uikit.config.json` through `AmityUiKitProvider configs`, use `BehaviourProvider`/`behaviour` for runtime navigation overrides, and use `AmityPageRenderer` when embedding standalone pages.",
297
+ "React Native source anchors: `Amity-Social-UIKit-React-Native-CLI-OpenSource/Onboarding.md`, `src/core/providers/AmityUIKitProvider.tsx`, `src/social/providers/BehaviourProvider.tsx`, and `src/social/enums/enumUIKitID.ts`.",
298
+ ];
299
+ }
300
+ if (normalized.includes("android")) {
301
+ return [
302
+ "Android UIKit: customize `common/src/main/assets/config.json`, set behavior through `AmityUIKit4Manager.behavior`/`AmityUIKit4Behaviour`, and use the string providers or resource overlays for localization.",
303
+ "Android source anchors: `Amity-Social-Cloud-UIKit-Android/common/src/main/assets/config.json`, `AmityUIKit4Behaviour.kt`, sample customization behavior files, and `LOCALIZATION_GUIDE.md`.",
304
+ ];
305
+ }
306
+ if (normalized.includes("flutter") || normalized.includes("dart")) {
307
+ return [
308
+ "Flutter UIKit: customize the packaged config asset, rely on `ConfigRepository` path/wildcard resolution, and assign behavior overrides through `AmityUIKit4Manager.behavior` where supported.",
309
+ "Flutter source anchors: `Amity-Social-Cloud-UIKit-Flutter/assets/config/config.json`, `lib/v4/core/config_repository.dart`, `uikit_behavior.dart`, and the behavior examples in `example/lib/main.dart`.",
310
+ ];
311
+ }
312
+ if (normalized.includes("ios") || normalized.includes("swift")) {
313
+ return [
314
+ "iOS UIKit: use the official Dynamic UI, Component Styling, Localization, behavior override, and fork guidance; the local UIKit source snapshot used for this research did not include an iOS repo.",
315
+ ];
316
+ }
317
+ return [
318
+ "Web/TypeScript UIKit: pass local config through `AmityUIKitProvider configs`, enable `syncNetworkConfig` for Dynamic UI, pass `pageBehavior` for navigation/action overrides, and use the `localization` prop for locale bundles or string overrides.",
319
+ "Web source anchors: `Amity-Social-Cloud-UIKit-Web/readme.md`, `src/v4/core/providers/AmityUIKitProvider.tsx`, `src/v4/core/providers/CustomizationProvider`, `src/v4/core/providers/PageBehaviorProvider.tsx`, and `src/v4/constants/customization.ts`.",
320
+ ];
321
+ }
322
+ function labelForLevel(level) {
323
+ switch (level) {
324
+ case "dynamic-ui":
325
+ return "Dynamic UI";
326
+ case "component-styling":
327
+ return "Component Styling";
328
+ case "localization":
329
+ return "Localization";
330
+ case "behavior-overrides":
331
+ return "Behavior Overrides";
332
+ case "fork-and-extend":
333
+ return "Fork and Extend";
334
+ case "sdk-custom-ui":
335
+ return "SDK Custom UI";
336
+ case "hybrid":
337
+ return "Hybrid";
338
+ }
339
+ }
340
+ function normalizeCustomizationAnswer(raw) {
341
+ const normalized = (typeof raw === "string" ? raw : "").trim().toLowerCase();
342
+ if (!normalized) {
343
+ return undefined;
344
+ }
345
+ if (/\bhybrid\b|\bmixed\b|\bboth\b/.test(normalized)) {
346
+ return "hybrid";
347
+ }
348
+ if (/\bsdk\b|\bcustom ui\b|\bfrom scratch\b|\bapi\b/.test(normalized)) {
349
+ return "sdk-custom-ui";
350
+ }
351
+ if (/\bfork\b|\bextend\b(?!\s+(?:the\s+)?(?:theme|brand|colou?rs?|styling|palette|look))|\bsource\b(?!\s+(?:the\s+)?(?:brand|colou?rs?|theme|palette|styling|config))/.test(normalized)) {
352
+ return "fork-and-extend";
353
+ }
354
+ if (/\bbehavio[u]?r\b|\bnavigation\b|\broute\b|\btap\b|\bclick\b/.test(normalized)) {
355
+ return "behavior-overrides";
356
+ }
357
+ if (/\blocali[sz](?:e|es|ed|ing|ation|ations)?\b|\blocale\b|\bstrings?\b|\btranslat(?:e|es|ed|ing|ion|ions)\b|\blanguage\b/.test(normalized)) {
358
+ return "localization";
359
+ }
360
+ if (/\b(?:remote|network)\s+config\b|\bdynamic ui\b/.test(normalized)) {
361
+ return "dynamic-ui";
362
+ }
363
+ if (/\bcomponent\b|\bstyling\b|\bconfig\b|\bexclu(?:de|des|ded|ding|sion|sions)\b/.test(normalized)) {
364
+ return "component-styling";
365
+ }
366
+ if (/\bdynamic\b|\bminimal\b|\btheme\b|\bbrand\b/.test(normalized)) {
367
+ return "dynamic-ui";
368
+ }
369
+ return undefined;
370
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amityco/social-plus-vise",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",