@amityco/social-plus-vise 0.14.1 → 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 CHANGED
@@ -4,6 +4,17 @@ All notable changes to `@amityco/social-plus-vise` are documented in this file.
4
4
 
5
5
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.14.2 — 2026-06-03
8
+
9
+ **Theme:** SDK facts bridge for social.plus Block Factory.
10
+
11
+ ### Added
12
+ - **Bundled SDK surface snapshot** in `sdk-surface/` for offline/npm use across TypeScript, Android, iOS, and Flutter.
13
+ - **Internal `vise sdk-facts` CLI command and `get_sdk_facts` MCP tool** for Block Factory. The tool is projectless and read-only: it proves SDK symbol/capability/model-symbol facts from the normalized SDK surface without inspecting customer code.
14
+ - **TypeScript Comments/Reactions capability facts** for the first Block Factory validation slice, including React Native aliasing to the TypeScript SDK surface.
15
+
16
+ ---
17
+
7
18
  ## 0.14.1 — 2026-06-03
8
19
 
9
20
  **Theme:** Retract the enumerative DesignBuildBrief from plan output — our own benchmark said so.
package/README.md CHANGED
@@ -43,9 +43,9 @@ See [Usage Flow](#usage-flow) for the full step-by-step diagram.
43
43
 
44
44
  ## What Vise Does: Agentic Workflow Governance
45
45
 
46
- Instead of just providing a CLI or AI skills, Vise implements a technique called **Agentic Workflow Governance**. Think of it as building a software factory directly on top of the customer's project.
46
+ Instead of just providing a CLI or AI skills, Vise implements a technique called **Agentic Workflow Governance**. Think of it as a customer-project integration harness: the governed build loop runs inside the target repo, grounded in the real project, real docs, and real validation signals.
47
47
 
48
- Vise acts as the foreman of this factory, wrapping your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
48
+ Vise wraps your local coding agents in compliance guardrails when they integrate social.plus SDKs. It inspects your project, grounds the agent in hosted docs, enforces 300 platform-specific compliance rules, checks the generated UI against the customer's design system, surfaces the full SDK feature surface so nothing is silently dropped, and runs your project's own build/lint/typecheck sensors. **Your source code never leaves your machine.**
49
49
 
50
50
  At a glance, Vise sits between the user's prompt and the agent's code changes. The agent still edits the app; Vise turns the request into a grounded plan, records the local contract, and keeps checking until the integration is ready to ship.
51
51
 
@@ -81,6 +81,15 @@ Vise validates on three layers, and the layer is set by the *kind of claim* —
81
81
 
82
82
  Only correctness is gated (it can be made FP-free); conformance and completeness are surfaced, because "all post types" and "matches the brand" are legitimately scope-dependent. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
83
83
 
84
+ ### Relationship to social.plus Block Factory
85
+
86
+ Vise has two deliberately separate roles:
87
+
88
+ - **Customer integration helper:** runs inside customer projects to inspect, plan, validate, and sensor-check social.plus SDK integrations.
89
+ - **Block Factory SDK facts provider:** planned internal mode for social.plus Block Factory to verify SDK capabilities, symbols, and model schemas before reusable blocks are generated or released.
90
+
91
+ Vise owns SDK truth and customer-project governance. social.plus Block Factory owns block contracts, package adapters, previews, conformance tests, and release readiness. See [docs/SDK_FACTS_FOR_BLOCK_FACTORY.md](docs/SDK_FACTS_FOR_BLOCK_FACTORY.md) for the internal provider-side plan.
92
+
84
93
  ### Design-conformant UI
85
94
 
86
95
  Vise can ingest the customer's aesthetic into a **design contract** and guide generation to match it — from an HTML/CSS prototype (`vise design extract`) or from the host app's own design system across web + Android + Flutter + iOS (`vise design extract --from-project`: CSS vars/Tailwind/token modules, `colors.xml`, Flutter `Color(0x…)`, iOS `.colorset`/Swift). `vise design check` reports token conformance; `vise design preview` writes a visual review; `vise design reference` generates a full visual design-system spec (swatches, type samples, component demos). All advisory.
package/dist/server.js CHANGED
@@ -14,6 +14,7 @@ import { planIntegrationTool } from "./tools/integration.js";
14
14
  import { inspectProjectTool, validateSetupTool } from "./tools/project.js";
15
15
  import { resolveRequestTool, suggestPatchTool } from "./tools/resolve.js";
16
16
  import { runSensorsTool } from "./tools/sensors.js";
17
+ import { getSdkFactsTool } from "./tools/sdkFacts.js";
17
18
  import { debugIssueTool, debugIssue } from "./tools/debug.js";
18
19
  import { packageName, packageVersion } from "./version.js";
19
20
  const tools = new Map([
@@ -39,6 +40,7 @@ const tools = new Map([
39
40
  designPreviewTool,
40
41
  designReferenceTool,
41
42
  designInitTokensTool,
43
+ getSdkFactsTool,
42
44
  ].map((tool) => [tool.name, tool]));
43
45
  const bundledSkillName = "social-plus-vise";
44
46
  // Pre-rebrand `install-skill` runs created skill dirs/files under this name. We
@@ -201,6 +203,20 @@ async function handleCli(args) {
201
203
  });
202
204
  return "exit";
203
205
  }
206
+ if (command === "sdk-facts" || command === "sdk_facts") {
207
+ assertOnlyKnownFlags(args, ["platform", "capability", "surface-dir", "format", "include-symbols"], "sdk-facts");
208
+ const format = flagValue(args, "format") ?? "json";
209
+ if (format !== "json") {
210
+ throw new Error("sdk-facts currently supports --format json only.");
211
+ }
212
+ await printToolResult(getSdkFactsTool, {
213
+ platform: requiredFlagValue(args, "platform", "sdk-facts requires --platform."),
214
+ capability: flagValue(args, "capability"),
215
+ surfaceDir: flagValue(args, "surface-dir"),
216
+ includeSymbols: hasFlag(args, "include-symbols"),
217
+ });
218
+ return "exit";
219
+ }
204
220
  if (command === "init") {
205
221
  assertOnlyKnownFlags(args, ["request", "surface", "surface-path"], "init");
206
222
  console.log(JSON.stringify(await initCompliance(positionalRepoPath(args.slice(1)), requiredFlagValue(args, "request", "init requires --request."), flagValue(args, "surface") ?? flagValue(args, "surface-path")), null, 2));
@@ -468,6 +484,16 @@ Resolve a natural-language request into the closest supported Vise outcome.
468
484
 
469
485
  Usage:
470
486
  vise resolve [repoPath] --request "Add a social feed"`;
487
+ }
488
+ if (command === "sdk-facts" || command === "sdk_facts") {
489
+ return `${packageName} sdk-facts
490
+
491
+ Read bundled SDK surface facts for social.plus Block Factory planning. Internal, projectless, and read-only.
492
+
493
+ Usage:
494
+ vise sdk-facts --platform typescript --capability comments --format json
495
+ vise sdk-facts --platform react-native --capability reactions --include-symbols
496
+ vise sdk-facts --platform android --surface-dir ./sdk-surface --format json`;
471
497
  }
472
498
  if (command === "init") {
473
499
  return `${packageName} init
@@ -577,6 +603,7 @@ Usage:
577
603
  vise status [repoPath] Print compliance summary
578
604
  vise validate [repoPath] Validate setup and common risks
579
605
  vise run-sensors [repoPath] Run detected project sensors
606
+ vise sdk-facts --platform ... Internal SDK surface facts for Block Factory
580
607
  vise design extract <prototype> Extract a design contract from an HTML/CSS prototype
581
608
  vise design check [repoPath] Advisory (non-blocking) UI-vs-contract conformance report
582
609
  vise design preview [repoPath] Write an HTML visual review of the contract + conformance
@@ -830,7 +857,7 @@ function ciCheckResult(result) {
830
857
  };
831
858
  }
832
859
  function positionalRepoPath(args) {
833
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
860
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
834
861
  for (let index = 0; index < args.length; index += 1) {
835
862
  const arg = args[index];
836
863
  if (!arg) {
@@ -852,7 +879,7 @@ function positionalRepoPath(args) {
852
879
  }
853
880
  function requiredPositionalText(args, message) {
854
881
  const values = [];
855
- const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
882
+ const flagsWithValues = new Set(["request", "surface", "surface-path", "platform", "capability", "surface-dir", "format", "include", "timeout-ms", "query", "path", "limit", "answer", "target", "dest", "destination", "rule", "confidence", "signer", "identity", "evidence-file", "rationale", "repo", "reference"]);
856
883
  for (let index = 0; index < args.length; index += 1) {
857
884
  const arg = args[index];
858
885
  if (!arg) {
@@ -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.1",
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
+ });