@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 +11 -0
- package/README.md +11 -2
- package/dist/server.js +29 -2
- 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
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
|
|
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
|
|
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.
|
|
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
|
+
});
|