@amityco/social-plus-vise 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/README.md +92 -0
- package/dist/outcomes.js +574 -0
- package/dist/server.js +810 -0
- package/dist/tools/compliance.js +965 -0
- package/dist/tools/docs.js +312 -0
- package/dist/tools/harness.js +229 -0
- package/dist/tools/integration.js +332 -0
- package/dist/tools/patch.js +67 -0
- package/dist/tools/project.js +908 -0
- package/dist/tools/resolve.js +120 -0
- package/dist/tools/sensors.js +185 -0
- package/dist/types.js +31 -0
- package/dist/version.js +19 -0
- package/package.json +64 -0
- package/rules/design.yaml +66 -0
- package/rules/feed.yaml +126 -0
- package/rules/live-data.yaml +66 -0
- package/rules/push.yaml +95 -0
- package/rules/sdk-lifecycle.yaml +422 -0
- package/rules/security.yaml +162 -0
- package/skills/social-plus-vise/SKILL.md +199 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { objectInput, optionalNumberField, stringField, textResult } from "../types.js";
|
|
4
|
+
import { packageUserAgent } from "../version.js";
|
|
5
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
|
|
6
|
+
const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
const MIN_EXPECTED_PAGES = 10;
|
|
8
|
+
let hostedCacheEntry;
|
|
9
|
+
let hostedCachePending;
|
|
10
|
+
function localDocsRoot() {
|
|
11
|
+
return process.env.SOCIAL_PLUS_DOCS_ROOT ? path.resolve(process.env.SOCIAL_PLUS_DOCS_ROOT) : undefined;
|
|
12
|
+
}
|
|
13
|
+
function docsBaseUrl() {
|
|
14
|
+
return (process.env.SOCIAL_PLUS_DOCS_BASE_URL ?? "https://learn.social.plus").replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
function fetchTimeoutMs() {
|
|
17
|
+
const raw = process.env.SOCIAL_PLUS_DOCS_FETCH_TIMEOUT_MS;
|
|
18
|
+
if (!raw) {
|
|
19
|
+
return DEFAULT_FETCH_TIMEOUT_MS;
|
|
20
|
+
}
|
|
21
|
+
const value = Number(raw);
|
|
22
|
+
return Number.isFinite(value) && value > 0 ? value : DEFAULT_FETCH_TIMEOUT_MS;
|
|
23
|
+
}
|
|
24
|
+
function cacheTtlMs() {
|
|
25
|
+
const raw = process.env.SOCIAL_PLUS_DOCS_CACHE_TTL_MS;
|
|
26
|
+
if (!raw) {
|
|
27
|
+
return DEFAULT_CACHE_TTL_MS;
|
|
28
|
+
}
|
|
29
|
+
const value = Number(raw);
|
|
30
|
+
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_CACHE_TTL_MS;
|
|
31
|
+
}
|
|
32
|
+
export const searchDocsTool = {
|
|
33
|
+
name: "search_docs",
|
|
34
|
+
description: "Search social.plus documentation. For implementation requests, call plan_integration first so required inputs and stop conditions are known before docs lookup.",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
query: { type: "string" },
|
|
39
|
+
limit: { type: "number", default: 5 },
|
|
40
|
+
},
|
|
41
|
+
required: ["query"],
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
async call(input) {
|
|
45
|
+
const args = objectInput(input);
|
|
46
|
+
const query = stringField(args, "query");
|
|
47
|
+
const limit = optionalNumberField(args, "limit", 5);
|
|
48
|
+
return textResult({ results: await searchDocs(query, limit) });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export const getDocPageTool = {
|
|
52
|
+
name: "get_doc_page",
|
|
53
|
+
description: "Read a single social.plus docs page by docs path.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
path: { type: "string" },
|
|
58
|
+
},
|
|
59
|
+
required: ["path"],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
async call(input) {
|
|
63
|
+
const args = objectInput(input);
|
|
64
|
+
const requestedPath = stringField(args, "path");
|
|
65
|
+
const page = await getDocPage(requestedPath);
|
|
66
|
+
return textResult(page);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
async function searchDocs(query, limit) {
|
|
70
|
+
const pages = await loadDocPages();
|
|
71
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const page of pages) {
|
|
74
|
+
const lower = page.content.toLowerCase();
|
|
75
|
+
const pathAndTitle = `${page.path} ${page.title}`.toLowerCase();
|
|
76
|
+
let score = terms.reduce((sum, term) => {
|
|
77
|
+
const contentScore = countOccurrences(lower, term);
|
|
78
|
+
const metadataScore = countOccurrences(pathAndTitle, term) * 25;
|
|
79
|
+
return sum + contentScore + metadataScore;
|
|
80
|
+
}, 0);
|
|
81
|
+
if (terms.every((term) => pathAndTitle.includes(term))) {
|
|
82
|
+
score += 100;
|
|
83
|
+
}
|
|
84
|
+
if (score === 0) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
results.push({
|
|
88
|
+
path: page.path,
|
|
89
|
+
title: page.title,
|
|
90
|
+
snippet: snippetFor(page.content, terms),
|
|
91
|
+
score,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return results
|
|
95
|
+
.sort((a, b) => b.score - a.score)
|
|
96
|
+
.slice(0, Math.max(1, Math.min(limit, 20)))
|
|
97
|
+
.map(({ score: _score, ...result }) => result);
|
|
98
|
+
}
|
|
99
|
+
async function getDocPage(requestedPath) {
|
|
100
|
+
const normalized = normalizeDocsPath(requestedPath);
|
|
101
|
+
const pages = await loadDocPages();
|
|
102
|
+
const page = pages.find((candidate) => normalizeDocsPath(candidate.path) === normalized);
|
|
103
|
+
if (!page) {
|
|
104
|
+
throw new Error(`Docs page not found: ${requestedPath}. Use search_docs to find the canonical path.`);
|
|
105
|
+
}
|
|
106
|
+
return page;
|
|
107
|
+
}
|
|
108
|
+
async function loadDocPages() {
|
|
109
|
+
const root = localDocsRoot();
|
|
110
|
+
if (root) {
|
|
111
|
+
return loadLocalDocPages(root);
|
|
112
|
+
}
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
if (hostedCacheEntry && now - hostedCacheEntry.fetchedAt < cacheTtlMs()) {
|
|
115
|
+
return hostedCacheEntry.pages;
|
|
116
|
+
}
|
|
117
|
+
hostedCachePending ??= loadHostedDocPages()
|
|
118
|
+
.then((pages) => {
|
|
119
|
+
hostedCacheEntry = { pages, fetchedAt: Date.now() };
|
|
120
|
+
return pages;
|
|
121
|
+
})
|
|
122
|
+
.finally(() => {
|
|
123
|
+
hostedCachePending = undefined;
|
|
124
|
+
});
|
|
125
|
+
return hostedCachePending;
|
|
126
|
+
}
|
|
127
|
+
async function loadHostedDocPages() {
|
|
128
|
+
const baseUrl = docsBaseUrl();
|
|
129
|
+
const fullUrl = `${baseUrl}/llms-full.txt`;
|
|
130
|
+
const indexUrl = `${baseUrl}/llms.txt`;
|
|
131
|
+
const failures = [];
|
|
132
|
+
try {
|
|
133
|
+
const content = await fetchText(fullUrl);
|
|
134
|
+
return parseLlmsFull(content, baseUrl);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
failures.push(`${fullUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const content = await fetchText(indexUrl);
|
|
141
|
+
return parseLlmsIndex(content, baseUrl);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
failures.push(`${indexUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Failed to load social.plus docs from hosted sources.\n${failures.join("\n")}`);
|
|
147
|
+
}
|
|
148
|
+
async function loadLocalDocPages(root) {
|
|
149
|
+
const files = await findDocFiles(root);
|
|
150
|
+
return Promise.all(files.map(async (file) => {
|
|
151
|
+
const raw = await readFile(file, "utf8");
|
|
152
|
+
const content = cleanMdx(raw);
|
|
153
|
+
const relativePath = path.relative(root, file);
|
|
154
|
+
return {
|
|
155
|
+
path: normalizeDocsPath(relativePath),
|
|
156
|
+
title: titleFromContent(content, file),
|
|
157
|
+
content,
|
|
158
|
+
};
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
async function fetchText(url) {
|
|
162
|
+
const timeoutMs = fetchTimeoutMs();
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
headers: {
|
|
168
|
+
accept: "text/plain, text/markdown, */*",
|
|
169
|
+
"user-agent": packageUserAgent,
|
|
170
|
+
},
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error(`Failed to fetch docs from ${url}: ${response.status} ${response.statusText}`);
|
|
175
|
+
}
|
|
176
|
+
return await response.text();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
180
|
+
throw new Error(`Timed out fetching docs from ${url} after ${timeoutMs}ms.`);
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
clearTimeout(timeout);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function parseLlmsFull(content, baseUrl) {
|
|
189
|
+
const pageHeading = /^### \[([^\]]+)\]\(([^)]+)\)\s*$/gm;
|
|
190
|
+
const matches = Array.from(content.matchAll(pageHeading));
|
|
191
|
+
const pages = [];
|
|
192
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
193
|
+
const match = matches[index];
|
|
194
|
+
const nextMatch = matches[index + 1];
|
|
195
|
+
const title = match[1].trim();
|
|
196
|
+
const url = absolutizeUrl(match[2].trim(), baseUrl);
|
|
197
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
198
|
+
const end = nextMatch?.index ?? content.length;
|
|
199
|
+
const pageContent = content.slice(start, end).trim();
|
|
200
|
+
pages.push({
|
|
201
|
+
path: pathFromDocsUrl(url),
|
|
202
|
+
title,
|
|
203
|
+
url,
|
|
204
|
+
content: cleanMdx(`# ${title}\n\n${pageContent}`),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
assertMinimumPages(pages, "llms-full.txt", "### [title](url)");
|
|
208
|
+
return pages;
|
|
209
|
+
}
|
|
210
|
+
export function parseLlmsIndex(content, baseUrl) {
|
|
211
|
+
const pageLine = /^- \[([^\]]+)\]\(([^)]+)\)(?::\s*(.*))?\s*$/gm;
|
|
212
|
+
const pages = [];
|
|
213
|
+
for (const match of content.matchAll(pageLine)) {
|
|
214
|
+
const title = match[1].trim();
|
|
215
|
+
const url = absolutizeUrl(match[2].trim(), baseUrl);
|
|
216
|
+
const summary = match[3]?.trim() || "No summary available.";
|
|
217
|
+
pages.push({
|
|
218
|
+
path: pathFromDocsUrl(url),
|
|
219
|
+
title,
|
|
220
|
+
url,
|
|
221
|
+
content: cleanMdx(`# ${title}\n\n${summary}`),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
assertMinimumPages(pages, "llms.txt", "- [title](url)");
|
|
225
|
+
return pages;
|
|
226
|
+
}
|
|
227
|
+
function assertMinimumPages(pages, source, expectedFormat) {
|
|
228
|
+
if (pages.length < MIN_EXPECTED_PAGES) {
|
|
229
|
+
throw new Error(`Parsed only ${pages.length} pages from ${source}; expected at least ${MIN_EXPECTED_PAGES}. ` +
|
|
230
|
+
`The hosted docs format may have changed; the parser expects entries shaped like "${expectedFormat}".`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function absolutizeUrl(url, baseUrl) {
|
|
234
|
+
return new URL(url, baseUrl).toString();
|
|
235
|
+
}
|
|
236
|
+
function pathFromDocsUrl(url) {
|
|
237
|
+
return normalizeDocsPath(new URL(url).pathname);
|
|
238
|
+
}
|
|
239
|
+
function normalizeDocsPath(docsPath) {
|
|
240
|
+
let normalized = docsPath.trim();
|
|
241
|
+
if (normalized.startsWith("http://") || normalized.startsWith("https://")) {
|
|
242
|
+
normalized = new URL(normalized).pathname;
|
|
243
|
+
}
|
|
244
|
+
normalized = normalized.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
245
|
+
normalized = normalized.replace(/\.(md|mdx)$/i, "");
|
|
246
|
+
return normalized;
|
|
247
|
+
}
|
|
248
|
+
function safeDocsPath(root, relativePath) {
|
|
249
|
+
const fullPath = path.resolve(root, relativePath);
|
|
250
|
+
if (!fullPath.startsWith(root + path.sep)) {
|
|
251
|
+
throw new Error("Docs path must stay inside social-plus-docs.");
|
|
252
|
+
}
|
|
253
|
+
return fullPath;
|
|
254
|
+
}
|
|
255
|
+
async function findDocFiles(root) {
|
|
256
|
+
const files = [];
|
|
257
|
+
async function walk(directory) {
|
|
258
|
+
let entries;
|
|
259
|
+
try {
|
|
260
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".next") {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const entryPath = path.join(directory, entry.name);
|
|
270
|
+
if (entry.isDirectory()) {
|
|
271
|
+
await walk(entryPath);
|
|
272
|
+
}
|
|
273
|
+
else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
|
|
274
|
+
safeDocsPath(root, path.relative(root, entryPath));
|
|
275
|
+
files.push(entryPath);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
await walk(root);
|
|
280
|
+
return files;
|
|
281
|
+
}
|
|
282
|
+
function cleanMdx(content) {
|
|
283
|
+
return content
|
|
284
|
+
.replace(/^---[\s\S]*?---\s*/m, "")
|
|
285
|
+
.replace(/^import .*$/gm, "")
|
|
286
|
+
.replace(/<[^>\n]+>/g, "")
|
|
287
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
288
|
+
.trim();
|
|
289
|
+
}
|
|
290
|
+
function titleFromContent(content, file) {
|
|
291
|
+
const heading = content.match(/^#\s+(.+)$/m);
|
|
292
|
+
if (heading) {
|
|
293
|
+
return heading[1].trim();
|
|
294
|
+
}
|
|
295
|
+
return path.basename(file).replace(/\.(md|mdx)$/i, "");
|
|
296
|
+
}
|
|
297
|
+
function snippetFor(content, terms) {
|
|
298
|
+
const lower = content.toLowerCase();
|
|
299
|
+
const index = terms.map((term) => lower.indexOf(term)).filter((value) => value >= 0).sort((a, b) => a - b)[0] ?? 0;
|
|
300
|
+
const start = Math.max(0, index - 120);
|
|
301
|
+
const end = Math.min(content.length, index + 240);
|
|
302
|
+
return content.slice(start, end).replace(/\s+/g, " ").trim();
|
|
303
|
+
}
|
|
304
|
+
function countOccurrences(content, term) {
|
|
305
|
+
let count = 0;
|
|
306
|
+
let index = content.indexOf(term);
|
|
307
|
+
while (index >= 0) {
|
|
308
|
+
count += 1;
|
|
309
|
+
index = content.indexOf(term, index + term.length);
|
|
310
|
+
}
|
|
311
|
+
return count;
|
|
312
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { classifyOutcome, getOutcomeDefinition } from "../outcomes.js";
|
|
4
|
+
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
5
|
+
import { inspectProject } from "./project.js";
|
|
6
|
+
export function harnessControlsFor(outcome, platforms) {
|
|
7
|
+
const docsQuery = getOutcomeDefinition(outcome).docsQuery(platforms[0] ?? "sdk");
|
|
8
|
+
return {
|
|
9
|
+
guides: [
|
|
10
|
+
{
|
|
11
|
+
name: "Resolve request",
|
|
12
|
+
kind: "guide",
|
|
13
|
+
execution: "computational",
|
|
14
|
+
timing: "before-change",
|
|
15
|
+
action: "resolve_request",
|
|
16
|
+
purpose: "Narrow the user's request into a known social.plus integration outcome.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "Canonical docs",
|
|
20
|
+
kind: "guide",
|
|
21
|
+
execution: "computational",
|
|
22
|
+
timing: "before-change",
|
|
23
|
+
action: `search_docs query=\"${docsQuery}\"`,
|
|
24
|
+
purpose: "Load source-of-truth setup guidance before proposing code.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "Integration plan",
|
|
28
|
+
kind: "guide",
|
|
29
|
+
execution: "inferential",
|
|
30
|
+
timing: "during-change",
|
|
31
|
+
action: "plan_integration",
|
|
32
|
+
purpose: "Give the coding agent an evidence-backed implementation packet before edits happen.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
sensors: [
|
|
36
|
+
{
|
|
37
|
+
name: "Project inspection",
|
|
38
|
+
kind: "sensor",
|
|
39
|
+
execution: "computational",
|
|
40
|
+
timing: "before-change",
|
|
41
|
+
action: "inspect_project",
|
|
42
|
+
purpose: "Detect platform and framework signals from customer-owned files.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Setup validator",
|
|
46
|
+
kind: "sensor",
|
|
47
|
+
execution: "computational",
|
|
48
|
+
timing: "after-change",
|
|
49
|
+
action: "validate_setup",
|
|
50
|
+
purpose: "Catch common setup mistakes after the agent inspects or changes the app.",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
steeringLoop: [
|
|
54
|
+
"Resolve the request and inspect the project.",
|
|
55
|
+
"Fetch only the docs needed for the detected platform and outcome.",
|
|
56
|
+
"Generate a reviewable integration plan before edits.",
|
|
57
|
+
"Apply edits in the host coding agent, not inside Vise v1.",
|
|
58
|
+
"Run deterministic sensors first: setup validation and detected project checks.",
|
|
59
|
+
"Use inferential review only after deterministic signals are available.",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export const planHarnessTool = {
|
|
64
|
+
name: "plan_harness",
|
|
65
|
+
description: "Build the feedforward guides, feedback sensors, and steering loop for a social.plus SDK integration request.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
repoPath: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Absolute or relative path to the customer repository root.",
|
|
72
|
+
},
|
|
73
|
+
request: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Natural-language integration request.",
|
|
76
|
+
},
|
|
77
|
+
surfacePath: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Optional app/workspace path inside repoPath, such as apps/web or apps/mobile.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ["repoPath", "request"],
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
},
|
|
85
|
+
async call(input) {
|
|
86
|
+
const args = objectInput(input);
|
|
87
|
+
const repoPath = stringField(args, "repoPath");
|
|
88
|
+
const request = stringField(args, "request");
|
|
89
|
+
return textResult(await buildHarnessPlan(repoPath, request, optionalStringField(args, "surfacePath")));
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
async function buildHarnessPlan(repoPath, request, surfacePath) {
|
|
93
|
+
const inspection = await inspectProject(repoPath, surfacePath);
|
|
94
|
+
const outcome = classifyOutcome(request);
|
|
95
|
+
const controls = harnessControlsFor(outcome, inspection.platforms);
|
|
96
|
+
const commandSensors = await detectCommandSensors(inspection.effectiveRoot, inspection.platforms);
|
|
97
|
+
const harnessability = assessHarnessability(inspection.platforms, commandSensors, inspection.designSignals.length);
|
|
98
|
+
return {
|
|
99
|
+
outcome,
|
|
100
|
+
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
101
|
+
availableSurfaces: inspection.surfaces.map((surface) => ({ path: surface.path, platforms: surface.platforms })),
|
|
102
|
+
targetPlatforms: inspection.platforms,
|
|
103
|
+
harnessability,
|
|
104
|
+
guides: controls.guides,
|
|
105
|
+
sensors: [...controls.sensors, ...commandSensors],
|
|
106
|
+
steeringLoop: controls.steeringLoop,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function detectCommandSensors(repoPath, platforms) {
|
|
110
|
+
const root = path.resolve(repoPath);
|
|
111
|
+
const sensors = [];
|
|
112
|
+
if (platforms.includes("typescript") || platforms.includes("react-native")) {
|
|
113
|
+
sensors.push(...(await packageJsonSensors(root)));
|
|
114
|
+
}
|
|
115
|
+
if (platforms.includes("android") && (await exists(path.join(root, "gradlew")))) {
|
|
116
|
+
sensors.push({
|
|
117
|
+
name: "Android assemble",
|
|
118
|
+
command: ["./gradlew", "assembleDebug"],
|
|
119
|
+
timing: "after-change",
|
|
120
|
+
purpose: "Check Android project compilation after SDK setup changes.",
|
|
121
|
+
source: "gradlew",
|
|
122
|
+
}, {
|
|
123
|
+
name: "Android unit tests",
|
|
124
|
+
command: ["./gradlew", "test"],
|
|
125
|
+
timing: "after-change",
|
|
126
|
+
purpose: "Run available Android JVM tests after integration changes.",
|
|
127
|
+
source: "gradlew",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (platforms.includes("flutter")) {
|
|
131
|
+
sensors.push({
|
|
132
|
+
name: "Flutter analyze",
|
|
133
|
+
command: ["flutter", "analyze"],
|
|
134
|
+
timing: "after-change",
|
|
135
|
+
purpose: "Check Dart static analysis after SDK setup changes.",
|
|
136
|
+
source: "pubspec.yaml",
|
|
137
|
+
}, {
|
|
138
|
+
name: "Flutter tests",
|
|
139
|
+
command: ["flutter", "test"],
|
|
140
|
+
timing: "after-change",
|
|
141
|
+
purpose: "Run available Flutter tests after integration changes.",
|
|
142
|
+
source: "pubspec.yaml",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return sensors;
|
|
146
|
+
}
|
|
147
|
+
async function packageJsonSensors(root) {
|
|
148
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const scripts = parsed.scripts ?? {};
|
|
157
|
+
const preferredScripts = ["typecheck", "build", "test", "lint"];
|
|
158
|
+
const sensors = preferredScripts
|
|
159
|
+
.filter((script) => scripts[script])
|
|
160
|
+
.map((script) => ({
|
|
161
|
+
name: `npm ${script}`,
|
|
162
|
+
command: ["npm", "run", script],
|
|
163
|
+
timing: "after-change",
|
|
164
|
+
purpose: `Run the project's ${script} script as a deterministic feedback sensor.`,
|
|
165
|
+
source: "package.json",
|
|
166
|
+
}));
|
|
167
|
+
if (hasPackageDependency(parsed, "@amityco/ts-sdk")) {
|
|
168
|
+
sensors.push({
|
|
169
|
+
name: "TypeScript SDK import smoke",
|
|
170
|
+
command: [
|
|
171
|
+
"node",
|
|
172
|
+
"-e",
|
|
173
|
+
"require.resolve('@amityco/ts-sdk'); console.log('Vise smoke: @amityco/ts-sdk resolves')",
|
|
174
|
+
],
|
|
175
|
+
timing: "after-change",
|
|
176
|
+
purpose: "Verify the social.plus TypeScript SDK resolves from the host project runtime environment.",
|
|
177
|
+
source: "package.json",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return sensors;
|
|
181
|
+
}
|
|
182
|
+
function hasPackageDependency(packageJson, dependencyName) {
|
|
183
|
+
return Boolean(packageJson.dependencies?.[dependencyName] ??
|
|
184
|
+
packageJson.devDependencies?.[dependencyName] ??
|
|
185
|
+
packageJson.peerDependencies?.[dependencyName] ??
|
|
186
|
+
packageJson.optionalDependencies?.[dependencyName]);
|
|
187
|
+
}
|
|
188
|
+
function assessHarnessability(platforms, commandSensors, designSignalCount) {
|
|
189
|
+
const affordances = [];
|
|
190
|
+
const gaps = [];
|
|
191
|
+
if (platforms.length > 0) {
|
|
192
|
+
affordances.push(`Detected platform signals: ${platforms.join(", ")}.`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
gaps.push("No platform signals detected; ask the user for the app framework or repository root.");
|
|
196
|
+
}
|
|
197
|
+
if (platforms.some((platform) => ["typescript", "react-native", "android", "flutter"].includes(platform))) {
|
|
198
|
+
affordances.push("Detected a platform with deterministic setup checks available in Vise.");
|
|
199
|
+
}
|
|
200
|
+
if (commandSensors.length > 0) {
|
|
201
|
+
affordances.push(`Detected ${commandSensors.length} project command sensor(s).`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
gaps.push("No project build/typecheck/test command sensors were detected yet.");
|
|
205
|
+
}
|
|
206
|
+
if (designSignalCount > 0) {
|
|
207
|
+
affordances.push(`Detected ${designSignalCount} design/theme signal(s) for UI integration grounding.`);
|
|
208
|
+
}
|
|
209
|
+
if (platforms.includes("ios")) {
|
|
210
|
+
gaps.push("iOS support is guided until deterministic validators are expanded.");
|
|
211
|
+
}
|
|
212
|
+
if (platforms.length === 0) {
|
|
213
|
+
return { level: "weak", affordances, gaps };
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
level: commandSensors.length > 0 ? "strong" : "moderate",
|
|
217
|
+
affordances,
|
|
218
|
+
gaps,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function exists(filePath) {
|
|
222
|
+
try {
|
|
223
|
+
await access(filePath);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|