@amityco/social-plus-vise 1.2.0 → 1.3.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 +30 -0
- package/README.md +4 -1
- package/dist/capabilities.js +6 -3
- package/dist/explore.js +51 -0
- package/dist/humanFormat.js +226 -0
- package/dist/outcomes.js +15 -14
- package/dist/server.js +110 -38
- package/dist/solutionPath.js +1 -1
- package/dist/tools/compliance.js +289 -35
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +24 -4
- package/dist/tools/project.js +26 -8
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/uikitCustomization.js +19 -5
- package/package.json +1 -1
package/dist/tools/design.js
CHANGED
|
@@ -764,11 +764,15 @@ export async function generateDesignReference(repoPath, contract, title) {
|
|
|
764
764
|
.join("\n ");
|
|
765
765
|
const digestShort = esc(contract.digest.slice(0, 23));
|
|
766
766
|
const logoLetter = esc(title.slice(0, 1).toUpperCase());
|
|
767
|
+
const rootDecls = [...tokenCss.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)]
|
|
768
|
+
.map((m) => ` ${m[1]}: ${safeCss(m[2].trim())};`)
|
|
769
|
+
.join("\n");
|
|
770
|
+
const rootCss = rootDecls ? `:root{\n${rootDecls}\n}` : "";
|
|
767
771
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
|
|
768
772
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
769
773
|
<title>${esc(title)} — Design System</title>
|
|
770
774
|
<style>
|
|
771
|
-
${
|
|
775
|
+
${rootCss}
|
|
772
776
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
773
777
|
html{scroll-behavior:smooth}
|
|
774
778
|
body{
|
|
@@ -1094,6 +1098,8 @@ export async function runDesignCheck(repoPath) {
|
|
|
1094
1098
|
const declaredTokens = contract.tokens.filter((token) => token.provenance === "declared");
|
|
1095
1099
|
const contractColorValues = new Set(contract.tokens.filter((token) => token.category === "color").map((token) => token.value));
|
|
1096
1100
|
const referenced = new Set();
|
|
1101
|
+
const referencedInApp = new Set();
|
|
1102
|
+
const seedTokensRel = SP_TOKENS_PATH.split("/").join(path.sep);
|
|
1097
1103
|
const colorSample = [];
|
|
1098
1104
|
const definedVars = new Set();
|
|
1099
1105
|
const varRefs = [];
|
|
@@ -1115,13 +1121,18 @@ export async function runDesignCheck(repoPath) {
|
|
|
1115
1121
|
scanned += 1;
|
|
1116
1122
|
const rel = path.relative(repoRoot, file);
|
|
1117
1123
|
const isCss = file.toLowerCase().endsWith(".css") || file.toLowerCase().endsWith(".scss");
|
|
1124
|
+
const isSeedFile = rel === SP_TOKENS_PATH || rel === seedTokensRel;
|
|
1125
|
+
const contentLower = content.toLowerCase();
|
|
1118
1126
|
for (const token of declaredTokens) {
|
|
1119
1127
|
const key = tokenKey(token);
|
|
1120
|
-
if (referenced.has(key)) {
|
|
1128
|
+
if (referenced.has(key) && referencedInApp.has(key)) {
|
|
1121
1129
|
continue;
|
|
1122
1130
|
}
|
|
1123
|
-
if ((token.name && content.includes(token.name)) ||
|
|
1131
|
+
if ((token.name && content.includes(token.name)) || contentLower.includes(token.value.toLowerCase())) {
|
|
1124
1132
|
referenced.add(key);
|
|
1133
|
+
if (!isSeedFile) {
|
|
1134
|
+
referencedInApp.add(key);
|
|
1135
|
+
}
|
|
1125
1136
|
}
|
|
1126
1137
|
}
|
|
1127
1138
|
for (const value of scanColorLiterals(content)) {
|
|
@@ -1144,12 +1155,19 @@ export async function runDesignCheck(repoPath) {
|
|
|
1144
1155
|
}
|
|
1145
1156
|
const referencedTokens = declaredTokens.filter((token) => referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
|
|
1146
1157
|
const unreferencedTokens = declaredTokens.filter((token) => !referenced.has(tokenKey(token))).map((token) => token.name ?? token.value);
|
|
1158
|
+
const seededOnlyTokens = declaredTokens
|
|
1159
|
+
.filter((token) => referenced.has(tokenKey(token)) && !referencedInApp.has(tokenKey(token)))
|
|
1160
|
+
.map((token) => token.name ?? token.value);
|
|
1147
1161
|
const contractTokenNames = new Set(contract.tokens.map((token) => token.name).filter((name) => Boolean(name)));
|
|
1148
1162
|
const undefinedRefs = dedupeByToken(varRefs.filter((ref) => !definedVars.has(ref.token) && !contractTokenNames.has(ref.token)));
|
|
1149
1163
|
return {
|
|
1150
1164
|
status: "advisory",
|
|
1151
1165
|
message: `Checked ${scanned} source file(s) against design contract ${contract.digest}. ` +
|
|
1152
|
-
`${referencedTokens.length}/${declaredTokens.length} declared tokens referenced
|
|
1166
|
+
`${referencedTokens.length}/${declaredTokens.length} declared tokens referenced` +
|
|
1167
|
+
(seededOnlyTokens.length > 0
|
|
1168
|
+
? ` (${referencedTokens.length - seededOnlyTokens.length} used in your code, ${seededOnlyTokens.length} only in the Vise-seeded ${SP_TOKENS_PATH} — apply them to count)`
|
|
1169
|
+
: "") +
|
|
1170
|
+
`; ${totalColors - onContract} of ${totalColors} color literal(s) are off-contract (review hints)` +
|
|
1153
1171
|
(undefinedRefs.length > 0 ? `; ${undefinedRefs.length} undefined token reference(s) (likely broken styles)` : "") +
|
|
1154
1172
|
".",
|
|
1155
1173
|
contract: contractSummary(contract),
|
|
@@ -1158,6 +1176,8 @@ export async function runDesignCheck(repoPath) {
|
|
|
1158
1176
|
referenced: referencedTokens.length,
|
|
1159
1177
|
referenced_tokens: referencedTokens,
|
|
1160
1178
|
unreferenced_tokens: unreferencedTokens,
|
|
1179
|
+
referenced_in_app: referencedTokens.length - seededOnlyTokens.length,
|
|
1180
|
+
seeded_only_tokens: seededOnlyTokens,
|
|
1161
1181
|
},
|
|
1162
1182
|
colorLiterals: {
|
|
1163
1183
|
scanned_files: scanned,
|
package/dist/tools/project.js
CHANGED
|
@@ -220,7 +220,7 @@ async function validateAndroid(root) {
|
|
|
220
220
|
findings.push(finding("android.dependency.sdk", "warning", "No obvious social.plus Android SDK dependency was found in Gradle files.", relativeFile(root, buildFiles[0]), "Add the SDK dependency from the Android quick-start docs and keep all Amity dependencies on compatible versions."));
|
|
221
221
|
}
|
|
222
222
|
else if (containsAny(buildContent, [/co\.amity\.android:amity-sdk:\+/, /co\.amity\.android:amity-sdk:latest/i, /version\s*=\s*["']\+["']/])) {
|
|
223
|
-
findings.push(finding("android.sdk.version.pinned", "warning", "The Android SDK dependency
|
|
223
|
+
findings.push(finding("android.sdk.version.pinned", "warning", "The Android SDK dependency uses an uncontrolled version (e.g. latest / + / a moving branch), not a pinned version or reviewed semver range.", relativeFile(root, buildFiles[0]), "Pin the social.plus Android SDK to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
|
|
224
224
|
}
|
|
225
225
|
if (setupFiles.length === 0) {
|
|
226
226
|
findings.push(finding("android.setup.present", "warning", "No obvious AmityCoreClient.setup call was found in Kotlin/Java files.", undefined, "Call SDK setup once during application startup before any social.plus API usage."));
|
|
@@ -327,7 +327,7 @@ async function validateFlutter(root) {
|
|
|
327
327
|
findings.push(finding("flutter.dependency.sdk", "warning", "No obvious social.plus/Amity dependency was found in pubspec.yaml.", "pubspec.yaml", "Add the Flutter SDK dependency from the Flutter quick-start docs."));
|
|
328
328
|
}
|
|
329
329
|
else if (/\bamity_sdk\s*:\s*(?:any|\*|latest)\b/i.test(pubspec)) {
|
|
330
|
-
findings.push(finding("flutter.sdk.version.pinned", "warning", "The Flutter SDK dependency
|
|
330
|
+
findings.push(finding("flutter.sdk.version.pinned", "warning", "The Flutter SDK dependency uses an uncontrolled version (e.g. any / a moving git ref), not a pinned version or reviewed semver range.", "pubspec.yaml", "Pin amity_sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
|
|
331
331
|
}
|
|
332
332
|
if (setupFiles.length === 0) {
|
|
333
333
|
findings.push(finding("flutter.setup.present", "warning", "No obvious AmityCoreClient.setup call was found in lib/*.dart.", undefined, "Call AmityCoreClient.setup before any social.plus API usage."));
|
|
@@ -424,7 +424,7 @@ async function validateTypeScript(root, platform) {
|
|
|
424
424
|
findings.push(finding("typescript.dependency.sdk", "warning", "No obvious social.plus/Amity package was found in package.json.", "package.json", "Add the TypeScript SDK dependency from the web quick-start docs."));
|
|
425
425
|
}
|
|
426
426
|
else if (isFloatingPackageDependency(packageJson, "@amityco/ts-sdk")) {
|
|
427
|
-
findings.push(finding(`${platform}.sdk.version.pinned`, "warning", "The TypeScript SDK dependency
|
|
427
|
+
findings.push(finding(`${platform}.sdk.version.pinned`, "warning", "The TypeScript SDK dependency uses an uncontrolled version (latest / * / x), not a pinned version or reviewed semver range (^/~ is accepted).", "package.json", "Pin @amityco/ts-sdk to an explicit version or reviewed semver range so CI does not pick up unreviewed SDK APIs."));
|
|
428
428
|
}
|
|
429
429
|
if (setupFiles.length === 0) {
|
|
430
430
|
findings.push(finding("typescript.client.create", "warning", "No obvious TypeScript client initialization pattern was found.", undefined, "Create the social.plus client before login and before API usage."));
|
|
@@ -940,13 +940,18 @@ function validateComments(root, platform, sourceContent) {
|
|
|
940
940
|
break;
|
|
941
941
|
}
|
|
942
942
|
}
|
|
943
|
-
const
|
|
943
|
+
const isObserverContent = (c) => {
|
|
944
944
|
const stripped = c.replace(/\bawait\b[^\n;]*?(?:get|query)Comments\s*\(/g, "");
|
|
945
945
|
return /(?:get|query)Comments\s*\(/.test(stripped)
|
|
946
946
|
|| /CommentLiveCollection|AmityCommentCollection|commentCollection/.test(c);
|
|
947
|
-
}
|
|
947
|
+
};
|
|
948
|
+
const observerFiles = [...sourceContent.entries()].filter(([, c]) => isObserverContent(c)).map(([f]) => f);
|
|
949
|
+
const hasReactiveCommentObserver = observerFiles.length > 0;
|
|
948
950
|
const cleanupMarkers = COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM[platform] ?? COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM.typescript;
|
|
949
|
-
|
|
951
|
+
const ANDROID_OBSERVER_SELF_CLEAN = /(?:(?:get|query)Comments\s*\(|CommentLiveCollection|AmityCommentCollection|commentCollection)[^;{}]{0,300}?(?:\.stateIn\s*\([^;{}]*\bviewModelScope\b|\.asLiveData\s*\(|\.collectAsStateWithLifecycle\s*\(|\brepeatOnLifecycle\b|\bDisposableEffect\b)/;
|
|
952
|
+
const observerSelfCleans = platform === "android" &&
|
|
953
|
+
observerFiles.some((file) => ANDROID_OBSERVER_SELF_CLEAN.test(commentStripped(file, platform, sourceContent.get(file) ?? "")));
|
|
954
|
+
if (commentFiles.length > 0 && hasReactiveCommentObserver && !observerSelfCleans && !containsAny(sourceContent, cleanupMarkers)) {
|
|
950
955
|
findings.push(finding(`${platform}.comments.observer-cleanup`, "warning", "Comment observer/subscription was found but no obvious cleanup was detected.", relativeFile(root, commentFiles[0]), "Dispose or unsubscribe comment observers when the lifecycle owner is destroyed."));
|
|
951
956
|
}
|
|
952
957
|
const strippedCommentFiles = new Map();
|
|
@@ -1032,6 +1037,16 @@ const CHAT_PRESENCE_PATTERNS = [
|
|
|
1032
1037
|
/\bchatClient\b/i,
|
|
1033
1038
|
/\bmessage\s*live\s*collection\b/i,
|
|
1034
1039
|
];
|
|
1040
|
+
const CHAT_SEND_CALL_PATTERNS = [
|
|
1041
|
+
/\bsendMessage\s*\(/,
|
|
1042
|
+
/\bcreateMessage\b/,
|
|
1043
|
+
/\bcreateTextMessage\b/,
|
|
1044
|
+
/\bcreateImageMessage\b/,
|
|
1045
|
+
/\bcreateFileMessage\b/,
|
|
1046
|
+
/\bcreateVideoMessage\b/,
|
|
1047
|
+
/\bcreateAudioMessage\b/,
|
|
1048
|
+
/\bcreateCustomMessage\b/,
|
|
1049
|
+
];
|
|
1035
1050
|
const CHAT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM = {
|
|
1036
1051
|
typescript: [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
|
|
1037
1052
|
"react-native": [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
|
|
@@ -1071,6 +1086,7 @@ function validateChat(root, platform, sourceContent) {
|
|
|
1071
1086
|
const chatFileContent = new Map();
|
|
1072
1087
|
for (const f of chatFiles)
|
|
1073
1088
|
chatFileContent.set(f, sourceContent.get(f) ?? "");
|
|
1089
|
+
const hasSendCall = containsAny(chatFileContent, CHAT_SEND_CALL_PATTERNS);
|
|
1074
1090
|
const hasErrorHandling = containsAny(chatFileContent, [
|
|
1075
1091
|
/\bcatch\b/,
|
|
1076
1092
|
/\b\.catch\b/,
|
|
@@ -1080,7 +1096,7 @@ function validateChat(root, platform, sourceContent) {
|
|
|
1080
1096
|
/\berror\s*:/,
|
|
1081
1097
|
/\btry\b/,
|
|
1082
1098
|
]);
|
|
1083
|
-
if (!hasErrorHandling) {
|
|
1099
|
+
if (hasSendCall && !hasErrorHandling) {
|
|
1084
1100
|
findings.push(finding(`${platform}.chat.send-error-handling`, "warning", "Message send calls found without error handling.", relativeFile(root, chatFiles[0]), "Wrap sendMessage in try/catch or handle the error callback to show send failure state."));
|
|
1085
1101
|
}
|
|
1086
1102
|
if (!containsAny(chatFileContent, MODERATION_MARKERS)) {
|
|
@@ -1450,6 +1466,8 @@ function validatePostsStatusFilter(root, platform, sourceContent) {
|
|
|
1450
1466
|
/\/\/\s*vise:\s*moderation review feed/i
|
|
1451
1467
|
];
|
|
1452
1468
|
for (const [file, content] of sourceContent) {
|
|
1469
|
+
if (path.basename(file).endsWith('.d.ts'))
|
|
1470
|
+
continue;
|
|
1453
1471
|
const rel = relativeFile(root, file);
|
|
1454
1472
|
const hasPostQuery = POST_QUERY_PATTERNS.some((p) => p.test(content));
|
|
1455
1473
|
if (hasPostQuery) {
|
|
@@ -2305,7 +2323,7 @@ async function validateIos(root) {
|
|
|
2305
2323
|
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
2324
|
}
|
|
2307
2325
|
else if (containsAny(manifestContent, [/\.package\s*\([^)]*\bbranch\s*:/s, /pod\s+['"][^'"]*Amity[^'"]*['"]\s*$/m])) {
|
|
2308
|
-
findings.push(finding("ios.sdk.version.pinned", "warning", "The iOS SDK dependency
|
|
2326
|
+
findings.push(finding("ios.sdk.version.pinned", "warning", "The iOS SDK dependency uses an uncontrolled version (a moving branch or unspecified CocoaPods version), not a pinned version or reviewed semver range.", 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."));
|
|
2309
2327
|
}
|
|
2310
2328
|
if (setupFiles.length === 0) {
|
|
2311
2329
|
findings.push(finding("ios.setup.present", "warning", "No obvious AmityClient initialization was found in Swift files.", undefined, "Initialize AmityClient before login and before social.plus API usage."));
|
package/dist/tools/sdkFacts.js
CHANGED
|
@@ -174,9 +174,16 @@ export async function getSdkFacts(options) {
|
|
|
174
174
|
notes: [
|
|
175
175
|
"Symbol facts are existence-only: Vise confirms public symbols in the normalized SDK surface, but semantic correctness and idiomatic usage still require rules, docs, and sensors.",
|
|
176
176
|
modelFacts.facts
|
|
177
|
-
? `Field-level model facts are extraction-grounded (${modelFacts.facts.fieldsGrounding}) by ${modelFacts.facts.extraction.extractor}
|
|
177
|
+
? `Field-level model facts are extraction-grounded (${modelFacts.facts.fieldsGrounding}) by ${modelFacts.facts.extraction.extractor}.${modelFacts.facts.fieldsGrounding === "names-only"
|
|
178
|
+
? " names-only grounding means the source proves field names but no types."
|
|
179
|
+
: ""}`
|
|
178
180
|
: "No field-level model facts for this platform snapshot: model claims stay symbol-only (absence over fabrication).",
|
|
179
181
|
"Remote SP_SDK_SURFACE_URL resolution is recorded in the manifest for a future online snapshot source; this MVP uses local override/env/bundled directories only.",
|
|
182
|
+
...(platform !== "typescript"
|
|
183
|
+
? [
|
|
184
|
+
`Capability anchors are authored TypeScript-first at this MVP, so capabilities is empty for ${platform}. This means capability mapping is not yet available on ${platform} — it does NOT mean the SDK lacks the feature. The symbol and model facts above are extracted from the ${platform} surface and remain authoritative.`,
|
|
185
|
+
]
|
|
186
|
+
: []),
|
|
180
187
|
],
|
|
181
188
|
};
|
|
182
189
|
if (options.includeSymbols) {
|
|
@@ -4,7 +4,7 @@ const SIGNALS = [
|
|
|
4
4
|
level: "dynamic-ui",
|
|
5
5
|
label: "Minimal, server-driven brand or theme updates",
|
|
6
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,
|
|
7
|
+
pattern: /\b(dynamic ui|remote config|network config|syncNetworkConfig|without (?:an? )?(?:app )?(?:release|update|rebuild)|(?:with no|no)\s+(?:an? )?(?:app )?(?:release|rebuild|re-?deploy(?:ment)?|deploy)|real[-\s]?time theme|brand colors?|theme tokens?|palette|console[-\s]?driven)\b/i,
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
id: "component-styling",
|
|
@@ -39,7 +39,7 @@ const SIGNALS = [
|
|
|
39
39
|
level: "sdk-custom-ui",
|
|
40
40
|
label: "Custom UI beyond UIKit's customization model",
|
|
41
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,
|
|
42
|
+
pattern: /\b(totally different ui|brand[-\s]?new ui|bespoke ui|unique ui|complete design freedom|build from scratch|hand[-\s]?rolled)\b|\b(?:our|my|their|its|your)\s+own\s+(?:\w+\s+){0,2}?(?:component(?:\s+tree)?|components?|screens?|widgets?|ui)\b/i,
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
id: "basic-theme",
|
|
@@ -71,6 +71,10 @@ const SOURCE_EVIDENCE = [
|
|
|
71
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
72
|
];
|
|
73
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
|
+
const SERVER_DRIVEN_ORIGIN = /\b(remote config|network config|syncNetworkConfig|real[-\s]?time theme|server[-\s]?driven|(?:without|with no|no)\s+(?:an? )?(?:app )?(?:release|rebuild|re-?deploy(?:ment)?|deploy)|update(?:d|s)? remotely|remotely (?:update|change|configure|manage)\w*)\b/i;
|
|
75
|
+
const BUNDLED_CONFIG_ORIGIN = /\b(config\.json|uikit\.config|bundled config|config file)\b/i;
|
|
76
|
+
const THEME_TOKEN_STYLING = /\b(primary_color|preferred_theme|dark mode|light mode|brand colors?|palette|theme)\b/i;
|
|
77
|
+
const STRUCTURAL_STYLING = /\b((?:hide|remove)\b|exclu(?:de|des|ded|ding|sion|sions)|button text|icons?|page id|component id|element id|reactions?|feature flags?)\b/i;
|
|
74
78
|
export function recommendUIKitCustomization(args) {
|
|
75
79
|
const answers = args.answers ?? {};
|
|
76
80
|
const request = typeof args.request === "string" ? args.request : "";
|
|
@@ -94,7 +98,7 @@ export function recommendUIKitCustomization(args) {
|
|
|
94
98
|
const matchedLevels = levelsFromMatches(matches);
|
|
95
99
|
const answeredLevel = explicitAnswer;
|
|
96
100
|
const recommendedLevel = answeredLevel
|
|
97
|
-
?? (unrecognizedExplicitAnswer || featureCapabilityOnly ? "needs-decision" : chooseRecommendedLevel(matches, args.solutionPath));
|
|
101
|
+
?? (unrecognizedExplicitAnswer || featureCapabilityOnly ? "needs-decision" : chooseRecommendedLevel(matches, request, args.solutionPath));
|
|
98
102
|
const additionalLevels = matchedLevels.filter((level) => level !== recommendedLevel);
|
|
99
103
|
const status = statusFor(recommendedLevel, matchedLevels, args.solutionPath, explicitAnswer, unrecognizedExplicitAnswer);
|
|
100
104
|
const confidence = confidenceFor({ explicitAnswer, matches, recommendedLevel, status, solutionPath: args.solutionPath, unrecognizedExplicitAnswer });
|
|
@@ -127,9 +131,11 @@ export function recommendUIKitCustomization(args) {
|
|
|
127
131
|
advisoryOnly: "This is advisory UIKit customization guidance. It does not change outcome classification, compliance rules, sidecar schema, or `vise check` exit codes.",
|
|
128
132
|
};
|
|
129
133
|
}
|
|
134
|
+
const NEGATED_CUSTOM_CODE = /\b(?:do not|don'?t|dont|won'?t|wont|can'?t|cant|cannot|no|without|not|never|avoid|skip)\b(?:\s+\w+){0,3}?\s+(?:run\s+)?custom\s+(?:code|handler|action)\b/gi;
|
|
130
135
|
function collectSignals(request) {
|
|
136
|
+
const cleaned = request.replace(NEGATED_CUSTOM_CODE, " ");
|
|
131
137
|
return SIGNALS.flatMap((signal) => {
|
|
132
|
-
const match =
|
|
138
|
+
const match = cleaned.match(signal.pattern);
|
|
133
139
|
if (!match?.[0]) {
|
|
134
140
|
return [];
|
|
135
141
|
}
|
|
@@ -162,7 +168,7 @@ function levelsFromMatches(matches) {
|
|
|
162
168
|
"sdk-custom-ui",
|
|
163
169
|
].filter((level) => matches.some((match) => match.level === level));
|
|
164
170
|
}
|
|
165
|
-
function chooseRecommendedLevel(matches, solutionPath) {
|
|
171
|
+
function chooseRecommendedLevel(matches, request, solutionPath) {
|
|
166
172
|
const matchedLevels = levelsFromMatches(matches);
|
|
167
173
|
const strongLevels = levelsFromMatches(matches.filter((match) => match.strength === "strong"));
|
|
168
174
|
const pool = strongLevels.length > 0 ? strongLevels : matchedLevels;
|
|
@@ -175,6 +181,14 @@ function chooseRecommendedLevel(matches, solutionPath) {
|
|
|
175
181
|
if (pool.includes("behavior-overrides")) {
|
|
176
182
|
return "behavior-overrides";
|
|
177
183
|
}
|
|
184
|
+
if (pool.includes("dynamic-ui") &&
|
|
185
|
+
pool.includes("component-styling") &&
|
|
186
|
+
SERVER_DRIVEN_ORIGIN.test(request) &&
|
|
187
|
+
!BUNDLED_CONFIG_ORIGIN.test(request) &&
|
|
188
|
+
THEME_TOKEN_STYLING.test(request) &&
|
|
189
|
+
!STRUCTURAL_STYLING.test(request)) {
|
|
190
|
+
return "dynamic-ui";
|
|
191
|
+
}
|
|
178
192
|
if (pool.includes("component-styling")) {
|
|
179
193
|
return "component-styling";
|
|
180
194
|
}
|