@amityco/social-plus-vise 0.14.27 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +9 -14
- package/dist/capabilities.js +44 -0
- package/dist/server.js +7 -1
- package/dist/tools/ast.js +302 -25
- package/dist/tools/blocks.js +95 -2
- package/dist/tools/compliance.js +15 -4
- package/dist/tools/docs.js +48 -0
- package/dist/tools/harness.js +114 -1
- package/dist/tools/project.js +106 -11
- package/dist/tools/sdkFacts.js +128 -7
- package/dist/tools/sensors.js +30 -0
- package/docs-cache/README.md +34 -0
- package/docs-cache/social-plus-sdk/core-concepts/foundation/logging.mdx +236 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android.mdx +262 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter.mdx +195 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios.mdx +452 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview.mdx +133 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript.mdx +264 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device.mdx +191 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview.mdx +43 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup.mdx +360 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup.mdx +457 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup.mdx +423 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup.mdx +384 -0
- package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview.mdx +94 -0
- package/docs-cache/social-plus-sdk/getting-started/authentication.mdx +808 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start.mdx +304 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start.mdx +121 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start.mdx +225 -0
- package/docs-cache/social-plus-sdk/getting-started/platform-setup/web/web-quick-start.mdx +99 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/community-invitation.mdx +459 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/join-leave-community.mdx +449 -0
- package/docs-cache/social-plus-sdk/social/communities-spaces/organization/query-community-members.mdx +376 -0
- package/docs-cache/social-plus-sdk/social/content-management/posts/creation/text-post.mdx +318 -0
- package/docs-cache/social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status.mdx +399 -0
- package/docs-cache/social-plus-sdk/social/user-relationship/blocking/block-unblock-user.mdx +166 -0
- package/docs-cache/social-plus-sdk/social/user-relationship/following/get-follower-following-list.mdx +339 -0
- package/package.json +15 -3
- package/scripts/dart-model-extractor/bin/extract_models.dart +169 -0
- package/scripts/dart-model-extractor/pubspec.lock +149 -0
- package/scripts/dart-model-extractor/pubspec.yaml +16 -0
- package/scripts/extract-sdk-models.mjs +749 -0
- package/scripts/import-sdk-surface.mjs +52 -21
- package/sdk-surface/manifest.json +48 -2
- package/sdk-surface/models.android.json +990 -0
- package/sdk-surface/models.flutter.json +980 -0
- package/sdk-surface/models.ios.json +980 -0
- package/sdk-surface/models.typescript.json +1304 -0
- package/skills/social-plus-vise/SKILL.md +15 -1
package/dist/tools/ast.js
CHANGED
|
@@ -29,11 +29,21 @@ function loadNativeBindings() {
|
|
|
29
29
|
const ParserCtor = nodeRequire("tree-sitter");
|
|
30
30
|
const tsGrammars = nodeRequire("tree-sitter-typescript");
|
|
31
31
|
const kotlinGrammar = nodeRequire("tree-sitter-kotlin");
|
|
32
|
+
// Swift loads in its own try/catch: a missing tree-sitter-swift prebuild on an
|
|
33
|
+
// exotic platform must degrade ONLY the Swift helpers, not all AST analysis.
|
|
34
|
+
let swiftGrammar = null;
|
|
35
|
+
try {
|
|
36
|
+
swiftGrammar = nodeRequire("tree-sitter-swift");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
swiftGrammar = null;
|
|
40
|
+
}
|
|
32
41
|
nativeBindings = {
|
|
33
42
|
Parser: ParserCtor,
|
|
34
43
|
tsGrammar: tsGrammars.typescript,
|
|
35
44
|
tsxGrammar: tsGrammars.tsx,
|
|
36
45
|
kotlinGrammar,
|
|
46
|
+
swiftGrammar,
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
catch {
|
|
@@ -50,6 +60,14 @@ function loadNativeBindings() {
|
|
|
50
60
|
export function astAvailable() {
|
|
51
61
|
return loadNativeBindings() !== null;
|
|
52
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether the Swift grammar specifically is available. tree-sitter-swift is loaded
|
|
65
|
+
* independently of the core bindings (see loadNativeBindings), so Swift-only
|
|
66
|
+
* validators can check this and fall back to regex without disabling ts/tsx/kotlin.
|
|
67
|
+
*/
|
|
68
|
+
export function swiftAstAvailable() {
|
|
69
|
+
return loadNativeBindings()?.swiftGrammar != null;
|
|
70
|
+
}
|
|
53
71
|
/**
|
|
54
72
|
* Strip comments from source code using tree-sitter AST.
|
|
55
73
|
* Replaces comment spans with whitespace (preserving line structure).
|
|
@@ -101,6 +119,12 @@ function getParser(language) {
|
|
|
101
119
|
parser.setLanguage(native.tsxGrammar);
|
|
102
120
|
else if (language === "kotlin")
|
|
103
121
|
parser.setLanguage(native.kotlinGrammar);
|
|
122
|
+
else if (language === "swift") {
|
|
123
|
+
if (native.swiftGrammar == null) {
|
|
124
|
+
throw new Error("tree-sitter-swift unavailable; Swift AST analysis disabled (regex fallback in effect)");
|
|
125
|
+
}
|
|
126
|
+
parser.setLanguage(native.swiftGrammar);
|
|
127
|
+
}
|
|
104
128
|
else
|
|
105
129
|
parser.setLanguage(native.tsGrammar);
|
|
106
130
|
parsers.set(language, parser);
|
|
@@ -166,7 +190,10 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
166
190
|
results.push({ callee, node, args });
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
169
|
-
// Kotlin: call_expression = navigation_expression + call_suffix
|
|
193
|
+
// Kotlin AND Swift: call_expression = navigation_expression + call_suffix.
|
|
194
|
+
// (tree-sitter-swift descends from the Kotlin grammar, so the node names —
|
|
195
|
+
// navigation_expression / navigation_suffix / call_suffix / value_arguments —
|
|
196
|
+
// are identical; only Swift's labeled arguments add a value_argument_label.)
|
|
170
197
|
const navNode = node.namedChild(0);
|
|
171
198
|
const suffixNode = node.namedChild(1);
|
|
172
199
|
if (!navNode || !suffixNode || suffixNode.type !== "call_suffix")
|
|
@@ -181,8 +208,11 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
181
208
|
for (let i = 0; i < valArgsNode.namedChildCount; i++) {
|
|
182
209
|
const valArg = valArgsNode.namedChild(i);
|
|
183
210
|
if (valArg && valArg.type === "value_argument") {
|
|
184
|
-
// The actual expression is inside value_argument
|
|
185
|
-
|
|
211
|
+
// The actual expression is inside value_argument. Swift labeled arguments
|
|
212
|
+
// put a value_argument_label first — the value is the last named child.
|
|
213
|
+
// Kotlin behaviour (namedChild(0)) is intentionally unchanged.
|
|
214
|
+
const first = valArg.namedChild(0);
|
|
215
|
+
const expr = first?.type === "value_argument_label" ? valArg.namedChild(valArg.namedChildCount - 1) : first;
|
|
186
216
|
if (expr)
|
|
187
217
|
args.push(expr);
|
|
188
218
|
}
|
|
@@ -192,47 +222,239 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
192
222
|
});
|
|
193
223
|
return results;
|
|
194
224
|
}
|
|
225
|
+
// Bound on identifier→identifier resolution chains. Five hops covers realistic
|
|
226
|
+
// re-binding (`const RAW = "…"; const T = RAW; const X = T;`) while keeping the
|
|
227
|
+
// walk cheap; a `visited` set guards against cyclic self-reference (which would
|
|
228
|
+
// be a type error anyway, but the parser still produces a tree for it).
|
|
229
|
+
const MAX_RESOLUTION_HOPS = 5;
|
|
195
230
|
/**
|
|
196
231
|
* Resolve an AST node to its literal string value within the same file.
|
|
197
232
|
*
|
|
198
233
|
* Handles:
|
|
199
234
|
* - String literals directly → returns the string value
|
|
200
|
-
* -
|
|
201
|
-
*
|
|
235
|
+
* - A pure-passthrough template literal — `` `${X}` `` with exactly one
|
|
236
|
+
* substitution and no cooked text — which is value-equivalent to `X`; the
|
|
237
|
+
* substitution is resolved through this same resolver. Templates with any
|
|
238
|
+
* literal text (`` `pre${X}` ``) or a dynamic substitution (`` `${a.b}` ``)
|
|
239
|
+
* are NOT pure passthrough and stay unresolved. (Arbitrary string
|
|
240
|
+
* concatenation is a separate, larger blind spot — intentionally not handled.)
|
|
241
|
+
* - Identifiers that reference a const/let/var whose initializer resolves to a
|
|
242
|
+
* literal, following identifier→identifier re-bind chains up to
|
|
243
|
+
* MAX_RESOLUTION_HOPS deep (multi-hop), with a cycle guard. A binding to any
|
|
244
|
+
* dynamic expression (member access, call, parameter, env fallback, …)
|
|
245
|
+
* terminates the chain and stays unresolved — multi-hop never makes a runtime
|
|
246
|
+
* value look literal.
|
|
202
247
|
*
|
|
203
248
|
* Returns undefined if the value cannot be statically resolved.
|
|
204
249
|
*/
|
|
205
250
|
export function resolveLiteralValue(node, tree) {
|
|
251
|
+
return resolveLiteralValueBounded(node, tree, MAX_RESOLUTION_HOPS, new Set());
|
|
252
|
+
}
|
|
253
|
+
function resolveLiteralValueBounded(node, tree, hopsLeft, visited) {
|
|
206
254
|
// Direct string literal
|
|
207
255
|
const directValue = extractStringLiteral(node);
|
|
208
256
|
if (directValue !== undefined)
|
|
209
257
|
return directValue;
|
|
210
|
-
//
|
|
211
|
-
//
|
|
258
|
+
// Pure-passthrough template literal: `${X}` (exactly one substitution, no text).
|
|
259
|
+
// Equivalent to resolving X itself. Bounded to this one shape — not concat.
|
|
260
|
+
if (node.type === "template_string") {
|
|
261
|
+
const passthrough = templatePassthroughSubstitution(node);
|
|
262
|
+
if (passthrough)
|
|
263
|
+
return resolveLiteralValueBounded(passthrough, tree, hopsLeft, visited);
|
|
264
|
+
}
|
|
265
|
+
// Identifier — resolve to its declaration in the same file, then recurse into
|
|
266
|
+
// the initializer so identifier→identifier re-binds and template re-wraps
|
|
267
|
+
// chain through to the underlying literal.
|
|
268
|
+
// TypeScript uses "identifier", Kotlin uses "simple_identifier".
|
|
212
269
|
if (node.type === "identifier" || node.type === "simple_identifier") {
|
|
270
|
+
if (hopsLeft <= 0)
|
|
271
|
+
return undefined;
|
|
213
272
|
const name = node.text;
|
|
214
|
-
|
|
273
|
+
if (visited.has(name))
|
|
274
|
+
return undefined; // cycle guard
|
|
275
|
+
visited.add(name);
|
|
276
|
+
const valueNode = resolveIdentifierToValueNode(name, tree.rootNode);
|
|
277
|
+
if (!valueNode)
|
|
278
|
+
return undefined;
|
|
279
|
+
return resolveLiteralValueBounded(valueNode, tree, hopsLeft - 1, visited);
|
|
215
280
|
}
|
|
216
281
|
return undefined;
|
|
217
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* If `node` is a template literal that is a pure passthrough of a single
|
|
285
|
+
* substitution — `` `${X}` `` with exactly one `template_substitution` child,
|
|
286
|
+
* no `string_fragment` (cooked text) — return that substitution's inner
|
|
287
|
+
* expression node. Otherwise undefined.
|
|
288
|
+
*/
|
|
289
|
+
function templatePassthroughSubstitution(node) {
|
|
290
|
+
if (node.type !== "template_string")
|
|
291
|
+
return undefined;
|
|
292
|
+
let substitution;
|
|
293
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
294
|
+
const child = node.namedChild(i);
|
|
295
|
+
if (!child)
|
|
296
|
+
continue;
|
|
297
|
+
if (child.type === "template_substitution") {
|
|
298
|
+
if (substitution)
|
|
299
|
+
return undefined; // more than one substitution → not passthrough
|
|
300
|
+
substitution = child;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Any other named child (e.g. string_fragment cooked text) → has literal
|
|
304
|
+
// content, so not a pure passthrough.
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!substitution)
|
|
309
|
+
return undefined;
|
|
310
|
+
// The substitution wraps a single expression: `${ expr }`.
|
|
311
|
+
return substitution.namedChild(0) ?? undefined;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Unwrap TypeScript expression wrappers that are transparent for static value
|
|
315
|
+
* extraction — type casts and grouping that don't change the runtime value:
|
|
316
|
+
* - `expr as T` (as_expression)
|
|
317
|
+
* - `expr satisfies T` (satisfies_expression)
|
|
318
|
+
* - `(expr)` (parenthesized_expression)
|
|
319
|
+
* - `expr!` (non_null_expression)
|
|
320
|
+
* - `<T>expr` (type_assertion, angle-bracket cast)
|
|
321
|
+
* Nested wrappers are peeled fully (`({ … } as any)!`). The inner expression is
|
|
322
|
+
* always the wrapper's FIRST named child across these node types. Non-wrapper
|
|
323
|
+
* nodes are returned unchanged, so callers can apply this unconditionally.
|
|
324
|
+
*
|
|
325
|
+
* High-frequency motivation: agents emit `as any` constantly to silence TS
|
|
326
|
+
* errors, which otherwise hides the payload object from property extraction.
|
|
327
|
+
*/
|
|
328
|
+
export function unwrapExpression(node) {
|
|
329
|
+
let current = node;
|
|
330
|
+
const wrappers = new Set([
|
|
331
|
+
"as_expression",
|
|
332
|
+
"satisfies_expression",
|
|
333
|
+
"parenthesized_expression",
|
|
334
|
+
"non_null_expression",
|
|
335
|
+
"type_assertion",
|
|
336
|
+
]);
|
|
337
|
+
// Bounded peel: a handful of stacked casts at most; the guard prevents any
|
|
338
|
+
// pathological loop if a grammar ever yields a self-referential first child.
|
|
339
|
+
for (let i = 0; i < 16 && wrappers.has(current.type); i++) {
|
|
340
|
+
const inner = current.namedChild(0);
|
|
341
|
+
if (!inner)
|
|
342
|
+
break;
|
|
343
|
+
current = inner;
|
|
344
|
+
}
|
|
345
|
+
return current;
|
|
346
|
+
}
|
|
218
347
|
/**
|
|
219
348
|
* Pick a specific named property from an object argument node.
|
|
220
349
|
* E.g., from `{ userId: HARDCODED }`, pick the value node for "userId".
|
|
350
|
+
*
|
|
351
|
+
* The argument is unwrapped first, so a cast/parenthesized/non-null-wrapped
|
|
352
|
+
* payload (`{ … } as any`, `({ … })`, `{ … }!`) is still treated as the inner
|
|
353
|
+
* object — otherwise the wrapper hides every property from extraction.
|
|
221
354
|
*/
|
|
222
355
|
export function pickObjectProperty(objectNode, propertyName) {
|
|
223
|
-
|
|
356
|
+
const unwrapped = unwrapExpression(objectNode);
|
|
357
|
+
if (unwrapped.type !== "object")
|
|
224
358
|
return undefined;
|
|
225
|
-
for (let i = 0; i <
|
|
226
|
-
const prop =
|
|
359
|
+
for (let i = 0; i < unwrapped.namedChildCount; i++) {
|
|
360
|
+
const prop = unwrapped.namedChild(i);
|
|
227
361
|
if (!prop || prop.type !== "pair")
|
|
228
362
|
continue;
|
|
229
363
|
const key = prop.childForFieldName("key");
|
|
230
364
|
if (key && key.text === propertyName) {
|
|
231
|
-
|
|
365
|
+
const value = prop.childForFieldName("value");
|
|
366
|
+
return value ? unwrapExpression(value) : undefined;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Pick the value of a labeled argument from a Swift call expression node.
|
|
373
|
+
* E.g., from `client.login(userId: HARDCODED, sessionHandler: handler)`,
|
|
374
|
+
* pick the value node for label "userId".
|
|
375
|
+
*
|
|
376
|
+
* Swift-shaped trees only (call_suffix → value_arguments → value_argument with a
|
|
377
|
+
* value_argument_label). Returns undefined when the label is absent.
|
|
378
|
+
*/
|
|
379
|
+
export function pickLabeledArgument(callNode, label) {
|
|
380
|
+
for (let i = 0; i < callNode.namedChildCount; i++) {
|
|
381
|
+
const suffix = callNode.namedChild(i);
|
|
382
|
+
if (!suffix || suffix.type !== "call_suffix")
|
|
383
|
+
continue;
|
|
384
|
+
const valArgs = suffix.namedChild(0);
|
|
385
|
+
if (!valArgs || valArgs.type !== "value_arguments")
|
|
386
|
+
continue;
|
|
387
|
+
for (let j = 0; j < valArgs.namedChildCount; j++) {
|
|
388
|
+
const valArg = valArgs.namedChild(j);
|
|
389
|
+
if (!valArg || valArg.type !== "value_argument")
|
|
390
|
+
continue;
|
|
391
|
+
const first = valArg.namedChild(0);
|
|
392
|
+
if (first?.type !== "value_argument_label" || first.text !== label)
|
|
393
|
+
continue;
|
|
394
|
+
const value = valArg.namedChild(valArg.namedChildCount - 1);
|
|
395
|
+
// namedChild(last) === the label itself when the argument has no value
|
|
396
|
+
// (selector-reference form) — treat as absent.
|
|
397
|
+
return value && value !== first ? value : undefined;
|
|
232
398
|
}
|
|
233
399
|
}
|
|
234
400
|
return undefined;
|
|
235
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Find Swift `let`/`var` declarations whose initializer text matches a pattern AND
|
|
404
|
+
* which are function-local (declared inside a function/initializer/closure/computed-
|
|
405
|
+
* property body rather than at type or file scope). Used for retention rules: a
|
|
406
|
+
* function-local binding dies with the stack frame, while a type-scope property
|
|
407
|
+
* lives with the object — a distinction regex bridges cannot make reliably.
|
|
408
|
+
*
|
|
409
|
+
* Conservative direction: a declaration only counts as local when a KNOWN
|
|
410
|
+
* function-ish ancestor encloses it, so unknown containers degrade toward
|
|
411
|
+
* "retained" (quiet), never toward a false positive.
|
|
412
|
+
*/
|
|
413
|
+
export function findSwiftFunctionLocalDeclarations(tree, initializerPattern) {
|
|
414
|
+
const results = [];
|
|
415
|
+
walkTree(tree.rootNode, (node) => {
|
|
416
|
+
if (node.type !== "property_declaration")
|
|
417
|
+
return;
|
|
418
|
+
const value = swiftPropertyInitializer(node);
|
|
419
|
+
if (!value || !initializerPattern.test(value.text))
|
|
420
|
+
return;
|
|
421
|
+
if (hasFunctionAncestor(node))
|
|
422
|
+
results.push(node);
|
|
423
|
+
});
|
|
424
|
+
return results;
|
|
425
|
+
}
|
|
426
|
+
const SWIFT_FUNCTION_SCOPES = new Set([
|
|
427
|
+
"function_declaration",
|
|
428
|
+
"init_declaration",
|
|
429
|
+
"deinit_declaration",
|
|
430
|
+
"lambda_literal",
|
|
431
|
+
"computed_property",
|
|
432
|
+
"computed_getter",
|
|
433
|
+
"computed_setter",
|
|
434
|
+
]);
|
|
435
|
+
function hasFunctionAncestor(node) {
|
|
436
|
+
let current = node.parent;
|
|
437
|
+
while (current) {
|
|
438
|
+
if (SWIFT_FUNCTION_SCOPES.has(current.type))
|
|
439
|
+
return true;
|
|
440
|
+
current = current.parent;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
/** The initializer expression of a Swift property_declaration (the value after `=`), if any. */
|
|
445
|
+
function swiftPropertyInitializer(node) {
|
|
446
|
+
// Shape: property_declaration = value_binding_pattern, pattern, [type_annotation], [value expr]
|
|
447
|
+
// The initializer (when present) is the last named child and is none of the structural parts.
|
|
448
|
+
const last = node.namedChild(node.namedChildCount - 1);
|
|
449
|
+
if (!last)
|
|
450
|
+
return undefined;
|
|
451
|
+
if (["value_binding_pattern", "pattern", "type_annotation", "modifiers", "attribute"].includes(last.type))
|
|
452
|
+
return undefined;
|
|
453
|
+
// computed_property is a body, not an initializer.
|
|
454
|
+
if (last.type === "computed_property")
|
|
455
|
+
return undefined;
|
|
456
|
+
return last;
|
|
457
|
+
}
|
|
236
458
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
237
459
|
function walkTree(node, visit) {
|
|
238
460
|
visit(node);
|
|
@@ -322,9 +544,47 @@ function extractStringLiteral(node) {
|
|
|
322
544
|
return text.slice(1, -1);
|
|
323
545
|
}
|
|
324
546
|
}
|
|
547
|
+
// Swift: line_string_literal contains line_str_text chunks; an interpolated
|
|
548
|
+
// string has >1 named child (line_str_text + interpolated_expression) and is
|
|
549
|
+
// NOT statically resolvable — mirror the TS template-literal treatment.
|
|
550
|
+
if (node.type === "line_string_literal") {
|
|
551
|
+
if (node.namedChildCount === 0)
|
|
552
|
+
return ""; // empty string ""
|
|
553
|
+
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "line_str_text") {
|
|
554
|
+
return node.namedChild(0).text;
|
|
555
|
+
}
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
// Swift: multi-line """…""" literal. Swift semantics strip the newline after the
|
|
559
|
+
// opening and before the closing delimiter.
|
|
560
|
+
if (node.type === "multi_line_string_literal") {
|
|
561
|
+
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "multi_line_str_text") {
|
|
562
|
+
return node.namedChild(0).text.replace(/^\n/, "").replace(/\n[ \t]*$/, "");
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
325
566
|
return undefined;
|
|
326
567
|
}
|
|
327
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Find the binding for `name` in the same file and return the AST node that is
|
|
570
|
+
* its initializer VALUE — not the resolved string. The caller
|
|
571
|
+
* (resolveLiteralValueBounded) then recurses into that node, so an initializer
|
|
572
|
+
* that is itself an identifier (re-bind) or a pure-passthrough template literal
|
|
573
|
+
* chains through to the underlying literal. Returns undefined when the binding
|
|
574
|
+
* is absent or is a parameter / dynamic expression rather than a value
|
|
575
|
+
* initializer.
|
|
576
|
+
*
|
|
577
|
+
* Per-platform value node:
|
|
578
|
+
* - TypeScript/JS: `variable_declarator` → its `value` field.
|
|
579
|
+
* - Kotlin: `property_declaration` with a `variable_declaration` → the initializer
|
|
580
|
+
* sibling (the node after `=`).
|
|
581
|
+
* - Swift: `property_declaration` with `pattern` → the DIRECT string-literal
|
|
582
|
+
* initializer only. A literal nested inside another expression
|
|
583
|
+
* (`env["KEY"] ?? ""`) is not a static binding and stays unresolved — so for
|
|
584
|
+
* Swift we deliberately surface only a line/multi-line string literal node,
|
|
585
|
+
* preserving the prior conservative behavior.
|
|
586
|
+
*/
|
|
587
|
+
function resolveIdentifierToValueNode(name, root) {
|
|
328
588
|
let result;
|
|
329
589
|
walkTree(root, (node) => {
|
|
330
590
|
if (result !== undefined)
|
|
@@ -337,25 +597,42 @@ function resolveIdentifierToLiteral(name, root) {
|
|
|
337
597
|
const valueNode = node.childForFieldName("value");
|
|
338
598
|
if (!valueNode)
|
|
339
599
|
return;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
600
|
+
// Casts/grouping around the initializer are transparent (e.g.
|
|
601
|
+
// `const T = RAW as string;`).
|
|
602
|
+
result = unwrapExpression(valueNode);
|
|
343
603
|
return;
|
|
344
604
|
}
|
|
345
|
-
// Kotlin: property_declaration with variable_declaration +
|
|
605
|
+
// Kotlin: property_declaration with variable_declaration + initializer
|
|
346
606
|
if (node.type === "property_declaration") {
|
|
347
607
|
const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
|
|
348
|
-
if (
|
|
608
|
+
if (varDecl) {
|
|
609
|
+
const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
|
|
610
|
+
if (!idNode || idNode.text !== name)
|
|
611
|
+
return;
|
|
612
|
+
// The initializer is the named child after the variable_declaration.
|
|
613
|
+
const declIdx = node.namedChildren.indexOf(varDecl);
|
|
614
|
+
const initializer = node.namedChildren[declIdx + 1];
|
|
615
|
+
// Only surface a string literal or a re-bind identifier as the value node;
|
|
616
|
+
// anything else (call, when-expression, etc.) is dynamic and stays
|
|
617
|
+
// unresolved, matching the prior literal-only behavior.
|
|
618
|
+
if (initializer && (initializer.type === "string_literal" || initializer.type === "simple_identifier")) {
|
|
619
|
+
result = initializer;
|
|
620
|
+
}
|
|
349
621
|
return;
|
|
350
|
-
|
|
351
|
-
|
|
622
|
+
}
|
|
623
|
+
// Swift: property_declaration with pattern → simple_identifier; the string
|
|
624
|
+
// literal (or a re-bind identifier) must be a DIRECT child (the
|
|
625
|
+
// initializer) — a literal nested inside e.g. `env["KEY"] ?? ""` is not a
|
|
626
|
+
// static binding and must not resolve.
|
|
627
|
+
const pattern = node.namedChildren.find((c) => c.type === "pattern");
|
|
628
|
+
if (!pattern)
|
|
352
629
|
return;
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
630
|
+
const swiftId = pattern.namedChildren.find((c) => c.type === "simple_identifier");
|
|
631
|
+
if (!swiftId || swiftId.text !== name)
|
|
355
632
|
return;
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
result =
|
|
633
|
+
const swiftValue = node.namedChildren.find((c) => c.type === "line_string_literal" || c.type === "multi_line_string_literal" || c.type === "simple_identifier");
|
|
634
|
+
if (swiftValue)
|
|
635
|
+
result = swiftValue;
|
|
359
636
|
}
|
|
360
637
|
});
|
|
361
638
|
return result;
|
package/dist/tools/blocks.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { completenessCapabilityIds } from "../capabilities.js";
|
|
3
4
|
import { packageVersion } from "../version.js";
|
|
4
5
|
import { inspectProject } from "./project.js";
|
|
5
6
|
import { detectCommandSensors } from "./harness.js";
|
|
6
7
|
import { readDesignContract } from "./design.js";
|
|
8
|
+
const blocksSidecarSchemaVersion = "2026-06-10.vise-blocks-sidecar.v1";
|
|
7
9
|
const registryPlatformByVisePlatform = {
|
|
8
10
|
typescript: "react",
|
|
9
11
|
"react-native": "react-native",
|
|
@@ -21,6 +23,7 @@ export async function listRegistryBlocks(registryPath) {
|
|
|
21
23
|
status: block.status,
|
|
22
24
|
surfaces: block.surfaces,
|
|
23
25
|
requiredSdkCapabilities: block.requiredSdkCapabilities,
|
|
26
|
+
providesCapabilities: providesCapabilitiesFor(block),
|
|
24
27
|
events: block.events,
|
|
25
28
|
})),
|
|
26
29
|
};
|
|
@@ -88,7 +91,8 @@ export async function validateBlockInstall(options) {
|
|
|
88
91
|
const repoPath = requiredRepoPath(options);
|
|
89
92
|
const sidecar = await readSidecar(repoPath);
|
|
90
93
|
const installed = (sidecar?.installed ?? []).filter((entry) => !options.blockId || entry.blockId === options.blockId);
|
|
91
|
-
|
|
94
|
+
// Seed with plan findings so the providesCapabilities seam guard also surfaces in validate mode.
|
|
95
|
+
const findings = [...plan.findings];
|
|
92
96
|
if (installed.length === 0) {
|
|
93
97
|
findings.push({
|
|
94
98
|
ruleId: "blocks.sidecar.installed",
|
|
@@ -154,6 +158,18 @@ async function buildInstallPlan(options, mode) {
|
|
|
154
158
|
stopConditions.push(`Missing safe install anchor ${targetFile.anchor} in ${targetFile.path}.`);
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
// Seam guard: the factory declares providesCapabilities, but Vise owns the
|
|
162
|
+
// completeness id vocabulary. Unknown ids warn (never block) — they simply
|
|
163
|
+
// can't satisfy a completeness gap in `vise check`.
|
|
164
|
+
const providesCapabilities = providesCapabilitiesFor(block);
|
|
165
|
+
const knownCapabilityIds = completenessCapabilityIds();
|
|
166
|
+
const unknownCapabilityIds = providesCapabilities.filter((id) => !knownCapabilityIds.has(id));
|
|
167
|
+
const findings = unknownCapabilityIds.map((id) => ({
|
|
168
|
+
ruleId: "blocks.providesCapabilities.known",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: `Block ${block.blockId} declares providesCapabilities id "${id}", which is not in this Vise's completeness checklist vocabulary.`,
|
|
171
|
+
recommendation: "Unknown ids never satisfy completeness gaps. Align the Block Factory contract with Vise's capability catalog (vise owns the id space) or upgrade Vise.",
|
|
172
|
+
}));
|
|
157
173
|
return {
|
|
158
174
|
status: stopConditions.length > 0 ? "needs-review" : "ready",
|
|
159
175
|
mode,
|
|
@@ -168,11 +184,19 @@ async function buildInstallPlan(options, mode) {
|
|
|
168
184
|
packageSource: options.packageSource,
|
|
169
185
|
packageChange,
|
|
170
186
|
targetFiles,
|
|
187
|
+
providesCapabilities,
|
|
188
|
+
findings,
|
|
171
189
|
sensors: sensors.map((sensor) => ({ name: sensor.name, command: sensor.command, source: sensor.source })),
|
|
172
190
|
stopConditions,
|
|
173
191
|
sidecarPath: path.join("sp-vise", "blocks.json"),
|
|
174
192
|
};
|
|
175
193
|
}
|
|
194
|
+
function providesCapabilitiesFor(block) {
|
|
195
|
+
if (!Array.isArray(block.providesCapabilities)) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
return [...new Set(block.providesCapabilities.filter((id) => typeof id === "string" && id.trim() !== ""))];
|
|
199
|
+
}
|
|
176
200
|
async function loadRegistry(registryPath) {
|
|
177
201
|
if (!registryPath) {
|
|
178
202
|
throw new Error("blocks command requires --registry <path>.");
|
|
@@ -286,7 +310,7 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
|
|
|
286
310
|
const sidecarPath = path.join(repoPath, "sp-vise", "blocks.json");
|
|
287
311
|
const existing = await readSidecar(repoPath);
|
|
288
312
|
const sidecar = existing ?? {
|
|
289
|
-
schemaVersion:
|
|
313
|
+
schemaVersion: blocksSidecarSchemaVersion,
|
|
290
314
|
viseVersion: packageVersion,
|
|
291
315
|
generatedAt: new Date().toISOString(),
|
|
292
316
|
installed: [],
|
|
@@ -296,11 +320,14 @@ async function writeBlocksSidecar(repoPath, plan, packageSource, filesTouched) {
|
|
|
296
320
|
blockVersion: plan.block.version,
|
|
297
321
|
platform: plan.registryPlatform,
|
|
298
322
|
packageSource,
|
|
323
|
+
dependencyName: plan.package.dependencyName,
|
|
324
|
+
providesCapabilities: plan.providesCapabilities,
|
|
299
325
|
filesTouched,
|
|
300
326
|
designContractDigest: designContract?.digest,
|
|
301
327
|
sdkFactsVersion: plan.block.version,
|
|
302
328
|
validationStatus: "installed",
|
|
303
329
|
};
|
|
330
|
+
sidecar.schemaVersion = blocksSidecarSchemaVersion;
|
|
304
331
|
sidecar.viseVersion = packageVersion;
|
|
305
332
|
sidecar.generatedAt = new Date().toISOString();
|
|
306
333
|
sidecar.installed = [...sidecar.installed.filter((item) => item.blockId !== plan.block.id), entry];
|
|
@@ -317,6 +344,72 @@ async function readSidecar(repoPath) {
|
|
|
317
344
|
return null;
|
|
318
345
|
}
|
|
319
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Registry-free evidence that installed blocks deliver completeness capabilities.
|
|
349
|
+
*
|
|
350
|
+
* `vise check` cannot assume registry access, so a sidecar entry's
|
|
351
|
+
* `providesCapabilities` only counts when the install is still locally valid:
|
|
352
|
+
* the block's manifest dependency is declared in the project's package manifest
|
|
353
|
+
* AND every recorded `filesTouched` path still exists. On drift (file removed,
|
|
354
|
+
* package gone, pre-providesCapabilities sidecar) the entry contributes nothing
|
|
355
|
+
* and the capability reverts to the normal completeness gap.
|
|
356
|
+
*/
|
|
357
|
+
export async function installedBlockProvidedCapabilities(repoPath) {
|
|
358
|
+
const repoRoot = path.resolve(repoPath);
|
|
359
|
+
const sidecar = await readSidecar(repoRoot);
|
|
360
|
+
if (!sidecar || !Array.isArray(sidecar.installed)) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
const provided = [];
|
|
364
|
+
for (const entry of sidecar.installed) {
|
|
365
|
+
const capabilities = Array.isArray(entry.providesCapabilities)
|
|
366
|
+
? entry.providesCapabilities.filter((id) => typeof id === "string" && id.trim() !== "")
|
|
367
|
+
: [];
|
|
368
|
+
if (capabilities.length === 0) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (!(await installedEntryLocallyValid(repoRoot, entry))) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
for (const capabilityId of capabilities) {
|
|
375
|
+
provided.push({ capabilityId, blockId: entry.blockId });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return provided;
|
|
379
|
+
}
|
|
380
|
+
async function installedEntryLocallyValid(repoRoot, entry) {
|
|
381
|
+
if (!(await blockDependencyDeclared(repoRoot, entry))) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
for (const touched of entry.filesTouched ?? []) {
|
|
385
|
+
if (typeof touched !== "string" || touched.trim() === "") {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (!(await exists(path.join(repoRoot, touched)))) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
async function blockDependencyDeclared(repoRoot, entry) {
|
|
395
|
+
const dependencyName = entry.dependencyName;
|
|
396
|
+
if (!dependencyName) {
|
|
397
|
+
// Pre-2026-06-10 sidecars carry no package evidence; fail closed so the gap stays.
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (entry.platform === "flutter") {
|
|
401
|
+
const source = await readFile(path.join(repoRoot, "pubspec.yaml"), "utf8").catch(() => "");
|
|
402
|
+
return new RegExp(`\\b${escapeRegExp(dependencyName)}\\s*:`).test(source);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const packageJson = await readPackageJson(path.join(repoRoot, "package.json"));
|
|
406
|
+
const devDependencies = packageJson.devDependencies;
|
|
407
|
+
return Boolean(packageJson.dependencies?.[dependencyName] ?? devDependencies?.[dependencyName]);
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
320
413
|
function dependencyValue(root, packageInfo, packageSource, platform) {
|
|
321
414
|
if (!packageSource || packageSource === "npm") {
|
|
322
415
|
return platform === "flutter" ? "^0.1.0" : "^0.1.0";
|
package/dist/tools/compliance.js
CHANGED
|
@@ -2,12 +2,13 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
|
|
5
|
+
import { applyBlockProvidedCompleteness, assessProjectCompleteness, assessProjectSelectedOptionalCapabilities, availableOptionalCapabilityIds, optionalCapabilityChecklist, platformCapabilityAvailability, selectedOptionalCapabilityIds, } from "../capabilities.js";
|
|
6
6
|
import { getOutcomeDefinition, hasAnswer, planContextFor, resolveOutcome, } from "../outcomes.js";
|
|
7
7
|
import { contractRuleCandidatesForPublicId, hasMultipleContractRuleCandidates, productExpectationBindingForSensor, productExpectationTitle, publicProductRuleId, } from "../productExpectations.js";
|
|
8
8
|
import { objectInput, optionalBooleanField, optionalStringField, stringField, textResult } from "../types.js";
|
|
9
9
|
import { packageVersion } from "../version.js";
|
|
10
10
|
import { DESIGN_CONTRACT_CONFIRMATION_ANSWER_ID, buildDesignBrief, designContractConfirmationFromAnswers, designPreviewPath, readDesignContract, } from "./design.js";
|
|
11
|
+
import { installedBlockProvidedCapabilities } from "./blocks.js";
|
|
11
12
|
import { inspectProject, validateSetup } from "./project.js";
|
|
12
13
|
import { readCreativeSelection } from "./creative.js";
|
|
13
14
|
import { assessUxHarness, buildExperienceReport, buildUxHarness, readUxHarness, } from "./uxHarness.js";
|
|
@@ -737,7 +738,17 @@ export async function checkCompliance(repoPath) {
|
|
|
737
738
|
// Completeness-gap: capabilities that are neither present nor validly opted-out require an explicit decision
|
|
738
739
|
// (build it, or place // vise: scope-omit <id> — <reason>). The scope-omit escape hatch keeps this
|
|
739
740
|
// FP-free because any capability can be excluded with a recorded reason. Failure to assess is silently ignored.
|
|
740
|
-
const
|
|
741
|
+
const sourceCompleteness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
|
|
742
|
+
// Installed Block Factory blocks deliver capabilities inside their packages,
|
|
743
|
+
// which the customer-source scan cannot see. Overlay block evidence for
|
|
744
|
+
// still-missing checklist items, but only from sidecar entries that pass
|
|
745
|
+
// registry-free local validation (manifest dependency declared + every
|
|
746
|
+
// filesTouched path intact — see installedBlockProvidedCapabilities). On
|
|
747
|
+
// local drift the evidence is withheld and the normal gap returns.
|
|
748
|
+
const blockProvided = sourceCompleteness && sourceCompleteness.missing.length > 0
|
|
749
|
+
? await installedBlockProvidedCapabilities(repoRoot).catch(() => [])
|
|
750
|
+
: [];
|
|
751
|
+
const completeness = sourceCompleteness ? applyBlockProvidedCompleteness(sourceCompleteness, blockProvided) : undefined;
|
|
741
752
|
const hasCompletenessGap = (completeness?.missing.length ?? 0) > 0;
|
|
742
753
|
const selectedOptionalCapabilities = (await assessProjectSelectedOptionalCapabilities(inspection.effectiveRoot, compliance.outcome, compliance.selected_optional_capabilities ?? []).catch(() => null)) ?? undefined;
|
|
743
754
|
const hasSelectedOptionalFailures = ((selectedOptionalCapabilities?.failed.length ?? 0) > 0) || ((selectedOptionalCapabilities?.unknown.length ?? 0) > 0);
|
|
@@ -1337,7 +1348,7 @@ function sourcePathCandidatesFromString(value) {
|
|
|
1337
1348
|
if (looksLikeSourcePath(trimmed)) {
|
|
1338
1349
|
candidates.add(trimmed);
|
|
1339
1350
|
}
|
|
1340
|
-
const pathPattern = /(?:[A-Za-z0-9_@.-]+\/)+[A-Za-z0-9_@.-]+\.(?:ts|
|
|
1351
|
+
const pathPattern = /(?:[A-Za-z0-9_@.-]+\/)+[A-Za-z0-9_@.-]+\.(?:tsx|ts|jsx|json|js|kts|kt|java|dart|swift|xml|gradle|ya?ml|env|plist|pbxproj|podspec)|\b(?:Podfile|Package\.swift|pubspec\.yaml|package\.json|AndroidManifest\.xml)\b/g;
|
|
1341
1352
|
for (const match of trimmed.matchAll(pathPattern)) {
|
|
1342
1353
|
candidates.add(match[0]);
|
|
1343
1354
|
}
|
|
@@ -1346,7 +1357,7 @@ function sourcePathCandidatesFromString(value) {
|
|
|
1346
1357
|
function looksLikeSourcePath(value) {
|
|
1347
1358
|
return (/[/\\]/.test(value) ||
|
|
1348
1359
|
/^(?:Podfile|Package\.swift|pubspec\.yaml|package\.json|AndroidManifest\.xml)$/.test(value) ||
|
|
1349
|
-
/\.(?:ts|
|
|
1360
|
+
/\.(?:tsx|ts|jsx|json|js|kts|kt|java|dart|swift|xml|gradle|ya?ml|env|plist|pbxproj|podspec)(?::\d+)?$/i.test(value));
|
|
1350
1361
|
}
|
|
1351
1362
|
function cleanEvidencePathCandidate(value) {
|
|
1352
1363
|
return value
|