@amityco/social-plus-vise 1.1.1 → 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 +43 -0
- package/README.md +22 -10
- 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 +275 -37
- package/dist/solutionPath.js +274 -0
- package/dist/tools/compliance.js +289 -35
- package/dist/tools/debug.js +83 -26
- package/dist/tools/design.js +24 -4
- package/dist/tools/harness.js +30 -2
- package/dist/tools/integration.js +161 -10
- package/dist/tools/project.js +39 -13
- package/dist/tools/sdkFacts.js +8 -1
- package/dist/tools/sensors.js +1 -1
- package/dist/uikitCustomization.js +384 -0
- 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/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
|
}
|
|
@@ -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."));
|
|
@@ -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."));
|
|
@@ -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."));
|
|
@@ -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 = {
|
|
@@ -937,13 +940,18 @@ function validateComments(root, platform, sourceContent) {
|
|
|
937
940
|
break;
|
|
938
941
|
}
|
|
939
942
|
}
|
|
940
|
-
const
|
|
943
|
+
const isObserverContent = (c) => {
|
|
941
944
|
const stripped = c.replace(/\bawait\b[^\n;]*?(?:get|query)Comments\s*\(/g, "");
|
|
942
945
|
return /(?:get|query)Comments\s*\(/.test(stripped)
|
|
943
946
|
|| /CommentLiveCollection|AmityCommentCollection|commentCollection/.test(c);
|
|
944
|
-
}
|
|
947
|
+
};
|
|
948
|
+
const observerFiles = [...sourceContent.entries()].filter(([, c]) => isObserverContent(c)).map(([f]) => f);
|
|
949
|
+
const hasReactiveCommentObserver = observerFiles.length > 0;
|
|
945
950
|
const cleanupMarkers = COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM[platform] ?? COMMENT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM.typescript;
|
|
946
|
-
|
|
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)) {
|
|
947
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."));
|
|
948
956
|
}
|
|
949
957
|
const strippedCommentFiles = new Map();
|
|
@@ -1029,6 +1037,16 @@ const CHAT_PRESENCE_PATTERNS = [
|
|
|
1029
1037
|
/\bchatClient\b/i,
|
|
1030
1038
|
/\bmessage\s*live\s*collection\b/i,
|
|
1031
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
|
+
];
|
|
1032
1050
|
const CHAT_OBSERVER_CLEANUP_MARKERS_BY_PLATFORM = {
|
|
1033
1051
|
typescript: [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
|
|
1034
1052
|
"react-native": [/\bunsubscribe\b/, /\bdispose\b/, /\bremoveListener\b/, /\buseEffect\b.*\breturn\b/s],
|
|
@@ -1068,6 +1086,7 @@ function validateChat(root, platform, sourceContent) {
|
|
|
1068
1086
|
const chatFileContent = new Map();
|
|
1069
1087
|
for (const f of chatFiles)
|
|
1070
1088
|
chatFileContent.set(f, sourceContent.get(f) ?? "");
|
|
1089
|
+
const hasSendCall = containsAny(chatFileContent, CHAT_SEND_CALL_PATTERNS);
|
|
1071
1090
|
const hasErrorHandling = containsAny(chatFileContent, [
|
|
1072
1091
|
/\bcatch\b/,
|
|
1073
1092
|
/\b\.catch\b/,
|
|
@@ -1077,7 +1096,7 @@ function validateChat(root, platform, sourceContent) {
|
|
|
1077
1096
|
/\berror\s*:/,
|
|
1078
1097
|
/\btry\b/,
|
|
1079
1098
|
]);
|
|
1080
|
-
if (!hasErrorHandling) {
|
|
1099
|
+
if (hasSendCall && !hasErrorHandling) {
|
|
1081
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."));
|
|
1082
1101
|
}
|
|
1083
1102
|
if (!containsAny(chatFileContent, MODERATION_MARKERS)) {
|
|
@@ -1434,7 +1453,9 @@ function validatePostsStatusFilter(root, platform, sourceContent) {
|
|
|
1434
1453
|
];
|
|
1435
1454
|
const FILTER_MARKERS = [
|
|
1436
1455
|
/\bfeedTypes?\s*[:(]/,
|
|
1456
|
+
/\breviewStatus\s*[:(]/,
|
|
1437
1457
|
/\bincludeDeleted\s*\(\s*false\s*\)/,
|
|
1458
|
+
/\bdeletedOption\s*:\s*\.notDeleted\b/,
|
|
1438
1459
|
/\bisDeleted\s*:\s*false\b/,
|
|
1439
1460
|
/\bisFlagged\s*:\s*false\b/,
|
|
1440
1461
|
/\bstatuses\s*[:(]/,
|
|
@@ -1445,6 +1466,8 @@ function validatePostsStatusFilter(root, platform, sourceContent) {
|
|
|
1445
1466
|
/\/\/\s*vise:\s*moderation review feed/i
|
|
1446
1467
|
];
|
|
1447
1468
|
for (const [file, content] of sourceContent) {
|
|
1469
|
+
if (path.basename(file).endsWith('.d.ts'))
|
|
1470
|
+
continue;
|
|
1448
1471
|
const rel = relativeFile(root, file);
|
|
1449
1472
|
const hasPostQuery = POST_QUERY_PATTERNS.some((p) => p.test(content));
|
|
1450
1473
|
if (hasPostQuery) {
|
|
@@ -2296,8 +2319,11 @@ async function validateIos(root) {
|
|
|
2296
2319
|
else if (!containsAny(manifestContent, [/Amity/i, /AmitySDK/i])) {
|
|
2297
2320
|
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
2321
|
}
|
|
2322
|
+
else if (containsAny(manifestContent, [/Amity-Social-Cloud-SDK-iOS-IPA/i])) {
|
|
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."));
|
|
2324
|
+
}
|
|
2299
2325
|
else if (containsAny(manifestContent, [/\.package\s*\([^)]*\bbranch\s*:/s, /pod\s+['"][^'"]*Amity[^'"]*['"]\s*$/m])) {
|
|
2300
|
-
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."));
|
|
2301
2327
|
}
|
|
2302
2328
|
if (setupFiles.length === 0) {
|
|
2303
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."));
|
|
@@ -2944,7 +2970,7 @@ function validateNotificationsPreferencesConfigured(root, platform, sourceConten
|
|
|
2944
2970
|
let hasPreferences = false;
|
|
2945
2971
|
let firstPushRegistrationFile = '';
|
|
2946
2972
|
for (const [filename, text] of sourceContent) {
|
|
2947
|
-
if (/\b(registerPushNotification|enablePushNotification)\b/.test(text)) {
|
|
2973
|
+
if (/\b(registerPushNotification|registerDeviceNotification|enablePushNotification)\b/.test(text)) {
|
|
2948
2974
|
hasPushRegistration = true;
|
|
2949
2975
|
if (!firstPushRegistrationFile) {
|
|
2950
2976
|
firstPushRegistrationFile = (typeof filename === 'string' && root) ? (filename.startsWith(root) ? filename.slice(root.length).replace(/^\//, '') : filename) : filename;
|
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) {
|
package/dist/tools/sensors.js
CHANGED
|
@@ -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) => {
|