@amityco/social-plus-vise 1.1.1 → 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.
@@ -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. No build/compile sensor is wired yet (xcodebuild environment requirements make it fragile); run-sensors will return no-sensors for iOS projects.");
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 supportLevel = supportFor(outcome, platform);
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: "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.",
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).filter((doc) => doc.path !== "unknown"),
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
  }
@@ -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*\(/, /\.subscribe\s*\(/, /queryPosts\s*\(/, /getPost\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;
@@ -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) => {