@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.
- package/CHANGELOG.md +20 -0
- package/README.md +31 -15
- package/dist/server.js +174 -8
- package/dist/solutionPath.js +274 -0
- package/dist/tools/harness.js +30 -2
- package/dist/tools/integration.js +161 -10
- package/dist/tools/project.js +13 -5
- package/dist/tools/sensors.js +1 -1
- package/dist/uikitCustomization.js +370 -0
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const UIKIT_SIGNALS = [
|
|
2
|
+
{
|
|
3
|
+
id: "explicit-uikit",
|
|
4
|
+
label: "Explicit social.plus UIKit / UI Kit mention",
|
|
5
|
+
strength: "strong",
|
|
6
|
+
pattern: /\b(?:social\.plus\s+)?(?:ui\s?kit|uikit)\b(?!\s+(?:experience|expertise|knowledge|background|familiarity))/i,
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "prebuilt-components",
|
|
10
|
+
label: "Prebuilt or ready-made social UI components",
|
|
11
|
+
strength: "strong",
|
|
12
|
+
pattern: /\b(pre[-\s]?built|ready[-\s]?made)\b|\b(?:ready[-\s]?to[-\s]?use|out[-\s]?of[-\s]?the[-\s]?box|plug[-\s]?and[-\s]?play|drop[-\s]?in|off[-\s]?the[-\s]?shelf)\b(?:\s+(?:social\.plus|social|messaging|message|notifications?|community|communities|chat|feeds?|profiles?|stor(?:y|ies)|comments?|users?|groups?|channels?|posts?|prebuilt|standard|ui\s?kit|uikit))?\s+(?:ui|ui\s?kit|uikit|components?|widgets?|screens?|views?|feed|chat|profile|comments?|communit(?:y|ies)|stor(?:y|ies)|surfaces?)\b/i,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "speed-to-launch",
|
|
16
|
+
label: "Launch speed / MVP pressure",
|
|
17
|
+
strength: "medium",
|
|
18
|
+
pattern: /\b(quick launch|launch quickly|ship quickly|ship fast|mvp|build (?:a |an |the )?prototype|prototype (?:quickly|fast|first)|rapid prototyp\w*|weeks not months|days not months|save (?:development|dev|build|engineering) time|reduce development time)\b/i,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "standard-layout",
|
|
22
|
+
label: "Standard social app layout or white-label surface",
|
|
23
|
+
strength: "medium",
|
|
24
|
+
pattern: /\b(standard social|standard layout|standard feed|standard chat|standard profile|white[-\s]?label|basic styling|minimal customization|moderate customization|brand colors only)\b/i,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "uikit-customization-ladder",
|
|
28
|
+
label: "UIKit customization mode",
|
|
29
|
+
strength: "medium",
|
|
30
|
+
pattern: /\b(dynamic ui|component styling|fork and extend|forked ui\s?kit|fork the ui\s?kit|customi[sz]e the ui\s?kit)\b/i,
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const REJECT_NEGATOR = "do not|don't|dont|won't|wont|can't|cant|cannot|avoid|skip|no|without|not|never";
|
|
34
|
+
const REJECT_CONNECTOR = "to|use|uses|using|used|want|wants|wanting|adopt|adopts|adopting|leverage|leveraging|rely|relying|touch|build|building|go|going|be|been|with|on";
|
|
35
|
+
const REJECT_UIKIT_NOUN = "(?:the\\s+)?(?:social\\.plus\\s+)?(?:ui\\s?kit|uikit)\\b";
|
|
36
|
+
const REJECT_SKILL_LOOKAHEAD = "(?!\\s+(?:experience|expertise|knowledge|background|familiarity))";
|
|
37
|
+
const REJECT_UIKIT_SOURCE = `\\b(?:${REJECT_NEGATOR})(?:\\s+(?:${REJECT_CONNECTOR}))*\\s+${REJECT_UIKIT_NOUN}${REJECT_SKILL_LOOKAHEAD}` +
|
|
38
|
+
`|\\b(?:instead of|rather than)(?:\\s+(?:${REJECT_CONNECTOR}))*\\s+${REJECT_UIKIT_NOUN}` +
|
|
39
|
+
`|\\b(?:anything|everything)\\s+but\\s+${REJECT_UIKIT_NOUN}`;
|
|
40
|
+
const NEGATED_FROM_SCRATCH = /\b(?:not|never|avoid|don't|dont|do not|rather than|instead of)\b[\s\w,]{0,16}?\bfrom scratch\b/i;
|
|
41
|
+
const NEGATED_STANDARD = /\b(?:not|never|avoid|don't|dont|do not|rather than|instead of)\b[\s\w,]{0,12}?\b(?:standard social|standard layout|standard feed|standard chat|standard profile|white[-\s]?label|basic styling|minimal customization|moderate customization)\b/i;
|
|
42
|
+
const SDK_SIGNALS = [
|
|
43
|
+
{
|
|
44
|
+
id: "direct-sdk",
|
|
45
|
+
label: "Direct SDK implementation",
|
|
46
|
+
strength: "strong",
|
|
47
|
+
pattern: /\b(?:directly|direct|using|use|with|via|embed|embedding|integrate|integrating)\s+(?:the\s+)?(?:social\.plus\s+)?SDK\b|\b(?:the\s+)?(?:social\.plus\s+)?SDK\s+for\b|\bSDK[-\s]?owned\b|\bSDK[-\s]?custom\b/i,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "reject-uikit",
|
|
51
|
+
label: "Explicitly rejects UIKit or chooses SDK instead of UIKit",
|
|
52
|
+
strength: "strong",
|
|
53
|
+
pattern: new RegExp(REJECT_UIKIT_SOURCE, "i"),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "custom-ui",
|
|
57
|
+
label: "Custom UI or non-standard experience",
|
|
58
|
+
strength: "strong",
|
|
59
|
+
pattern: /\b(?:custom|bespoke|unique|fully[-\s]?custom|completely[-\s]?custom|totally[-\s]?(?:different|custom)|brand[-\s]?new|differentiated)\s+(?:(?:social|messaging|notification|community|chat|feed|profile|story|stories|comment|comments|user|group|channel|post|navigation|onboarding|discovery)\s+)?(?:ui|interface|experience)\b|\bnon[-\s]?standard (?:ui|layout|flow|experience)\b|\bpixel[-\s]?perfect\b/i,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "custom-flows",
|
|
63
|
+
label: "Custom user flows or interactions",
|
|
64
|
+
strength: "strong",
|
|
65
|
+
pattern: /\b(custom flows?|custom user flows?|custom interactions?|custom workflows?|unique flows?|bespoke flows?|special interaction)\b/i,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "full-control",
|
|
69
|
+
label: "Full control / design freedom",
|
|
70
|
+
strength: "strong",
|
|
71
|
+
pattern: /\b(full control|complete control|maximum flexibility|complete design freedom|full design freedom|build from scratch|hand[-\s]?rolled|from scratch)\b/i,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "deep-app-integration",
|
|
75
|
+
label: "Deep existing-app integration",
|
|
76
|
+
strength: "medium",
|
|
77
|
+
pattern: /\b(deeply integrate|existing app flow|existing navigation|custom backend|server[-\s]?to[-\s]?server|non[-\s]?standard platform|advanced technical requirements)\b/i,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const SOURCE_EVIDENCE = [
|
|
81
|
+
"social.plus UIKit docs: prebuilt UI components, quick launch, and minimal/moderate customization paths.",
|
|
82
|
+
"social.plus SDK docs: custom experiences, unique user flows, complete design freedom, and existing-app integration.",
|
|
83
|
+
];
|
|
84
|
+
export function recommendSolutionPath(request, answers = {}) {
|
|
85
|
+
const text = typeof request === "string" ? request : "";
|
|
86
|
+
const explicitAnswer = normalizeSolutionPathAnswer(answers?.solution_path);
|
|
87
|
+
const sdkSignals = collectSignals(text, SDK_SIGNALS).filter((signal) => {
|
|
88
|
+
if (signal.id !== "full-control" || !/scratch/i.test(signal.matched)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return !NEGATED_FROM_SCRATCH.test(text);
|
|
92
|
+
});
|
|
93
|
+
const negativeUIKitSignals = sdkSignals.filter((signal) => signal.id === "reject-uikit");
|
|
94
|
+
let uikitScanText = text;
|
|
95
|
+
if (negativeUIKitSignals.length > 0) {
|
|
96
|
+
uikitScanText = text.replace(new RegExp(REJECT_UIKIT_SOURCE, "gi"), " ");
|
|
97
|
+
}
|
|
98
|
+
let uikitSignals = collectSignals(uikitScanText, UIKIT_SIGNALS).filter((signal) => {
|
|
99
|
+
if (signal.id !== "standard-layout") {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return !NEGATED_STANDARD.test(text);
|
|
103
|
+
});
|
|
104
|
+
if (negativeUIKitSignals.length > 0 && !uikitSignals.some((signal) => signal.strength === "strong")) {
|
|
105
|
+
uikitSignals = [];
|
|
106
|
+
}
|
|
107
|
+
if (explicitAnswer) {
|
|
108
|
+
return guidanceFor({
|
|
109
|
+
recommendation: explicitAnswer,
|
|
110
|
+
confidence: "high",
|
|
111
|
+
uikitSignals,
|
|
112
|
+
sdkSignals,
|
|
113
|
+
answered: explicitAnswer,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const hasStrongUIKit = uikitSignals.some((signal) => signal.strength === "strong");
|
|
117
|
+
const hasStrongSdk = sdkSignals.some((signal) => signal.strength === "strong");
|
|
118
|
+
if (uikitSignals.length > 0 && sdkSignals.length > 0) {
|
|
119
|
+
return guidanceFor({
|
|
120
|
+
recommendation: "needs-decision",
|
|
121
|
+
confidence: hasStrongUIKit || hasStrongSdk ? "medium" : "low",
|
|
122
|
+
uikitSignals,
|
|
123
|
+
sdkSignals,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (uikitSignals.length > 0) {
|
|
127
|
+
return guidanceFor({
|
|
128
|
+
recommendation: "uikit",
|
|
129
|
+
confidence: hasStrongUIKit ? "high" : "medium",
|
|
130
|
+
uikitSignals,
|
|
131
|
+
sdkSignals,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (sdkSignals.length > 0) {
|
|
135
|
+
return guidanceFor({
|
|
136
|
+
recommendation: "sdk",
|
|
137
|
+
confidence: hasStrongSdk ? "high" : "medium",
|
|
138
|
+
uikitSignals,
|
|
139
|
+
sdkSignals,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return guidanceFor({
|
|
143
|
+
recommendation: "sdk",
|
|
144
|
+
confidence: "medium",
|
|
145
|
+
uikitSignals,
|
|
146
|
+
sdkSignals,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function guidanceFor(args) {
|
|
150
|
+
const decision = {
|
|
151
|
+
requiredBeforeHandRolledUi: !args.answered && (args.recommendation === "uikit" || args.recommendation === "hybrid" || args.recommendation === "needs-decision"),
|
|
152
|
+
question: "Should this build use social.plus UIKit components, a direct SDK implementation, or a hybrid path?",
|
|
153
|
+
options: [
|
|
154
|
+
"uikit: use social.plus UIKit for standard social surfaces and theme/customize it",
|
|
155
|
+
"sdk: build the experience directly with the social.plus SDK",
|
|
156
|
+
"hybrid: use UIKit for standard surfaces and SDK/custom code for differentiated app-layer behavior",
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
if (args.recommendation === "uikit") {
|
|
160
|
+
return {
|
|
161
|
+
recommendation: "uikit",
|
|
162
|
+
confidence: args.confidence,
|
|
163
|
+
summary: args.answered === "uikit"
|
|
164
|
+
? "The customer explicitly selected the UIKit path. Confirm scope, then use social.plus UIKit as the primary implementation artifact instead of hand-rolling standard social UI."
|
|
165
|
+
: "The request sounds UIKit-shaped: favor social.plus UIKit for the standard social surface, then customize within the UIKit theming/component model before considering direct SDK UI.",
|
|
166
|
+
answerId: "solution_path",
|
|
167
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
168
|
+
decision,
|
|
169
|
+
implementationGuidance: [
|
|
170
|
+
"Confirm `solution_path=uikit` before the host agent hand-rolls standard feed, chat, profile, comments, community, or story UI.",
|
|
171
|
+
"Use UIKit installation, authentication, customization, and platform build guidance as the implementation source of truth.",
|
|
172
|
+
"Keep Vise's existing SDK setup, secrets, region, auth, build, and project-sensor guidance around the UIKit integration.",
|
|
173
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
174
|
+
"Keep design conformance advisory: use Dynamic UI, component styling, or fork-and-extend based on the customer's customization need.",
|
|
175
|
+
],
|
|
176
|
+
evidence: SOURCE_EVIDENCE,
|
|
177
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (args.recommendation === "needs-decision") {
|
|
181
|
+
return {
|
|
182
|
+
recommendation: "needs-decision",
|
|
183
|
+
confidence: args.confidence,
|
|
184
|
+
summary: args.answered === "hybrid"
|
|
185
|
+
? "The customer selected a hybrid path. Treat UIKit as the default for standard surfaces and use direct SDK/custom code only for the differentiated behavior."
|
|
186
|
+
: "The request contains both UIKit-shaped speed/prebuilt signals and SDK-shaped custom-control signals. Ask for the solution path before the agent starts UI implementation.",
|
|
187
|
+
answerId: "solution_path",
|
|
188
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
189
|
+
decision,
|
|
190
|
+
implementationGuidance: [
|
|
191
|
+
"Resolve `solution_path` before hand-rolling standard social UI.",
|
|
192
|
+
"Choose UIKit when the standard social surface can be themed or lightly customized.",
|
|
193
|
+
"Choose SDK when the requested workflow, layout, or interaction model is materially different from UIKit's component model.",
|
|
194
|
+
"Choose hybrid when UIKit can cover baseline social surfaces and SDK/custom code is needed for app-specific extensions.",
|
|
195
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
196
|
+
],
|
|
197
|
+
evidence: SOURCE_EVIDENCE,
|
|
198
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (args.recommendation === "hybrid") {
|
|
202
|
+
return {
|
|
203
|
+
recommendation: "hybrid",
|
|
204
|
+
confidence: args.confidence,
|
|
205
|
+
summary: "The customer selected a hybrid path. Treat UIKit as the default for standard surfaces and use direct SDK/custom code only for the differentiated behavior.",
|
|
206
|
+
answerId: "solution_path",
|
|
207
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
208
|
+
decision,
|
|
209
|
+
implementationGuidance: [
|
|
210
|
+
"Use UIKit for standard social surfaces that fit its component and customization model.",
|
|
211
|
+
"Use direct SDK/custom code only for differentiated app-layer behavior, custom profile/discovery surfaces, or non-standard flows.",
|
|
212
|
+
"Keep the handoff explicit in the implementation summary: name which surfaces are UIKit-owned and which are SDK/customer-owned.",
|
|
213
|
+
"Do not claim deterministic compliance for UIKit internals hidden outside the customer repo; validate customer wiring, configuration, custom code, and local build evidence.",
|
|
214
|
+
],
|
|
215
|
+
evidence: SOURCE_EVIDENCE,
|
|
216
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
recommendation: "sdk",
|
|
221
|
+
confidence: args.confidence,
|
|
222
|
+
summary: args.answered === "sdk"
|
|
223
|
+
? "The customer explicitly selected the direct SDK path. Continue with the normal Vise SDK integration harness."
|
|
224
|
+
: args.sdkSignals.some((signal) => signal.id === "reject-uikit")
|
|
225
|
+
? "The request explicitly rejects UIKit or chooses the SDK instead of UIKit, so the normal SDK-first Vise harness is the safer fit."
|
|
226
|
+
: args.sdkSignals.length > 0
|
|
227
|
+
? "The request asks for custom control, unique flows, or deep app integration, so the normal SDK-first Vise harness is the safer fit."
|
|
228
|
+
: "No strong UIKit fast-launch/prebuilt-surface signal was detected, so Vise keeps the existing SDK-first integration flow.",
|
|
229
|
+
answerId: "solution_path",
|
|
230
|
+
signals: { uikit: args.uikitSignals, sdk: args.sdkSignals },
|
|
231
|
+
decision,
|
|
232
|
+
implementationGuidance: [
|
|
233
|
+
"Use the normal Vise SDK integration flow: plan, answer intake, initialize the compliance contract, implement, check, and run sensors.",
|
|
234
|
+
"If the customer later asks for a standard prebuilt social surface or quick-launch MVP, re-plan with `--answer solution_path=uikit` or a UIKit-specific request.",
|
|
235
|
+
],
|
|
236
|
+
evidence: SOURCE_EVIDENCE,
|
|
237
|
+
advisoryOnly: "This is an advisory solution-path recommendation. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function collectSignals(request, signals) {
|
|
241
|
+
return signals.flatMap((signal) => {
|
|
242
|
+
const match = request.match(signal.pattern);
|
|
243
|
+
if (!match?.[0]) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
return [{
|
|
247
|
+
id: signal.id,
|
|
248
|
+
label: signal.label,
|
|
249
|
+
strength: signal.strength,
|
|
250
|
+
matched: match[0],
|
|
251
|
+
}];
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const REJECT_UIKIT_IN_ANSWER = new RegExp(REJECT_UIKIT_SOURCE, "i");
|
|
255
|
+
const REJECT_HYBRID_IN_ANSWER = /\b(?:not|no|without|avoid|don't|dont|do not|never|rather than|instead of)\b[\s\w,]{0,12}?\bhybrid\b/;
|
|
256
|
+
function normalizeSolutionPathAnswer(answer) {
|
|
257
|
+
const normalized = (typeof answer === "string" ? answer : "").trim().toLowerCase();
|
|
258
|
+
if (!normalized) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
if (/\bhybrid\b/.test(normalized) && !REJECT_HYBRID_IN_ANSWER.test(normalized)) {
|
|
262
|
+
return "hybrid";
|
|
263
|
+
}
|
|
264
|
+
if (REJECT_UIKIT_IN_ANSWER.test(normalized)) {
|
|
265
|
+
return "sdk";
|
|
266
|
+
}
|
|
267
|
+
if (/\b(ui\s?kit|uikit)\b/.test(normalized)) {
|
|
268
|
+
return "uikit";
|
|
269
|
+
}
|
|
270
|
+
if (/\bsdk\b/.test(normalized)) {
|
|
271
|
+
return "sdk";
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
package/dist/tools/harness.js
CHANGED
|
@@ -160,10 +160,38 @@ export async function detectCommandSensors(repoPath, platforms) {
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
if (platforms.includes("ios")) {
|
|
163
|
+
sensors.push(...(await swiftPackageSensors(root)));
|
|
163
164
|
sensors.push(...(await iosBuildSensors(root)));
|
|
164
165
|
}
|
|
165
166
|
return sensors;
|
|
166
167
|
}
|
|
168
|
+
async function swiftPackageSensors(root) {
|
|
169
|
+
if (!(await exists(path.join(root, "Package.swift")))) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
if (!(await commandOnPath("swift"))) {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
name: "iOS SwiftPM manifest",
|
|
176
|
+
command: ["swift"],
|
|
177
|
+
timing: "after-change",
|
|
178
|
+
purpose: "Parse the SwiftPM manifest after iOS SDK setup changes.",
|
|
179
|
+
source: "Package.swift",
|
|
180
|
+
skip: "Package.swift was detected, but swift is not on PATH in this environment. Install Xcode or Swift toolchain support to enable the SwiftPM manifest sensor; static iOS rule checks run regardless.",
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
name: "iOS SwiftPM manifest",
|
|
187
|
+
command: ["swift", "package", "describe", "--type", "json"],
|
|
188
|
+
timing: "after-change",
|
|
189
|
+
purpose: "Parse the SwiftPM manifest after iOS SDK setup changes without compiling or requiring simulator/signing assets.",
|
|
190
|
+
source: "Package.swift",
|
|
191
|
+
timeoutReason: "SwiftPM manifest parsing (`swift package describe`) did not finish before the sensor timeout. This is reported as a timeout (NOT a clean pass): a stall can be a slow toolchain OR an unresolvable/circular dependency. Static iOS rule checks still run; rerun with a longer `--timeout-ms` if the toolchain is just slow.",
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
167
195
|
const XCODEBUILD_ENVIRONMENT_SKIPS = [
|
|
168
196
|
{
|
|
169
197
|
pattern: "requires Xcode|command line tools instance",
|
|
@@ -330,8 +358,8 @@ function assessHarnessability(platforms, commandSensors, designSignalCount) {
|
|
|
330
358
|
if (designSignalCount > 0) {
|
|
331
359
|
affordances.push(`Detected ${designSignalCount} design/theme signal(s) for UI integration grounding.`);
|
|
332
360
|
}
|
|
333
|
-
if (platforms.includes("ios")) {
|
|
334
|
-
gaps.push("iOS: static compliance rules are fully operational
|
|
361
|
+
if (platforms.includes("ios") && commandSensors.length === 0) {
|
|
362
|
+
gaps.push("iOS: static compliance rules are fully operational, but no SwiftPM or Xcode command sensor was detected for this project root.");
|
|
335
363
|
}
|
|
336
364
|
if (platforms.length === 0) {
|
|
337
365
|
return { level: "weak", affordances, gaps };
|
|
@@ -10,6 +10,8 @@ import { detectCommandSensors } from "./harness.js";
|
|
|
10
10
|
import { inspectProject } from "./project.js";
|
|
11
11
|
import { creativeSurfaceHints, readCreativeSelection } from "./creative.js";
|
|
12
12
|
import { buildUxHarness, uxHarnessPlanContext } from "./uxHarness.js";
|
|
13
|
+
import { recommendSolutionPath } from "../solutionPath.js";
|
|
14
|
+
import { recommendUIKitCustomization } from "../uikitCustomization.js";
|
|
13
15
|
export const planIntegrationTool = {
|
|
14
16
|
name: "plan_integration",
|
|
15
17
|
description: "Create a grounded, evidence-backed implementation packet before an AI coding agent edits a customer project.",
|
|
@@ -72,7 +74,7 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
72
74
|
const uxHarnessOutcome = BROAD_SOCIAL_REGEX.test(request) && !hasAnswer(answers, "feature_surface") ? undefined : outcome;
|
|
73
75
|
const uxHarnessContext = uxHarness ? uxHarnessPlanContext(uxHarness, uxHarnessOutcome) : undefined;
|
|
74
76
|
const platform = preferredPlatform(inspection.platforms);
|
|
75
|
-
const
|
|
77
|
+
const baseSupportLevel = supportFor(outcome, platform);
|
|
76
78
|
const sensors = await detectCommandSensors(root, inspection.platforms);
|
|
77
79
|
const ctx = planContextFor({
|
|
78
80
|
request,
|
|
@@ -84,6 +86,9 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
84
86
|
});
|
|
85
87
|
const definition = getOutcomeDefinition(outcome);
|
|
86
88
|
const capabilityAvailability = await platformCapabilityAvailability(outcome, platform);
|
|
89
|
+
const solutionPath = recommendSolutionPath(request, answers);
|
|
90
|
+
const uikitCustomization = recommendUIKitCustomization({ request, answers, platform, solutionPath });
|
|
91
|
+
const supportLevel = supportLevelForPlan(outcome, platform, baseSupportLevel, uikitCustomization);
|
|
87
92
|
const designContract = await readDesignContract(repoRoot);
|
|
88
93
|
const designReview = designReviewGuidance(repoRoot, designContract, answers);
|
|
89
94
|
const acceptedDesignContract = designReview.status === "accepted" ? designContract : null;
|
|
@@ -108,6 +113,8 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
108
113
|
const packageJsonText = await readFile(path.join(root, "package.json"), "utf8").catch(() => undefined);
|
|
109
114
|
const sdkVersion = await sdkVersionGuidance(platform, packageJsonText);
|
|
110
115
|
const decisionsRequired = [
|
|
116
|
+
...solutionPathDecisions(solutionPath, answers),
|
|
117
|
+
...uikitCustomizationDecisions(uikitCustomization, answers),
|
|
111
118
|
...(creativeContext
|
|
112
119
|
? [
|
|
113
120
|
`[selected creative variant] Carry "${creativeContext.selectedVariant.title}" into the implementation plan. If any selected experience object is intentionally deferred, state that scope decision explicitly.`,
|
|
@@ -125,8 +132,10 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
125
132
|
return {
|
|
126
133
|
outcome,
|
|
127
134
|
platform,
|
|
135
|
+
solutionPath,
|
|
136
|
+
uikitCustomization,
|
|
128
137
|
supportLevel,
|
|
129
|
-
intent: intentFor(request, definition.interpretation),
|
|
138
|
+
intent: intentFor(request, definition.interpretation, uikitCustomization),
|
|
130
139
|
creativeContext,
|
|
131
140
|
uxHarness: uxHarnessContext,
|
|
132
141
|
socialWorkplan,
|
|
@@ -136,24 +145,28 @@ async function buildIntegrationPlan(repoPath, request, surfacePath, answers = {}
|
|
|
136
145
|
implementationSteps: [
|
|
137
146
|
...creativeImplementationSteps(creativeSelection),
|
|
138
147
|
...uxHarnessImplementationSteps(uxHarnessContext),
|
|
148
|
+
...solutionPathImplementationSteps(solutionPath),
|
|
149
|
+
...uikitCustomizationImplementationSteps(uikitCustomization),
|
|
139
150
|
...definition.implementationSteps(ctx),
|
|
140
151
|
],
|
|
141
152
|
validation: ["validate_setup", "run_sensors", ...definition.validation(platform)],
|
|
142
|
-
nextStep:
|
|
153
|
+
nextStep: nextStepForPlan(outcome, uikitCustomization, solutionPath),
|
|
143
154
|
requiredInputs: composeRequiredInputs(ctx, definition.requiredInputs(ctx)),
|
|
144
155
|
targetFiles: await targetFilesFor(root, outcome, platform, inspection.designSignals),
|
|
145
156
|
implementationRules: [
|
|
146
157
|
...composeImplementationRules(ctx, definition.implementationRules(ctx)),
|
|
147
158
|
...creativeImplementationRules(creativeSelection),
|
|
148
159
|
...uxHarnessImplementationRules(uxHarnessContext),
|
|
160
|
+
...solutionPathImplementationRules(solutionPath),
|
|
161
|
+
...uikitCustomizationImplementationRules(uikitCustomization),
|
|
149
162
|
],
|
|
150
163
|
intake,
|
|
151
|
-
docs: definition.docs(platform)
|
|
164
|
+
docs: docsForPlan(definition.docs(platform), uikitCustomization),
|
|
152
165
|
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
153
166
|
availableSurfaces: inspection.surfaces,
|
|
154
167
|
applicableRules: await applicableCompliancePlanRuleSummaries(outcome, inspection.platforms),
|
|
155
168
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
156
|
-
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath, Boolean(socialWorkplan)),
|
|
169
|
+
stopConditions: composeStopConditions(ctx, definition.stopConditions(ctx), inspection.surfaces, surfacePath, Boolean(socialWorkplan), uikitCustomization),
|
|
157
170
|
evidencePolicy: "Every implementation step must cite at least one detected file, docs page, validator rule, or required user input. If evidence is missing, stop and ask the user instead of inventing details.",
|
|
158
171
|
designContract: acceptedDesignContract ? designContractGuidance(acceptedDesignContract) : undefined,
|
|
159
172
|
completenessChecklist: completenessChecklistFor(outcome),
|
|
@@ -188,6 +201,137 @@ function optionalCapabilitiesFor(outcome, answers, request, availability) {
|
|
|
188
201
|
selected: selectedOptionalCapabilityIds(outcome, answers, request, availableIds),
|
|
189
202
|
};
|
|
190
203
|
}
|
|
204
|
+
function solutionPathDecisions(solutionPath, answers) {
|
|
205
|
+
if (hasAnswer(answers, solutionPath.answerId)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
if (!solutionPath.decision.requiredBeforeHandRolledUi) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return [
|
|
212
|
+
`[solution path] ${solutionPath.summary} Answer with --answer ${solutionPath.answerId}=uikit, =sdk, or =hybrid before hand-rolling standard social UI.`,
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
function solutionPathImplementationSteps(solutionPath) {
|
|
216
|
+
if (!solutionPath.decision.requiredBeforeHandRolledUi) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
step: solutionPath.recommendation === "uikit"
|
|
222
|
+
? "Confirm the social.plus UIKit path before building standard social UI from SDK primitives; use UIKit install/auth/customization docs as the primary implementation source. The SDK feed/chat/profile steps that follow apply ONLY to the direct-SDK (or hybrid SDK-side) route — do not hand-roll the standard surface from them when the customer is using UIKit."
|
|
223
|
+
: "Resolve whether this is a UIKit, SDK, or hybrid build before starting UI implementation; the SDK implementation steps that follow apply to the SDK/hybrid route, not to a UIKit build. Do not let mixed speed/customization signals collapse into a hand-rolled SDK UI by default.",
|
|
224
|
+
evidence: ["solutionPath", ...solutionPath.evidence],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
function solutionPathImplementationRules(solutionPath) {
|
|
229
|
+
if (solutionPath.recommendation === "sdk" && !solutionPath.decision.requiredBeforeHandRolledUi) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return [
|
|
233
|
+
solutionPath.advisoryOnly,
|
|
234
|
+
...solutionPath.implementationGuidance,
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
function uikitCustomizationDecisions(uikitCustomization, answers) {
|
|
238
|
+
if (!uikitCustomization) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
if (hasAnswer(answers, uikitCustomization.answerId)) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
if (uikitCustomization.decision.requiredBeforeCustomization) {
|
|
245
|
+
return [
|
|
246
|
+
`[UIKit customization] ${uikitCustomization.summary} Answer with --answer ${uikitCustomization.answerId}=dynamic-ui, =component-styling, =localization, =behavior-overrides, =fork-and-extend, =sdk-custom-ui, or =hybrid before editing UIKit UI/customization code.`,
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
return [
|
|
250
|
+
`[UIKit customization] ${uikitCustomization.summary} State this chosen route in the implementation summary, or re-plan with --answer ${uikitCustomization.answerId}=<route> if the customer chooses a different level.`,
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
function uikitCustomizationImplementationSteps(uikitCustomization) {
|
|
254
|
+
if (!uikitCustomization) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
step: `Apply the UIKit customization ladder before writing custom UI: start with ${uikitCustomization.recommendedLevel}, then escalate only if the customer goal cannot be expressed there.`,
|
|
260
|
+
evidence: ["uikitCustomization", ...uikitCustomization.evidence],
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
function uikitCustomizationImplementationRules(uikitCustomization) {
|
|
265
|
+
if (!uikitCustomization) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
return [
|
|
269
|
+
uikitCustomization.advisoryOnly,
|
|
270
|
+
...uikitCustomization.implementationGuidance,
|
|
271
|
+
...uikitCustomization.platformGuidance,
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
function docsForPlan(outcomeDocs, uikitCustomization) {
|
|
275
|
+
const docs = outcomeDocs.filter((doc) => doc.path !== "unknown");
|
|
276
|
+
if (!uikitCustomization) {
|
|
277
|
+
return docs;
|
|
278
|
+
}
|
|
279
|
+
const customizationLabel = labelForUIKitCustomizationLevel(uikitCustomization.recommendedLevel);
|
|
280
|
+
for (const docPath of uikitCustomization.docs) {
|
|
281
|
+
docs.push({
|
|
282
|
+
path: docPath,
|
|
283
|
+
reason: `UIKit ${customizationLabel} customization guidance for this solution path.`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return dedupeDocsByPath(docs);
|
|
287
|
+
}
|
|
288
|
+
function supportLevelForPlan(outcome, platform, baseSupportLevel, uikitCustomization) {
|
|
289
|
+
if (platform !== "unknown" && outcome === "unknown" && uikitCustomization) {
|
|
290
|
+
return "guided";
|
|
291
|
+
}
|
|
292
|
+
return baseSupportLevel;
|
|
293
|
+
}
|
|
294
|
+
function nextStepForPlan(outcome, uikitCustomization, solutionPath) {
|
|
295
|
+
if (outcome === "unknown" && uikitCustomization) {
|
|
296
|
+
return "Treat this as UIKit customization guidance first: resolve the UIKit route and concrete app/platform target, then implement customer-owned config, localization, behavior, fork, or SDK custom UI changes with local build evidence. Do not claim `vise check` deterministically validates hidden UIKit internals.";
|
|
297
|
+
}
|
|
298
|
+
if (solutionPath.decision.requiredBeforeHandRolledUi) {
|
|
299
|
+
return "First resolve the solution-path decision (UIKit vs SDK vs hybrid). The SDK implementation steps above apply only if you build this surface directly with the SDK (or the SDK side of a hybrid); if the customer uses social.plus UIKit for the standard surface, follow the UIKit guidance instead of hand-rolling those steps. Once the route is settled, implement the applicable steps, then run `vise check .` and fix findings until green — you are not done until the check passes or each finding is explicitly attested.";
|
|
300
|
+
}
|
|
301
|
+
return "After implementing every step above, run `vise check .` and fix findings until green. You are not done until the check passes or each finding is explicitly attested.";
|
|
302
|
+
}
|
|
303
|
+
function dedupeDocsByPath(docs) {
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
const out = [];
|
|
306
|
+
for (const doc of docs) {
|
|
307
|
+
if (seen.has(doc.path)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
seen.add(doc.path);
|
|
311
|
+
out.push(doc);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
function labelForUIKitCustomizationLevel(level) {
|
|
316
|
+
switch (level) {
|
|
317
|
+
case "dynamic-ui":
|
|
318
|
+
return "Dynamic UI";
|
|
319
|
+
case "component-styling":
|
|
320
|
+
return "Component Styling";
|
|
321
|
+
case "localization":
|
|
322
|
+
return "Localization";
|
|
323
|
+
case "behavior-overrides":
|
|
324
|
+
return "Behavior Overrides";
|
|
325
|
+
case "fork-and-extend":
|
|
326
|
+
return "Fork and Extend";
|
|
327
|
+
case "sdk-custom-ui":
|
|
328
|
+
return "SDK Custom UI";
|
|
329
|
+
case "hybrid":
|
|
330
|
+
return "Hybrid";
|
|
331
|
+
case "needs-decision":
|
|
332
|
+
return "decision";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
191
335
|
function creativeContextForPlan(selection) {
|
|
192
336
|
const selectedVariant = selection.selectedVariant;
|
|
193
337
|
const surfaceHints = creativeSurfaceHints(selection);
|
|
@@ -484,13 +628,17 @@ function designContractGuidance(contract) {
|
|
|
484
628
|
advisoryOnly: "This contract is advisory generation guidance — it adds no deterministic enforcement and never fails `vise check`.",
|
|
485
629
|
};
|
|
486
630
|
}
|
|
487
|
-
function intentFor(request, interpretation) {
|
|
631
|
+
function intentFor(request, interpretation, uikitCustomization) {
|
|
488
632
|
const broadSocialRequest = BROAD_SOCIAL_REGEX.test(request);
|
|
489
633
|
const designRequest = DESIGN_REGEX.test(request);
|
|
634
|
+
const uikitOnlyCustomization = interpretation === "Implement unknown." && uikitCustomization;
|
|
635
|
+
const uikitInterpretation = uikitOnlyCustomization
|
|
636
|
+
? `${uikitCustomization.summary} No deterministic SDK outcome was selected, so keep this as guided UIKit customization until the customer names the concrete social surface or app-owned customization target.`
|
|
637
|
+
: interpretation;
|
|
490
638
|
return {
|
|
491
639
|
rawRequest: request,
|
|
492
|
-
interpretation,
|
|
493
|
-
ambiguity: broadSocialRequest || designRequest ? "high" : "medium",
|
|
640
|
+
interpretation: uikitInterpretation,
|
|
641
|
+
ambiguity: broadSocialRequest || designRequest || uikitCustomization?.status === "needs-decision" || uikitOnlyCustomization ? "high" : "medium",
|
|
494
642
|
};
|
|
495
643
|
}
|
|
496
644
|
function intakeFor(ctx, outcomeQuestions, outcome, brief, availability, designReview) {
|
|
@@ -611,15 +759,18 @@ function composeImplementationRules(ctx, outcomeRules) {
|
|
|
611
759
|
}
|
|
612
760
|
return rules;
|
|
613
761
|
}
|
|
614
|
-
function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath, hasSocialWorkplan = false) {
|
|
762
|
+
function composeStopConditions(ctx, outcomeStops, surfaces, surfacePath, hasSocialWorkplan = false, uikitCustomization) {
|
|
615
763
|
const stops = [
|
|
616
764
|
"A required secret is missing and no safe ignored local env file or non-secret template path is clear.",
|
|
617
765
|
"The target file is ambiguous or missing and no safe conventional location is detected.",
|
|
618
766
|
"Docs lookup does not return the canonical page named in this plan.",
|
|
619
767
|
];
|
|
620
|
-
if (ctx.platform === "unknown" || ctx.outcome === "unknown") {
|
|
768
|
+
if (ctx.platform === "unknown" || (ctx.outcome === "unknown" && !uikitCustomization)) {
|
|
621
769
|
stops.push("The request or platform is unsupported; do not implement until clarified.");
|
|
622
770
|
}
|
|
771
|
+
if (ctx.outcome === "unknown" && uikitCustomization) {
|
|
772
|
+
stops.push("No deterministic SDK outcome was selected; do not run `vise init` or claim compliance coverage until a concrete social surface or app-owned UIKit customization target is confirmed.");
|
|
773
|
+
}
|
|
623
774
|
if (ctx.platforms.length > 1 && !surfacePath) {
|
|
624
775
|
stops.push(`Multiple platform signals detected (${ctx.platforms.join(", ")}); confirm which app surface should be modified.`);
|
|
625
776
|
}
|
package/dist/tools/project.js
CHANGED
|
@@ -204,7 +204,7 @@ async function validateAndroid(root) {
|
|
|
204
204
|
const loginFiles = filesMatching(sourceContent, [/AmityCoreClient\s*\.\s*login/, /AmityClient\s*\.\s*login/]);
|
|
205
205
|
const pushRegistrationFiles = filesMatching(sourceContent, [/registerPushNotification/, /enablePushNotification/, /PushNotification/]);
|
|
206
206
|
const pushUnregisterFiles = filesMatching(sourceContent, [/unregisterPushNotification/, /disablePushNotification/, /unregister.*DeviceToken/i]);
|
|
207
|
-
const liveDataFiles = filesMatching(sourceContent, [/LiveCollection/, /LiveObject/, /\.observe\s*\(/,
|
|
207
|
+
const liveDataFiles = filesMatching(sourceContent, [/LiveCollection/, /LiveObject/, /\.observe\s*\(/, /AmitySocialClient\s*\.\s*newPostRepository/, /\bgetPosts\s*\(/, /queryPosts\s*\(/, /getPost\s*\(/]);
|
|
208
208
|
if (!manifest) {
|
|
209
209
|
findings.push(finding("android.manifest.present", "warning", "No AndroidManifest.xml found at the default app path.", manifestPath, "Confirm the Android app module path, then validate permissions and Application wiring."));
|
|
210
210
|
}
|
|
@@ -317,8 +317,8 @@ async function validateFlutter(root) {
|
|
|
317
317
|
const dartContent = await readMany(dartFiles);
|
|
318
318
|
const setupFiles = filesMatching(dartContent, [/AmityCoreClient\s*\.\s*setup/]);
|
|
319
319
|
const loginFiles = filesMatching(dartContent, [/AmityCoreClient\s*\.\s*login/]);
|
|
320
|
-
const pushRegistrationFiles = filesMatching(dartContent, [/registerPushNotification/, /enablePushNotification/, /PushNotification/]);
|
|
321
|
-
const pushUnregisterFiles = filesMatching(dartContent, [/unregisterPushNotification/, /disablePushNotification/]);
|
|
320
|
+
const pushRegistrationFiles = filesMatching(dartContent, [/registerPushNotification/, /registerDeviceNotification/, /enablePushNotification/, /PushNotification/]);
|
|
321
|
+
const pushUnregisterFiles = filesMatching(dartContent, [/unregisterPushNotification/, /unregisterDeviceNotification/, /disablePushNotification/]);
|
|
322
322
|
const liveDataFiles = filesMatching(dartContent, [/LiveCollection/, /LiveObject/, /\.listen\s*\(/, /\.observe\s*\(/, /queryPosts\s*\(/, /getPost\s*\(/]);
|
|
323
323
|
if (!pubspec) {
|
|
324
324
|
findings.push(finding("flutter.pubspec.present", "warning", "No pubspec.yaml file was found.", "pubspec.yaml", "Point repoPath at the Flutter project root."));
|
|
@@ -715,6 +715,9 @@ function validateFeedUiStates(root, platform, sourceContent) {
|
|
|
715
715
|
if (isNonUiSourceFile(file)) {
|
|
716
716
|
continue;
|
|
717
717
|
}
|
|
718
|
+
if (platform === "android" && /\bclass\s+\w+\s*:\s*Application\s*\(/.test(content)) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
718
721
|
const observes = observationPatterns.some((pattern) => pattern.test(content));
|
|
719
722
|
if (!observes) {
|
|
720
723
|
continue;
|
|
@@ -741,7 +744,7 @@ const UI_STATE_PATTERNS_BY_PLATFORM = {
|
|
|
741
744
|
typescript: [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bempty\b/i, /\.length\s*===?\s*0/, /\busestate\b/i],
|
|
742
745
|
"react-native": [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bempty\b/i, /\.length\s*===?\s*0/, /ActivityIndicator/],
|
|
743
746
|
flutter: [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bisEmpty\b/, /CircularProgressIndicator/, /AsyncSnapshot/, /ConnectionState/],
|
|
744
|
-
android: [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bempty\b/i, /CircularProgressIndicator/, /LoadingState/, /\.collectAsState\b/],
|
|
747
|
+
android: [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bempty\b/i, /CircularProgressIndicator/, /LoadingState/, /FeedUiState\.(?:Loading|Empty|Error)\b/, /\.collectAsState\b/],
|
|
745
748
|
ios: [/\bisLoading\b/i, /\bloading\b/, /\berror\b/, /\bempty\b/i, /ProgressView/, /LoadingState/, /AsyncImage/],
|
|
746
749
|
};
|
|
747
750
|
const FEED_LIST_OBSERVATION_PATTERNS_BY_PLATFORM = {
|
|
@@ -1434,7 +1437,9 @@ function validatePostsStatusFilter(root, platform, sourceContent) {
|
|
|
1434
1437
|
];
|
|
1435
1438
|
const FILTER_MARKERS = [
|
|
1436
1439
|
/\bfeedTypes?\s*[:(]/,
|
|
1440
|
+
/\breviewStatus\s*[:(]/,
|
|
1437
1441
|
/\bincludeDeleted\s*\(\s*false\s*\)/,
|
|
1442
|
+
/\bdeletedOption\s*:\s*\.notDeleted\b/,
|
|
1438
1443
|
/\bisDeleted\s*:\s*false\b/,
|
|
1439
1444
|
/\bisFlagged\s*:\s*false\b/,
|
|
1440
1445
|
/\bstatuses\s*[:(]/,
|
|
@@ -2296,6 +2301,9 @@ async function validateIos(root) {
|
|
|
2296
2301
|
else if (!containsAny(manifestContent, [/Amity/i, /AmitySDK/i])) {
|
|
2297
2302
|
findings.push(finding("ios.dependency.sdk", "warning", "No obvious social.plus/Amity dependency was found in Podfile or Package.swift.", relativeFile(root, manifestFiles[0]), "Add the iOS SDK dependency from the iOS quick-start docs."));
|
|
2298
2303
|
}
|
|
2304
|
+
else if (containsAny(manifestContent, [/Amity-Social-Cloud-SDK-iOS-IPA/i])) {
|
|
2305
|
+
findings.push(finding("ios.dependency.swiftpm-repo", "warning", "The iOS SwiftPM dependency points at the obsolete Amity-Social-Cloud-SDK-iOS-IPA package repository.", relativeFile(root, manifestFiles[0]), "Use https://github.com/AmityCo/Amity-Social-Cloud-SDK-iOS-SwiftPM.git with product AmitySDK so SwiftPM can resolve the social.plus iOS SDK."));
|
|
2306
|
+
}
|
|
2299
2307
|
else if (containsAny(manifestContent, [/\.package\s*\([^)]*\bbranch\s*:/s, /pod\s+['"][^'"]*Amity[^'"]*['"]\s*$/m])) {
|
|
2300
2308
|
findings.push(finding("ios.sdk.version.pinned", "warning", "The iOS SDK dependency appears to use a floating branch or unspecified CocoaPods version.", relativeFile(root, manifestFiles[0]), "Pin the social.plus iOS SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
|
|
2301
2309
|
}
|
|
@@ -2944,7 +2952,7 @@ function validateNotificationsPreferencesConfigured(root, platform, sourceConten
|
|
|
2944
2952
|
let hasPreferences = false;
|
|
2945
2953
|
let firstPushRegistrationFile = '';
|
|
2946
2954
|
for (const [filename, text] of sourceContent) {
|
|
2947
|
-
if (/\b(registerPushNotification|enablePushNotification)\b/.test(text)) {
|
|
2955
|
+
if (/\b(registerPushNotification|registerDeviceNotification|enablePushNotification)\b/.test(text)) {
|
|
2948
2956
|
hasPushRegistration = true;
|
|
2949
2957
|
if (!firstPushRegistrationFile) {
|
|
2950
2958
|
firstPushRegistrationFile = (typeof filename === 'string' && root) ? (filename.startsWith(root) ? filename.slice(root.length).replace(/^\//, '') : filename) : filename;
|