@amityco/social-plus-vise 0.14.0 → 0.14.2
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 +21 -0
- package/README.md +11 -2
- package/dist/server.js +29 -2
- package/dist/tools/design.js +114 -12
- package/dist/tools/integration.js +9 -9
- package/dist/tools/sdkFacts.js +364 -0
- package/package.json +8 -3
- package/scripts/import-sdk-surface.mjs +130 -0
- package/sdk-surface/android.json +30310 -0
- package/sdk-surface/flutter.json +13036 -0
- package/sdk-surface/ios.json +36722 -0
- package/sdk-surface/manifest.json +62 -0
- package/sdk-surface/typescript.json +9994 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
|
|
5
|
+
const PLATFORM_ALIASES = {
|
|
6
|
+
ios: "ios",
|
|
7
|
+
swift: "ios",
|
|
8
|
+
android: "android",
|
|
9
|
+
kotlin: "android",
|
|
10
|
+
typescript: "typescript",
|
|
11
|
+
ts: "typescript",
|
|
12
|
+
javascript: "typescript",
|
|
13
|
+
"react-native": "typescript",
|
|
14
|
+
reactnative: "typescript",
|
|
15
|
+
flutter: "flutter",
|
|
16
|
+
dart: "flutter",
|
|
17
|
+
};
|
|
18
|
+
const CAPABILITIES = {
|
|
19
|
+
comments: {
|
|
20
|
+
required: [
|
|
21
|
+
{ name: "CommentRepository", kind: "type" },
|
|
22
|
+
{ name: "getComments", kind: "member", owner: "CommentRepository" },
|
|
23
|
+
{ name: "createComment", kind: "member", owner: "CommentRepository" },
|
|
24
|
+
],
|
|
25
|
+
optional: [
|
|
26
|
+
{ name: "updateComment", kind: "member", owner: "CommentRepository" },
|
|
27
|
+
{ name: "deleteComment", kind: "member", owner: "CommentRepository" },
|
|
28
|
+
{ name: "flagComment", kind: "member", owner: "CommentRepository" },
|
|
29
|
+
{ name: "unflagComment", kind: "member", owner: "CommentRepository" },
|
|
30
|
+
{ name: "isCommentFlaggedByMe", kind: "member", owner: "CommentRepository" },
|
|
31
|
+
],
|
|
32
|
+
models: [{ name: "Comment", kind: "model", owner: "Amity" }],
|
|
33
|
+
},
|
|
34
|
+
reactions: {
|
|
35
|
+
required: [
|
|
36
|
+
{ name: "ReactionRepository", kind: "type" },
|
|
37
|
+
{ name: "addReaction", kind: "member", owner: "ReactionRepository" },
|
|
38
|
+
{ name: "removeReaction", kind: "member", owner: "ReactionRepository" },
|
|
39
|
+
],
|
|
40
|
+
optional: [
|
|
41
|
+
{ name: "getReactions", kind: "member", owner: "ReactionRepository" },
|
|
42
|
+
{ name: "onReactionAdded", kind: "member", owner: "ReactionRepository" },
|
|
43
|
+
{ name: "onReactionRemoved", kind: "member", owner: "ReactionRepository" },
|
|
44
|
+
{ name: "myReactions", kind: "member", owner: "ReactionRepository" },
|
|
45
|
+
],
|
|
46
|
+
models: [{ name: "Reaction", kind: "model", owner: "Amity" }],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export const getSdkFactsTool = {
|
|
50
|
+
name: "get_sdk_facts",
|
|
51
|
+
description: "Read bundled social.plus SDK surface facts for Block Factory planning. Projectless and read-only; answers symbol existence, not semantic correctness.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
platform: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "SDK platform or alias: typescript, react-native, android, ios, flutter.",
|
|
58
|
+
},
|
|
59
|
+
capability: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Optional capability filter. Current MVP supports comments and reactions for the TypeScript/React Native surface.",
|
|
62
|
+
},
|
|
63
|
+
surfaceDir: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Optional directory containing manifest.json and platform JSON files. Defaults to SP_SDK_SURFACE_DIR, then bundled sdk-surface/.",
|
|
66
|
+
},
|
|
67
|
+
includeSymbols: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
description: "Include compact type/member symbol arrays. Defaults to false.",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ["platform"],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
async call(input) {
|
|
76
|
+
const args = objectInput(input);
|
|
77
|
+
return textResult(await getSdkFacts({
|
|
78
|
+
platform: stringField(args, "platform"),
|
|
79
|
+
capability: optionalStringField(args, "capability"),
|
|
80
|
+
surfaceDir: optionalStringField(args, "surfaceDir"),
|
|
81
|
+
includeSymbols: optionalBooleanField(args, "includeSymbols"),
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export function canonicalPlatform(platform) {
|
|
86
|
+
return PLATFORM_ALIASES[platform.trim().toLowerCase()] ?? null;
|
|
87
|
+
}
|
|
88
|
+
export async function getSdkFacts(options) {
|
|
89
|
+
const platform = canonicalPlatform(options.platform);
|
|
90
|
+
if (!platform) {
|
|
91
|
+
throw new Error(`Unsupported SDK platform: ${options.platform}. Supported platforms: typescript, react-native, android, ios, flutter.`);
|
|
92
|
+
}
|
|
93
|
+
const surfaceLocation = resolveSurfaceDirectory(options.surfaceDir);
|
|
94
|
+
const manifest = await readJson(path.join(surfaceLocation.directory, "manifest.json"));
|
|
95
|
+
const entry = manifest.surfaces?.[platform];
|
|
96
|
+
const surfaceFile = entry?.file ?? `${platform}.json`;
|
|
97
|
+
const surface = await readJson(path.join(surfaceLocation.directory, surfaceFile));
|
|
98
|
+
const symbols = collectSymbols(platform, surface);
|
|
99
|
+
const capabilityIds = requestedCapabilityIds(platform, options.capability);
|
|
100
|
+
const capabilities = capabilityIds.map((id) => buildCapabilityFact(id, platform, symbols));
|
|
101
|
+
const result = {
|
|
102
|
+
source: "social-plus-vise",
|
|
103
|
+
mode: "block-factory-sdk-facts",
|
|
104
|
+
requestedPlatform: options.platform,
|
|
105
|
+
platform,
|
|
106
|
+
surfaceSource: {
|
|
107
|
+
kind: surfaceLocation.kind,
|
|
108
|
+
directory: surfaceLocation.directory,
|
|
109
|
+
manifestSource: manifest.source,
|
|
110
|
+
manifestSchemaVersion: manifest.schemaVersion,
|
|
111
|
+
upstreamGit: manifest.upstreamGit,
|
|
112
|
+
importedAt: manifest.importedAt,
|
|
113
|
+
remoteUrlStatus: "not-implemented",
|
|
114
|
+
},
|
|
115
|
+
sdkProduct: entry?.sdkProduct ?? stringProperty(surface, "sdk_product") ?? stringProperty(surface, "sdk_package"),
|
|
116
|
+
sdkVersion: entry?.sdkVersion ?? null,
|
|
117
|
+
extractor: entry?.extractor ?? stringProperty(surface, "extractor"),
|
|
118
|
+
extractorVersion: entry?.extractorVersion ?? stringProperty(surface, "extractor_version"),
|
|
119
|
+
extractedAt: entry?.extractedAt ?? stringProperty(surface, "extracted_at"),
|
|
120
|
+
symbolSummary: {
|
|
121
|
+
typeCount: symbols.types.length,
|
|
122
|
+
memberCount: symbols.members.length,
|
|
123
|
+
},
|
|
124
|
+
capabilities,
|
|
125
|
+
notes: [
|
|
126
|
+
"Existence-only facts: Vise confirms public symbols in the normalized SDK surface, but semantic correctness and idiomatic usage still require rules, docs, and sensors.",
|
|
127
|
+
"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.",
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
if (options.includeSymbols) {
|
|
131
|
+
result.symbols = {
|
|
132
|
+
types: symbols.types,
|
|
133
|
+
members: symbols.members,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function resolveSurfaceDirectory(surfaceDir) {
|
|
139
|
+
if (surfaceDir) {
|
|
140
|
+
return { kind: "override", directory: path.resolve(surfaceDir) };
|
|
141
|
+
}
|
|
142
|
+
if (process.env.SP_SDK_SURFACE_DIR) {
|
|
143
|
+
return { kind: "env", directory: path.resolve(process.env.SP_SDK_SURFACE_DIR) };
|
|
144
|
+
}
|
|
145
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
146
|
+
return { kind: "bundled", directory: path.resolve(moduleDir, "..", "..", "sdk-surface") };
|
|
147
|
+
}
|
|
148
|
+
async function readJson(filePath) {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
154
|
+
throw new Error(`Unable to read SDK surface file ${filePath}: ${detail}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function requestedCapabilityIds(platform, capability) {
|
|
158
|
+
const available = Object.keys(CAPABILITIES);
|
|
159
|
+
if (!capability) {
|
|
160
|
+
return platform === "typescript" ? available : [];
|
|
161
|
+
}
|
|
162
|
+
const normalized = capability.trim().toLowerCase();
|
|
163
|
+
if (!available.includes(normalized)) {
|
|
164
|
+
throw new Error(`Unsupported SDK capability: ${capability}. Supported capabilities: ${available.join(", ")}.`);
|
|
165
|
+
}
|
|
166
|
+
if (platform !== "typescript") {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
return [normalized];
|
|
170
|
+
}
|
|
171
|
+
function buildCapabilityFact(id, platform, symbols) {
|
|
172
|
+
const definition = CAPABILITIES[id];
|
|
173
|
+
const requiredSymbols = definition.required.map((symbol) => checkSymbol(symbols, { ...symbol, required: true }));
|
|
174
|
+
const optionalSymbols = definition.optional.map((symbol) => checkSymbol(symbols, { ...symbol, required: false }));
|
|
175
|
+
const models = definition.models.map((symbol) => checkSymbol(symbols, { ...symbol, required: false }));
|
|
176
|
+
const missingRequired = requiredSymbols.filter((symbol) => !symbol.exists);
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
support: missingRequired.length > 0 ? "missing-symbols" : "supported",
|
|
180
|
+
platform,
|
|
181
|
+
requiredSymbols,
|
|
182
|
+
optionalSymbols,
|
|
183
|
+
models,
|
|
184
|
+
notes: [
|
|
185
|
+
"Model facts are symbol-only in this MVP; field-level schemas need richer extractor output before they become contract-grade.",
|
|
186
|
+
...(missingRequired.length > 0 ? [`Missing required symbol(s): ${missingRequired.map((symbol) => symbol.owner ? `${symbol.owner}.${symbol.name}` : symbol.name).join(", ")}.`] : []),
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function checkSymbol(symbols, spec) {
|
|
191
|
+
const haystack = spec.kind === "type" ? symbols.types : spec.kind === "member" ? symbols.members : symbols.types;
|
|
192
|
+
const match = haystack.find((symbol) => symbol.name === spec.name && (!spec.owner || symbol.owner === spec.owner || symbol.namespace === spec.owner));
|
|
193
|
+
return {
|
|
194
|
+
name: spec.name,
|
|
195
|
+
kind: spec.kind,
|
|
196
|
+
owner: spec.owner,
|
|
197
|
+
required: spec.required,
|
|
198
|
+
exists: Boolean(match),
|
|
199
|
+
source: match ? { file: match.file, line: match.line } : undefined,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function collectSymbols(platform, surface) {
|
|
203
|
+
if (platform === "typescript") {
|
|
204
|
+
return collectTypeScriptSymbols(surface);
|
|
205
|
+
}
|
|
206
|
+
return collectNativeSymbols(surface);
|
|
207
|
+
}
|
|
208
|
+
function collectTypeScriptSymbols(surface) {
|
|
209
|
+
const root = objectRecord(surface);
|
|
210
|
+
const types = [];
|
|
211
|
+
const members = [];
|
|
212
|
+
const namespaces = objectRecord(root.namespaces);
|
|
213
|
+
for (const [name, namespace] of Object.entries(namespaces)) {
|
|
214
|
+
collectTypeScriptNamespace(name, objectRecord(namespace), types, members);
|
|
215
|
+
}
|
|
216
|
+
for (const exportEntry of arrayRecords(root.root_exports)) {
|
|
217
|
+
if (typeof exportEntry.name === "string") {
|
|
218
|
+
types.push({
|
|
219
|
+
name: exportEntry.name,
|
|
220
|
+
kind: typeof exportEntry.kind === "string" ? exportEntry.kind : "export",
|
|
221
|
+
file: typeof exportEntry.file === "string" ? exportEntry.file : undefined,
|
|
222
|
+
line: typeof exportEntry.line === "number" ? exportEntry.line : null,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return sortSymbols(dedupeSymbols({ types, members }));
|
|
227
|
+
}
|
|
228
|
+
function collectTypeScriptNamespace(name, namespace, types, members) {
|
|
229
|
+
const sourceModule = typeof namespace.source_module === "string" ? namespace.source_module : undefined;
|
|
230
|
+
types.push({ name, kind: "namespace", file: sourceModule });
|
|
231
|
+
for (const member of arrayRecords(namespace.members)) {
|
|
232
|
+
const memberName = typeof member.name === "string" ? member.name : undefined;
|
|
233
|
+
if (!memberName) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const symbol = {
|
|
237
|
+
name: memberName,
|
|
238
|
+
kind: typeof member.kind === "string" ? member.kind : "member",
|
|
239
|
+
owner: name,
|
|
240
|
+
namespace: typeof member.namespace === "string" ? member.namespace : name,
|
|
241
|
+
file: typeof member.file === "string" ? member.file : undefined,
|
|
242
|
+
line: typeof member.line === "number" ? member.line : null,
|
|
243
|
+
};
|
|
244
|
+
if (symbol.kind === "type" || symbol.kind === "interface" || symbol.kind === "enum" || symbol.kind === "class") {
|
|
245
|
+
types.push(symbol);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
members.push(symbol);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const subNamespaces = objectRecord(namespace.sub_namespaces);
|
|
252
|
+
for (const [subName, subNamespace] of Object.entries(subNamespaces)) {
|
|
253
|
+
collectTypeScriptNamespace(`${name}.${subName}`, objectRecord(subNamespace), types, members);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function collectNativeSymbols(surface) {
|
|
257
|
+
const root = objectRecord(surface);
|
|
258
|
+
const types = [];
|
|
259
|
+
const members = [];
|
|
260
|
+
for (const collectionName of ["types", "interfaces", "protocols", "mixins", "extensions", "typedefs", "typealiases", "global_typealiases"]) {
|
|
261
|
+
collectTypeMap(objectRecord(root[collectionName]), types, members);
|
|
262
|
+
}
|
|
263
|
+
for (const collectionName of ["global_funcs", "global_consts"]) {
|
|
264
|
+
for (const symbol of arrayRecords(root[collectionName])) {
|
|
265
|
+
const name = typeof symbol.name === "string" ? symbol.name : undefined;
|
|
266
|
+
if (name) {
|
|
267
|
+
members.push({
|
|
268
|
+
name,
|
|
269
|
+
kind: typeof symbol.kind === "string" ? symbol.kind : collectionName,
|
|
270
|
+
file: sourceFile(symbol),
|
|
271
|
+
line: sourceLine(symbol),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return sortSymbols(dedupeSymbols({ types, members }));
|
|
277
|
+
}
|
|
278
|
+
function collectTypeMap(typeMap, types, members) {
|
|
279
|
+
for (const [name, rawType] of Object.entries(typeMap)) {
|
|
280
|
+
const typeInfo = objectRecord(rawType);
|
|
281
|
+
types.push({
|
|
282
|
+
name,
|
|
283
|
+
kind: typeof typeInfo.kind === "string" ? typeInfo.kind : "type",
|
|
284
|
+
file: sourceFile(typeInfo),
|
|
285
|
+
line: sourceLine(typeInfo),
|
|
286
|
+
});
|
|
287
|
+
for (const member of arrayRecords(typeInfo.members)) {
|
|
288
|
+
const memberName = typeof member.name === "string" ? member.name : undefined;
|
|
289
|
+
if (!memberName) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
members.push({
|
|
293
|
+
name: memberName,
|
|
294
|
+
kind: typeof member.kind === "string" ? member.kind : "member",
|
|
295
|
+
owner: name,
|
|
296
|
+
file: sourceFile(member),
|
|
297
|
+
line: sourceLine(member),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
for (const nestedType of arrayRecords(typeInfo.nested_types)) {
|
|
301
|
+
const nestedName = typeof nestedType.name === "string" ? nestedType.name : undefined;
|
|
302
|
+
if (!nestedName) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const qualifiedName = `${name}.${nestedName}`;
|
|
306
|
+
types.push({
|
|
307
|
+
name: qualifiedName,
|
|
308
|
+
kind: typeof nestedType.kind === "string" ? nestedType.kind : "nested_type",
|
|
309
|
+
owner: name,
|
|
310
|
+
file: sourceFile(nestedType),
|
|
311
|
+
line: sourceLine(nestedType),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function sourceFile(value) {
|
|
317
|
+
if (typeof value.file === "string") {
|
|
318
|
+
return value.file;
|
|
319
|
+
}
|
|
320
|
+
const primaryDecl = objectRecord(value.primary_decl);
|
|
321
|
+
return typeof primaryDecl.file === "string" ? primaryDecl.file : undefined;
|
|
322
|
+
}
|
|
323
|
+
function sourceLine(value) {
|
|
324
|
+
if (typeof value.line === "number") {
|
|
325
|
+
return value.line;
|
|
326
|
+
}
|
|
327
|
+
const primaryDecl = objectRecord(value.primary_decl);
|
|
328
|
+
return typeof primaryDecl.line === "number" ? primaryDecl.line : null;
|
|
329
|
+
}
|
|
330
|
+
function stringProperty(value, property) {
|
|
331
|
+
const record = objectRecord(value);
|
|
332
|
+
return typeof record[property] === "string" ? record[property] : undefined;
|
|
333
|
+
}
|
|
334
|
+
function objectRecord(value) {
|
|
335
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
336
|
+
}
|
|
337
|
+
function arrayRecords(value) {
|
|
338
|
+
return Array.isArray(value) ? value.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
339
|
+
}
|
|
340
|
+
function dedupeSymbols(symbols) {
|
|
341
|
+
return {
|
|
342
|
+
types: dedupeSymbolList(symbols.types),
|
|
343
|
+
members: dedupeSymbolList(symbols.members),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function dedupeSymbolList(symbols) {
|
|
347
|
+
const byKey = new Map();
|
|
348
|
+
for (const symbol of symbols) {
|
|
349
|
+
const key = `${symbol.owner ?? ""}:${symbol.namespace ?? ""}:${symbol.kind ?? ""}:${symbol.name}`;
|
|
350
|
+
if (!byKey.has(key)) {
|
|
351
|
+
byKey.set(key, symbol);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return [...byKey.values()];
|
|
355
|
+
}
|
|
356
|
+
function sortSymbols(symbols) {
|
|
357
|
+
return {
|
|
358
|
+
types: symbols.types.sort(compareSymbols),
|
|
359
|
+
members: symbols.members.sort(compareSymbols),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function compareSymbols(a, b) {
|
|
363
|
+
return `${a.owner ?? ""}.${a.name}`.localeCompare(`${b.owner ?? ""}.${b.name}`);
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amityco/social-plus-vise",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -40,11 +40,14 @@
|
|
|
40
40
|
"CHANGELOG.md",
|
|
41
41
|
"social.plus-vise.png",
|
|
42
42
|
"rules",
|
|
43
|
-
"skills"
|
|
43
|
+
"skills",
|
|
44
|
+
"scripts",
|
|
45
|
+
"sdk-surface"
|
|
44
46
|
],
|
|
45
47
|
"scripts": {
|
|
46
48
|
"build": "tsc -p tsconfig.json",
|
|
47
49
|
"postbuild": "chmod +x dist/server.js",
|
|
50
|
+
"sdk-surface:import": "node scripts/import-sdk-surface.mjs",
|
|
48
51
|
"pack:check": "npm pack --dry-run --cache /tmp/social-plus-foundry-npm-cache",
|
|
49
52
|
"publish:check": "npm run validate && npm publish --dry-run --access public --cache /tmp/social-plus-foundry-npm-cache",
|
|
50
53
|
"start": "node dist/server.js",
|
|
@@ -60,9 +63,11 @@
|
|
|
60
63
|
"test:fixture-symmetry": "npm run build && node test/run-fixture-symmetry.mjs",
|
|
61
64
|
"test:nonui-skip": "npm run build && node test/run-nonui-layer-skip.mjs",
|
|
62
65
|
"test:sdk-version": "npm run build && node test/run-sdk-version.mjs",
|
|
66
|
+
"test:sdk-surface": "node test/run-sdk-surface-snapshot.mjs",
|
|
67
|
+
"test:sdk-facts": "npm run build && node test/run-sdk-facts.mjs",
|
|
63
68
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
69
|
"test:e2e-package": "npm run build && node test/run-e2e-package.mjs",
|
|
65
|
-
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
70
|
+
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:ast && npm run test:design-extract && npm run test:design-brief && npm run test:capabilities && npm run test:classify && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:happy-path-clean && npm run test:fixture-symmetry && npm run test:nonui-skip && npm run test:sdk-version && npm run test:sdk-surface && npm run test:sdk-facts && npm run test:native-idioms && npm run test:grader-facts && npm run test:ground-truth && npm run test:improvements && npm run test:debug && npm run test:preflight && npm run test:e2e-package && npm run pack:check",
|
|
66
71
|
"test:ast": "node test/run-ast-helpers.mjs",
|
|
67
72
|
"test:design-extract": "npm run build && node test/run-design-extract.mjs",
|
|
68
73
|
"test:design-brief": "npm run build && node test/run-design-brief.mjs",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const outDir = path.join(repoRoot, "sdk-surface");
|
|
11
|
+
const platforms = ["typescript", "android", "ios", "flutter"];
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
function argValue(name) {
|
|
15
|
+
const idx = process.argv.indexOf(name);
|
|
16
|
+
return idx >= 0 ? process.argv[idx + 1] : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultSourceDir() {
|
|
20
|
+
const candidates = [
|
|
21
|
+
process.env.SP_SDK_SURFACE_DIR,
|
|
22
|
+
path.resolve(repoRoot, "..", "..", "sp-sdks", "social-plus-docs", ".docs-ops", "sdk-surface"),
|
|
23
|
+
path.resolve(repoRoot, "..", "social-plus-docs", ".docs-ops", "sdk-surface"),
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
return candidates.find((candidate) => existsSync(candidate)) || candidates[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sourceMetadata(surface) {
|
|
29
|
+
return {
|
|
30
|
+
sdkProduct: surface.sdk_product || surface.sdk_package || null,
|
|
31
|
+
sdkVersion: surface.sdk_version || null,
|
|
32
|
+
extractor: surface.extractor || null,
|
|
33
|
+
extractorVersion: surface.extractor_version || null,
|
|
34
|
+
extractedAt: surface.extracted_at || null,
|
|
35
|
+
sourceUrl: surface.source_url || null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateSurface(platform, surface) {
|
|
40
|
+
if (!surface || typeof surface !== "object" || Array.isArray(surface)) {
|
|
41
|
+
throw new Error(`${platform}.json must contain a JSON object`);
|
|
42
|
+
}
|
|
43
|
+
if (!surface.extractor || !surface.extractor_version || !surface.extracted_at) {
|
|
44
|
+
throw new Error(`${platform}.json is missing extractor, extractor_version, or extracted_at`);
|
|
45
|
+
}
|
|
46
|
+
const hasSymbols =
|
|
47
|
+
surface.types ||
|
|
48
|
+
surface.namespaces ||
|
|
49
|
+
surface.root_exports ||
|
|
50
|
+
surface.protocols ||
|
|
51
|
+
surface.interfaces ||
|
|
52
|
+
surface.extensions ||
|
|
53
|
+
surface.typedefs;
|
|
54
|
+
if (!hasSymbols) {
|
|
55
|
+
throw new Error(`${platform}.json does not look like a normalized SDK surface`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function gitMetadata(sourceDir) {
|
|
60
|
+
try {
|
|
61
|
+
const root = (await execFileAsync("git", ["-C", sourceDir, "rev-parse", "--show-toplevel"])).stdout.trim();
|
|
62
|
+
const commit = (await execFileAsync("git", ["-C", root, "rev-parse", "HEAD"])).stdout.trim();
|
|
63
|
+
const status = (await execFileAsync("git", ["-C", root, "status", "--short", "--", sourceDir])).stdout.trim();
|
|
64
|
+
return {
|
|
65
|
+
repo: path.basename(root),
|
|
66
|
+
commit,
|
|
67
|
+
dirty: Boolean(status),
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readJson(file) {
|
|
75
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function sha256(file) {
|
|
79
|
+
return createHash("sha256").update(await readFile(file)).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const sourceDir = path.resolve(argValue("--from") || defaultSourceDir());
|
|
84
|
+
if (!sourceDir || !existsSync(sourceDir)) {
|
|
85
|
+
throw new Error(`SDK surface source dir does not exist: ${sourceDir}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await mkdir(outDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
const manifest = {
|
|
91
|
+
schemaVersion: "0.1.0",
|
|
92
|
+
source: "social-plus-docs/.docs-ops/sdk-surface",
|
|
93
|
+
upstreamGit: await gitMetadata(sourceDir),
|
|
94
|
+
importedAt: new Date().toISOString(),
|
|
95
|
+
resolutionOrder: [
|
|
96
|
+
"--surface-dir",
|
|
97
|
+
"SP_SDK_SURFACE_DIR",
|
|
98
|
+
"SP_SDK_SURFACE_URL",
|
|
99
|
+
"bundled sdk-surface/",
|
|
100
|
+
],
|
|
101
|
+
surfaces: {},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const platform of platforms) {
|
|
105
|
+
const src = path.join(sourceDir, `${platform}.json`);
|
|
106
|
+
const dest = path.join(outDir, `${platform}.json`);
|
|
107
|
+
if (!existsSync(src)) {
|
|
108
|
+
throw new Error(`Missing SDK surface file: ${src}`);
|
|
109
|
+
}
|
|
110
|
+
const surface = await readJson(src);
|
|
111
|
+
validateSurface(platform, surface);
|
|
112
|
+
await copyFile(src, dest);
|
|
113
|
+
const bytes = (await readFile(dest)).byteLength;
|
|
114
|
+
manifest.surfaces[platform] = {
|
|
115
|
+
file: `${platform}.json`,
|
|
116
|
+
bytes,
|
|
117
|
+
sha256: await sha256(dest),
|
|
118
|
+
...sourceMetadata(surface),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const manifestPath = path.join(outDir, "manifest.json");
|
|
123
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
124
|
+
console.log(`imported ${platforms.length} SDK surfaces into ${path.relative(repoRoot, outDir)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main().catch((error) => {
|
|
128
|
+
console.error(`sdk-surface import failed: ${error.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
});
|