@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +9 -14
  3. package/dist/capabilities.js +44 -0
  4. package/dist/server.js +7 -1
  5. package/dist/tools/ast.js +302 -25
  6. package/dist/tools/blocks.js +95 -2
  7. package/dist/tools/compliance.js +15 -4
  8. package/dist/tools/docs.js +48 -0
  9. package/dist/tools/harness.js +114 -1
  10. package/dist/tools/project.js +106 -11
  11. package/dist/tools/sdkFacts.js +128 -7
  12. package/dist/tools/sensors.js +30 -0
  13. package/docs-cache/README.md +34 -0
  14. package/docs-cache/social-plus-sdk/core-concepts/foundation/logging.mdx +236 -0
  15. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android.mdx +262 -0
  16. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter.mdx +195 -0
  17. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios.mdx +452 -0
  18. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview.mdx +133 -0
  19. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript.mdx +264 -0
  20. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device.mdx +191 -0
  21. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview.mdx +43 -0
  22. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup.mdx +360 -0
  23. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup.mdx +457 -0
  24. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup.mdx +423 -0
  25. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup.mdx +384 -0
  26. package/docs-cache/social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview.mdx +94 -0
  27. package/docs-cache/social-plus-sdk/getting-started/authentication.mdx +808 -0
  28. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start.mdx +304 -0
  29. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start.mdx +121 -0
  30. package/docs-cache/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start.mdx +225 -0
  31. package/docs-cache/social-plus-sdk/getting-started/platform-setup/web/web-quick-start.mdx +99 -0
  32. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/community-invitation.mdx +459 -0
  33. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/join-leave-community.mdx +449 -0
  34. package/docs-cache/social-plus-sdk/social/communities-spaces/organization/query-community-members.mdx +376 -0
  35. package/docs-cache/social-plus-sdk/social/content-management/posts/creation/text-post.mdx +318 -0
  36. package/docs-cache/social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status.mdx +399 -0
  37. package/docs-cache/social-plus-sdk/social/user-relationship/blocking/block-unblock-user.mdx +166 -0
  38. package/docs-cache/social-plus-sdk/social/user-relationship/following/get-follower-following-list.mdx +339 -0
  39. package/package.json +15 -3
  40. package/scripts/dart-model-extractor/bin/extract_models.dart +169 -0
  41. package/scripts/dart-model-extractor/pubspec.lock +149 -0
  42. package/scripts/dart-model-extractor/pubspec.yaml +16 -0
  43. package/scripts/extract-sdk-models.mjs +749 -0
  44. package/scripts/import-sdk-surface.mjs +52 -21
  45. package/sdk-surface/manifest.json +48 -2
  46. package/sdk-surface/models.android.json +990 -0
  47. package/sdk-surface/models.flutter.json +980 -0
  48. package/sdk-surface/models.ios.json +980 -0
  49. package/sdk-surface/models.typescript.json +1304 -0
  50. 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
- const expr = valArg.namedChild(0);
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
- * - Identifiers that reference a const/let/var with a string literal initializer
201
- * (single-step resolution only)
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
- // Identifier try to resolve to declaration in same file
211
- // TypeScript uses "identifier", Kotlin uses "simple_identifier"
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
- return resolveIdentifierToLiteral(name, tree.rootNode);
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
- if (objectNode.type !== "object")
356
+ const unwrapped = unwrapExpression(objectNode);
357
+ if (unwrapped.type !== "object")
224
358
  return undefined;
225
- for (let i = 0; i < objectNode.namedChildCount; i++) {
226
- const prop = objectNode.namedChild(i);
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
- return prop.childForFieldName("value") ?? undefined;
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
- function resolveIdentifierToLiteral(name, root) {
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
- const literal = extractStringLiteral(valueNode);
341
- if (literal !== undefined)
342
- result = literal;
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 + string_literal
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 (!varDecl)
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
- const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
351
- if (!idNode || idNode.text !== name)
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 strLit = node.namedChildren.find((c) => c.type === "string_literal");
354
- if (!strLit)
630
+ const swiftId = pattern.namedChildren.find((c) => c.type === "simple_identifier");
631
+ if (!swiftId || swiftId.text !== name)
355
632
  return;
356
- const literal = extractStringLiteral(strLit);
357
- if (literal !== undefined)
358
- result = literal;
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;
@@ -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
- const findings = [];
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: "2026-06-04.vise-blocks-sidecar.v1",
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";
@@ -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 completeness = (await assessProjectCompleteness(inspection.effectiveRoot, compliance.outcome).catch(() => null)) ?? undefined;
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|tsx|js|jsx|kt|java|dart|swift|xml|gradle|kts|json|ya?ml|env|plist|pbxproj|podspec)|\b(?:Podfile|Package\.swift|pubspec\.yaml|package\.json|AndroidManifest\.xml)\b/g;
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|tsx|js|jsx|kt|java|dart|swift|xml|gradle|kts|json|ya?ml|env|plist|pbxproj|podspec)(?::\d+)?$/i.test(value));
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