@amityco/social-plus-vise 1.0.1 → 1.1.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 +62 -25
- package/LICENSE +8 -6
- package/README.md +51 -18
- package/dist/capabilities.js +19 -62
- package/dist/intelligence/grounding.js +0 -23
- package/dist/intelligence/placement.js +0 -9
- package/dist/outcomes.js +44 -22
- package/dist/server.js +75 -38
- package/dist/tools/ast.js +3 -209
- package/dist/tools/blocks.js +6 -20
- package/dist/tools/compliance.js +168 -43
- package/dist/tools/creative.js +15 -41
- package/dist/tools/debug.js +0 -16
- package/dist/tools/design.js +18 -364
- package/dist/tools/docs.js +53 -24
- package/dist/tools/experienceCompiler.js +7 -10
- package/dist/tools/experienceSensors.js +1 -1
- package/dist/tools/harness.js +2 -27
- package/dist/tools/integration.js +6 -38
- package/dist/tools/learning.js +1 -1
- package/dist/tools/project.js +763 -546
- package/dist/tools/sdkFacts.js +2 -15
- package/dist/tools/sdkVersion.js +3 -36
- package/dist/tools/sensors.js +0 -6
- package/dist/tools/uxHarness.js +12 -9
- package/package.json +8 -97
- package/rules/chat.yaml +225 -0
- package/rules/event.yaml +45 -0
- package/rules/feed.yaml +24 -24
- package/rules/invitation.yaml +58 -0
- package/rules/live-data.yaml +104 -2
- package/rules/notification-tray.yaml +106 -0
- package/rules/poll.yaml +71 -0
- package/rules/sdk-lifecycle.yaml +112 -6
- package/rules/search.yaml +131 -0
- package/rules/story.yaml +221 -0
- package/rules/user-blocking.yaml +71 -0
- package/sdk-surface/flutter.json +1 -1
- package/sdk-surface/ios.json +1 -1
- package/sdk-surface/manifest.json +12 -12
- package/sdk-surface/models.flutter.json +96 -96
- package/sdk-surface/models.ios.json +1 -1
- package/sdk-surface/typescript.json +4 -4
- package/skills/social-plus-vise/SKILL.md +25 -5
- package/scripts/catalog-coverage-html.mjs +0 -325
- package/scripts/catalog-relationships-html.mjs +0 -686
- package/scripts/catalog-sheets.mjs +0 -286
- package/scripts/dart-model-extractor/bin/extract_models.dart +0 -169
- package/scripts/dart-model-extractor/pubspec.lock +0 -149
- package/scripts/dart-model-extractor/pubspec.yaml +0 -16
- package/scripts/extract-sdk-models.mjs +0 -749
- package/scripts/import-sdk-surface.mjs +0 -161
- package/scripts/pilot-feedback.mjs +0 -107
- package/scripts/workshop-board-html.mjs +0 -1018
- package/scripts/workshop-kit.mjs +0 -252
- package/skills/vise-harness-engineer/SKILL.md +0 -35
package/dist/tools/ast.js
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST-based deterministic analysis helpers using tree-sitter.
|
|
3
|
-
*
|
|
4
|
-
* This module provides syntactic (not type-resolving) analysis for source files.
|
|
5
|
-
* It is an additive layer alongside the existing regex-based validators.
|
|
6
|
-
*
|
|
7
|
-
* Policy: When AST and regex disagree (e.g., regex matches a comment), the regex
|
|
8
|
-
* result wins for now. AST cleanup of comment false-matches is Phase 4 work.
|
|
9
|
-
*
|
|
10
|
-
* Scope: Single-file, single-step identifier resolution only.
|
|
11
|
-
* No cross-file imports, no type inference, no function boundary traversal.
|
|
12
|
-
*/
|
|
13
1
|
import { createRequire } from "node:module";
|
|
14
2
|
const nodeRequire = createRequire(import.meta.url);
|
|
15
|
-
// Lazily and defensively load the tree-sitter native bindings. tree-sitter ships
|
|
16
|
-
// prebuilt binaries for common platforms (darwin, linux-x64, win32-x64); on others
|
|
17
|
-
// (linux-arm64, Alpine/musl, win32-arm64) the binding can fail to load when no C++
|
|
18
|
-
// toolchain is present. AST is an ADDITIVE layer over the regex validators, so a
|
|
19
|
-
// load failure must degrade to regex-only — NOT take down the entire CLI (including
|
|
20
|
-
// doc-search/compliance commands that never touch a parser) at import time. Static
|
|
21
|
-
// top-level imports would throw at module load and brick every command; this loader
|
|
22
|
-
// confines the failure to the AST path. `undefined` = not yet attempted; `null` =
|
|
23
|
-
// attempted and unavailable.
|
|
24
3
|
let nativeBindings;
|
|
25
4
|
function loadNativeBindings() {
|
|
26
5
|
if (nativeBindings !== undefined)
|
|
@@ -29,8 +8,6 @@ function loadNativeBindings() {
|
|
|
29
8
|
const ParserCtor = nodeRequire("tree-sitter");
|
|
30
9
|
const tsGrammars = nodeRequire("tree-sitter-typescript");
|
|
31
10
|
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
11
|
let swiftGrammar = null;
|
|
35
12
|
try {
|
|
36
13
|
swiftGrammar = nodeRequire("tree-sitter-swift");
|
|
@@ -51,28 +28,12 @@ function loadNativeBindings() {
|
|
|
51
28
|
}
|
|
52
29
|
return nativeBindings;
|
|
53
30
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Whether tree-sitter native bindings are available in this environment. When
|
|
56
|
-
* false, every AST helper degrades gracefully: parse() throws (so tryParse()
|
|
57
|
-
* returns null and stripComments() returns the source unchanged), and validators
|
|
58
|
-
* fall back to their regex paths.
|
|
59
|
-
*/
|
|
60
31
|
export function astAvailable() {
|
|
61
32
|
return loadNativeBindings() !== null;
|
|
62
33
|
}
|
|
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
34
|
export function swiftAstAvailable() {
|
|
69
35
|
return loadNativeBindings()?.swiftGrammar != null;
|
|
70
36
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Strip comments from source code using tree-sitter AST.
|
|
73
|
-
* Replaces comment spans with whitespace (preserving line structure).
|
|
74
|
-
* Returns original source unchanged if parsing fails.
|
|
75
|
-
*/
|
|
76
37
|
export function stripComments(language, source) {
|
|
77
38
|
try {
|
|
78
39
|
const tree = parse(language, source);
|
|
@@ -80,7 +41,6 @@ export function stripComments(language, source) {
|
|
|
80
41
|
collectComments(tree.rootNode, commentRanges);
|
|
81
42
|
if (commentRanges.length === 0)
|
|
82
43
|
return source;
|
|
83
|
-
// Replace comment ranges with spaces (preserve newlines for line numbers)
|
|
84
44
|
const chars = source.split("");
|
|
85
45
|
for (const { start, end } of commentRanges) {
|
|
86
46
|
for (let i = start; i < end && i < chars.length; i++) {
|
|
@@ -105,7 +65,6 @@ function collectComments(node, out) {
|
|
|
105
65
|
collectComments(child, out);
|
|
106
66
|
}
|
|
107
67
|
}
|
|
108
|
-
// Parser instances are reusable — one per language.
|
|
109
68
|
const parsers = new Map();
|
|
110
69
|
function getParser(language) {
|
|
111
70
|
let parser = parsers.get(language);
|
|
@@ -131,18 +90,7 @@ function getParser(language) {
|
|
|
131
90
|
}
|
|
132
91
|
return parser;
|
|
133
92
|
}
|
|
134
|
-
// node-tree-sitter's native parser rejects sufficiently large inputs with a
|
|
135
|
-
// native "Invalid argument" error (~32KB string limit). It IS catchable, but an
|
|
136
|
-
// unguarded caller would otherwise abort the whole validate run. We short-circuit
|
|
137
|
-
// before the native call so callers get a clean, documented, catchable error and
|
|
138
|
-
// can degrade to regex-only for that file. Real codebases routinely have files
|
|
139
|
-
// past this size (1000+ line activities/view controllers).
|
|
140
93
|
export const MAX_PARSE_BYTES = 30000;
|
|
141
|
-
/**
|
|
142
|
-
* Parse source content into a tree-sitter syntax tree.
|
|
143
|
-
* Throws on oversized input (see MAX_PARSE_BYTES) — callers that want graceful
|
|
144
|
-
* degradation should use tryParse or wrap this in try/catch.
|
|
145
|
-
*/
|
|
146
94
|
export function parse(language, source) {
|
|
147
95
|
if (Buffer.byteLength(source, "utf8") > MAX_PARSE_BYTES) {
|
|
148
96
|
throw new Error(`source exceeds tree-sitter parse limit (${Buffer.byteLength(source, "utf8")} bytes > ${MAX_PARSE_BYTES})`);
|
|
@@ -150,11 +98,6 @@ export function parse(language, source) {
|
|
|
150
98
|
const parser = getParser(language);
|
|
151
99
|
return parser.parse(source);
|
|
152
100
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Parse, returning null instead of throwing when the source can't be parsed
|
|
155
|
-
* (oversized input, native parser error). Lets validators skip AST analysis for
|
|
156
|
-
* one file and fall back to regex without aborting the run.
|
|
157
|
-
*/
|
|
158
101
|
export function tryParse(language, source) {
|
|
159
102
|
try {
|
|
160
103
|
return parse(language, source);
|
|
@@ -163,16 +106,11 @@ export function tryParse(language, source) {
|
|
|
163
106
|
return null;
|
|
164
107
|
}
|
|
165
108
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Find all call expressions in the tree whose callee matches a pattern.
|
|
168
|
-
* Returns normalised callee strings and argument nodes.
|
|
169
|
-
*/
|
|
170
109
|
export function findCallExpressions(tree, calleePattern) {
|
|
171
110
|
const results = [];
|
|
172
111
|
walkTree(tree.rootNode, (node) => {
|
|
173
112
|
if (node.type !== "call_expression")
|
|
174
113
|
return;
|
|
175
|
-
// TypeScript: uses field names "function" and "arguments"
|
|
176
114
|
const calleeNode = node.childForFieldName("function");
|
|
177
115
|
if (calleeNode) {
|
|
178
116
|
const callee = normaliseCallee(calleeNode);
|
|
@@ -190,10 +128,6 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
190
128
|
results.push({ callee, node, args });
|
|
191
129
|
return;
|
|
192
130
|
}
|
|
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.)
|
|
197
131
|
const navNode = node.namedChild(0);
|
|
198
132
|
const suffixNode = node.namedChild(1);
|
|
199
133
|
if (!navNode || !suffixNode || suffixNode.type !== "call_suffix")
|
|
@@ -201,16 +135,12 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
201
135
|
const callee = normaliseKotlinCallee(navNode);
|
|
202
136
|
if (!callee || !calleePattern.test(callee))
|
|
203
137
|
return;
|
|
204
|
-
// Extract args from call_suffix → value_arguments → value_argument nodes
|
|
205
138
|
const args = [];
|
|
206
139
|
const valArgsNode = suffixNode.namedChild(0);
|
|
207
140
|
if (valArgsNode && valArgsNode.type === "value_arguments") {
|
|
208
141
|
for (let i = 0; i < valArgsNode.namedChildCount; i++) {
|
|
209
142
|
const valArg = valArgsNode.namedChild(i);
|
|
210
143
|
if (valArg && valArg.type === "value_argument") {
|
|
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
144
|
const first = valArg.namedChild(0);
|
|
215
145
|
const expr = first?.type === "value_argument_label" ? valArg.namedChild(valArg.namedChildCount - 1) : first;
|
|
216
146
|
if (expr)
|
|
@@ -222,56 +152,25 @@ export function findCallExpressions(tree, calleePattern) {
|
|
|
222
152
|
});
|
|
223
153
|
return results;
|
|
224
154
|
}
|
|
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
155
|
const MAX_RESOLUTION_HOPS = 5;
|
|
230
|
-
/**
|
|
231
|
-
* Resolve an AST node to its literal string value within the same file.
|
|
232
|
-
*
|
|
233
|
-
* Handles:
|
|
234
|
-
* - String literals directly → returns the string value
|
|
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.
|
|
247
|
-
*
|
|
248
|
-
* Returns undefined if the value cannot be statically resolved.
|
|
249
|
-
*/
|
|
250
156
|
export function resolveLiteralValue(node, tree) {
|
|
251
157
|
return resolveLiteralValueBounded(node, tree, MAX_RESOLUTION_HOPS, new Set());
|
|
252
158
|
}
|
|
253
159
|
function resolveLiteralValueBounded(node, tree, hopsLeft, visited) {
|
|
254
|
-
// Direct string literal
|
|
255
160
|
const directValue = extractStringLiteral(node);
|
|
256
161
|
if (directValue !== undefined)
|
|
257
162
|
return directValue;
|
|
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
163
|
if (node.type === "template_string") {
|
|
261
164
|
const passthrough = templatePassthroughSubstitution(node);
|
|
262
165
|
if (passthrough)
|
|
263
166
|
return resolveLiteralValueBounded(passthrough, tree, hopsLeft, visited);
|
|
264
167
|
}
|
|
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".
|
|
269
168
|
if (node.type === "identifier" || node.type === "simple_identifier") {
|
|
270
169
|
if (hopsLeft <= 0)
|
|
271
170
|
return undefined;
|
|
272
171
|
const name = node.text;
|
|
273
172
|
if (visited.has(name))
|
|
274
|
-
return undefined;
|
|
173
|
+
return undefined;
|
|
275
174
|
visited.add(name);
|
|
276
175
|
const valueNode = resolveIdentifierToValueNode(name, tree.rootNode);
|
|
277
176
|
if (!valueNode)
|
|
@@ -280,12 +179,6 @@ function resolveLiteralValueBounded(node, tree, hopsLeft, visited) {
|
|
|
280
179
|
}
|
|
281
180
|
return undefined;
|
|
282
181
|
}
|
|
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
182
|
function templatePassthroughSubstitution(node) {
|
|
290
183
|
if (node.type !== "template_string")
|
|
291
184
|
return undefined;
|
|
@@ -296,35 +189,17 @@ function templatePassthroughSubstitution(node) {
|
|
|
296
189
|
continue;
|
|
297
190
|
if (child.type === "template_substitution") {
|
|
298
191
|
if (substitution)
|
|
299
|
-
return undefined;
|
|
192
|
+
return undefined;
|
|
300
193
|
substitution = child;
|
|
301
194
|
}
|
|
302
195
|
else {
|
|
303
|
-
// Any other named child (e.g. string_fragment cooked text) → has literal
|
|
304
|
-
// content, so not a pure passthrough.
|
|
305
196
|
return undefined;
|
|
306
197
|
}
|
|
307
198
|
}
|
|
308
199
|
if (!substitution)
|
|
309
200
|
return undefined;
|
|
310
|
-
// The substitution wraps a single expression: `${ expr }`.
|
|
311
201
|
return substitution.namedChild(0) ?? undefined;
|
|
312
202
|
}
|
|
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
203
|
export function unwrapExpression(node) {
|
|
329
204
|
let current = node;
|
|
330
205
|
const wrappers = new Set([
|
|
@@ -334,8 +209,6 @@ export function unwrapExpression(node) {
|
|
|
334
209
|
"non_null_expression",
|
|
335
210
|
"type_assertion",
|
|
336
211
|
]);
|
|
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
212
|
for (let i = 0; i < 16 && wrappers.has(current.type); i++) {
|
|
340
213
|
const inner = current.namedChild(0);
|
|
341
214
|
if (!inner)
|
|
@@ -344,14 +217,6 @@ export function unwrapExpression(node) {
|
|
|
344
217
|
}
|
|
345
218
|
return current;
|
|
346
219
|
}
|
|
347
|
-
/**
|
|
348
|
-
* Pick a specific named property from an object argument node.
|
|
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.
|
|
354
|
-
*/
|
|
355
220
|
export function pickObjectProperty(objectNode, propertyName) {
|
|
356
221
|
const unwrapped = unwrapExpression(objectNode);
|
|
357
222
|
if (unwrapped.type !== "object")
|
|
@@ -368,14 +233,6 @@ export function pickObjectProperty(objectNode, propertyName) {
|
|
|
368
233
|
}
|
|
369
234
|
return undefined;
|
|
370
235
|
}
|
|
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
236
|
export function pickLabeledArgument(callNode, label) {
|
|
380
237
|
for (let i = 0; i < callNode.namedChildCount; i++) {
|
|
381
238
|
const suffix = callNode.namedChild(i);
|
|
@@ -392,24 +249,11 @@ export function pickLabeledArgument(callNode, label) {
|
|
|
392
249
|
if (first?.type !== "value_argument_label" || first.text !== label)
|
|
393
250
|
continue;
|
|
394
251
|
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
252
|
return value && value !== first ? value : undefined;
|
|
398
253
|
}
|
|
399
254
|
}
|
|
400
255
|
return undefined;
|
|
401
256
|
}
|
|
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
257
|
export function findSwiftFunctionLocalDeclarations(tree, initializerPattern) {
|
|
414
258
|
const results = [];
|
|
415
259
|
walkTree(tree.rootNode, (node) => {
|
|
@@ -441,21 +285,16 @@ function hasFunctionAncestor(node) {
|
|
|
441
285
|
}
|
|
442
286
|
return false;
|
|
443
287
|
}
|
|
444
|
-
/** The initializer expression of a Swift property_declaration (the value after `=`), if any. */
|
|
445
288
|
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
289
|
const last = node.namedChild(node.namedChildCount - 1);
|
|
449
290
|
if (!last)
|
|
450
291
|
return undefined;
|
|
451
292
|
if (["value_binding_pattern", "pattern", "type_annotation", "modifiers", "attribute"].includes(last.type))
|
|
452
293
|
return undefined;
|
|
453
|
-
// computed_property is a body, not an initializer.
|
|
454
294
|
if (last.type === "computed_property")
|
|
455
295
|
return undefined;
|
|
456
296
|
return last;
|
|
457
297
|
}
|
|
458
|
-
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
459
298
|
function walkTree(node, visit) {
|
|
460
299
|
visit(node);
|
|
461
300
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -481,8 +320,6 @@ function normaliseKotlinCallee(node) {
|
|
|
481
320
|
if (node.type === "simple_identifier")
|
|
482
321
|
return node.text;
|
|
483
322
|
if (node.type === "navigation_expression") {
|
|
484
|
-
// navigation_expression has children: expression + navigation_suffix
|
|
485
|
-
// e.g., AmityCoreClient.login → nav_expr(simple_id("AmityCoreClient"), nav_suffix(".login"))
|
|
486
323
|
const parts = [];
|
|
487
324
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
488
325
|
const child = node.namedChild(i);
|
|
@@ -492,13 +329,11 @@ function normaliseKotlinCallee(node) {
|
|
|
492
329
|
parts.push(child.text);
|
|
493
330
|
}
|
|
494
331
|
else if (child.type === "navigation_suffix") {
|
|
495
|
-
// navigation_suffix contains a simple_identifier after the dot
|
|
496
332
|
const id = child.namedChild(0);
|
|
497
333
|
if (id)
|
|
498
334
|
parts.push(id.text);
|
|
499
335
|
}
|
|
500
336
|
else if (child.type === "call_expression") {
|
|
501
|
-
// Chained: obj.method1().method2 — extract the last part from the chain
|
|
502
337
|
const innerCallee = normaliseKotlinCallee(child.namedChild(0));
|
|
503
338
|
if (innerCallee)
|
|
504
339
|
parts.push(innerCallee);
|
|
@@ -515,48 +350,38 @@ function normaliseKotlinCallee(node) {
|
|
|
515
350
|
}
|
|
516
351
|
function extractStringLiteral(node) {
|
|
517
352
|
if (node.type === "string") {
|
|
518
|
-
// tree-sitter-typescript: string nodes include the quotes — strip them
|
|
519
353
|
const text = node.text;
|
|
520
354
|
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
|
521
355
|
return text.slice(1, -1);
|
|
522
356
|
}
|
|
523
|
-
// Template literal with no interpolations
|
|
524
357
|
if (text.startsWith("`") && text.endsWith("`") && !text.includes("${")) {
|
|
525
358
|
return text.slice(1, -1);
|
|
526
359
|
}
|
|
527
360
|
}
|
|
528
|
-
// template_string without substitutions
|
|
529
361
|
if (node.type === "template_string" && node.namedChildCount === 0) {
|
|
530
362
|
const text = node.text;
|
|
531
363
|
if (text.startsWith("`") && text.endsWith("`")) {
|
|
532
364
|
return text.slice(1, -1);
|
|
533
365
|
}
|
|
534
366
|
}
|
|
535
|
-
// Kotlin: string_literal contains string_content child
|
|
536
367
|
if (node.type === "string_literal") {
|
|
537
368
|
const contentNode = node.namedChild(0);
|
|
538
369
|
if (contentNode && contentNode.type === "string_content") {
|
|
539
370
|
return contentNode.text;
|
|
540
371
|
}
|
|
541
|
-
// Simple case — strip quotes from text
|
|
542
372
|
const text = node.text;
|
|
543
373
|
if (text.startsWith('"') && text.endsWith('"')) {
|
|
544
374
|
return text.slice(1, -1);
|
|
545
375
|
}
|
|
546
376
|
}
|
|
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
377
|
if (node.type === "line_string_literal") {
|
|
551
378
|
if (node.namedChildCount === 0)
|
|
552
|
-
return "";
|
|
379
|
+
return "";
|
|
553
380
|
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "line_str_text") {
|
|
554
381
|
return node.namedChild(0).text;
|
|
555
382
|
}
|
|
556
383
|
return undefined;
|
|
557
384
|
}
|
|
558
|
-
// Swift: multi-line """…""" literal. Swift semantics strip the newline after the
|
|
559
|
-
// opening and before the closing delimiter.
|
|
560
385
|
if (node.type === "multi_line_string_literal") {
|
|
561
386
|
if (node.namedChildCount === 1 && node.namedChild(0)?.type === "multi_line_str_text") {
|
|
562
387
|
return node.namedChild(0).text.replace(/^\n/, "").replace(/\n[ \t]*$/, "");
|
|
@@ -565,31 +390,11 @@ function extractStringLiteral(node) {
|
|
|
565
390
|
}
|
|
566
391
|
return undefined;
|
|
567
392
|
}
|
|
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
393
|
function resolveIdentifierToValueNode(name, root) {
|
|
588
394
|
let result;
|
|
589
395
|
walkTree(root, (node) => {
|
|
590
396
|
if (result !== undefined)
|
|
591
397
|
return;
|
|
592
|
-
// TypeScript/JS: variable_declarator with name + value fields
|
|
593
398
|
if (node.type === "variable_declarator") {
|
|
594
399
|
const nameNode = node.childForFieldName("name");
|
|
595
400
|
if (!nameNode || nameNode.text !== name)
|
|
@@ -597,33 +402,22 @@ function resolveIdentifierToValueNode(name, root) {
|
|
|
597
402
|
const valueNode = node.childForFieldName("value");
|
|
598
403
|
if (!valueNode)
|
|
599
404
|
return;
|
|
600
|
-
// Casts/grouping around the initializer are transparent (e.g.
|
|
601
|
-
// `const T = RAW as string;`).
|
|
602
405
|
result = unwrapExpression(valueNode);
|
|
603
406
|
return;
|
|
604
407
|
}
|
|
605
|
-
// Kotlin: property_declaration with variable_declaration + initializer
|
|
606
408
|
if (node.type === "property_declaration") {
|
|
607
409
|
const varDecl = node.namedChildren.find((c) => c.type === "variable_declaration");
|
|
608
410
|
if (varDecl) {
|
|
609
411
|
const idNode = varDecl.namedChildren.find((c) => c.type === "simple_identifier");
|
|
610
412
|
if (!idNode || idNode.text !== name)
|
|
611
413
|
return;
|
|
612
|
-
// The initializer is the named child after the variable_declaration.
|
|
613
414
|
const declIdx = node.namedChildren.indexOf(varDecl);
|
|
614
415
|
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
416
|
if (initializer && (initializer.type === "string_literal" || initializer.type === "simple_identifier")) {
|
|
619
417
|
result = initializer;
|
|
620
418
|
}
|
|
621
419
|
return;
|
|
622
420
|
}
|
|
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
421
|
const pattern = node.namedChildren.find((c) => c.type === "pattern");
|
|
628
422
|
if (!pattern)
|
|
629
423
|
return;
|
package/dist/tools/blocks.js
CHANGED
|
@@ -14,7 +14,7 @@ const registryPlatformByVisePlatform = {
|
|
|
14
14
|
export async function listRegistryBlocks(registryPath) {
|
|
15
15
|
const registry = await loadRegistry(registryPath);
|
|
16
16
|
return {
|
|
17
|
-
source: "social-plus-
|
|
17
|
+
source: "social-plus-blocks",
|
|
18
18
|
mode: "block-registry",
|
|
19
19
|
schemaVersion: registry.schemaVersion,
|
|
20
20
|
blocks: registry.blocks.map((block) => ({
|
|
@@ -53,6 +53,7 @@ export async function addBlockInstall(options) {
|
|
|
53
53
|
...plan,
|
|
54
54
|
applied: true,
|
|
55
55
|
filesTouched,
|
|
56
|
+
nextStep: "Run `vise blocks validate <repoPath> --block <id> --registry <path>` to re-check for install drift after further edits.",
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
export async function validateBlockInstall(options) {
|
|
@@ -68,14 +69,14 @@ export async function validateBlockInstall(options) {
|
|
|
68
69
|
ruleId: "blocks.registry.known",
|
|
69
70
|
severity: "warning",
|
|
70
71
|
message: `Installed sidecar entry references unknown registry block ${entry.blockId}.`,
|
|
71
|
-
recommendation: "Update the
|
|
72
|
+
recommendation: "Update the block registry or remove stale block sidecar state.",
|
|
72
73
|
}));
|
|
73
74
|
if (installed.length === 0) {
|
|
74
75
|
findings.push({
|
|
75
76
|
ruleId: "blocks.sidecar.installed",
|
|
76
77
|
severity: "warning",
|
|
77
78
|
message: "No installed block sidecar entries exist.",
|
|
78
|
-
recommendation: "Run `vise blocks add <repoPath> --block <id> --apply` after reviewing the dry-run plan.",
|
|
79
|
+
recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
return {
|
|
@@ -91,14 +92,13 @@ export async function validateBlockInstall(options) {
|
|
|
91
92
|
const repoPath = requiredRepoPath(options);
|
|
92
93
|
const sidecar = await readSidecar(repoPath);
|
|
93
94
|
const installed = (sidecar?.installed ?? []).filter((entry) => !options.blockId || entry.blockId === options.blockId);
|
|
94
|
-
// Seed with plan findings so the providesCapabilities seam guard also surfaces in validate mode.
|
|
95
95
|
const findings = [...plan.findings];
|
|
96
96
|
if (installed.length === 0) {
|
|
97
97
|
findings.push({
|
|
98
98
|
ruleId: "blocks.sidecar.installed",
|
|
99
99
|
severity: "warning",
|
|
100
100
|
message: options.blockId ? `No sidecar entry exists for block ${options.blockId}.` : "No installed block sidecar entries exist.",
|
|
101
|
-
recommendation: "Run `vise blocks add <repoPath> --block <id> --apply` after reviewing the dry-run plan.",
|
|
101
|
+
recommendation: "Run `vise blocks add <repoPath> --block <id> --registry <path> --apply` after reviewing the dry-run plan.",
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
if (plan.packageChange.alreadyPresent === false) {
|
|
@@ -158,9 +158,6 @@ async function buildInstallPlan(options, mode) {
|
|
|
158
158
|
stopConditions.push(`Missing safe install anchor ${targetFile.anchor} in ${targetFile.path}.`);
|
|
159
159
|
}
|
|
160
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
161
|
const providesCapabilities = providesCapabilitiesFor(block);
|
|
165
162
|
const knownCapabilityIds = completenessCapabilityIds();
|
|
166
163
|
const unknownCapabilityIds = providesCapabilities.filter((id) => !knownCapabilityIds.has(id));
|
|
@@ -168,7 +165,7 @@ async function buildInstallPlan(options, mode) {
|
|
|
168
165
|
ruleId: "blocks.providesCapabilities.known",
|
|
169
166
|
severity: "warning",
|
|
170
167
|
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
|
|
168
|
+
recommendation: "Unknown ids never satisfy completeness gaps. Align the block contract with Vise's capability catalog (vise owns the id space) or upgrade Vise.",
|
|
172
169
|
}));
|
|
173
170
|
return {
|
|
174
171
|
status: stopConditions.length > 0 ? "needs-review" : "ready",
|
|
@@ -344,16 +341,6 @@ async function readSidecar(repoPath) {
|
|
|
344
341
|
return null;
|
|
345
342
|
}
|
|
346
343
|
}
|
|
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
344
|
export async function installedBlockProvidedCapabilities(repoPath) {
|
|
358
345
|
const repoRoot = path.resolve(repoPath);
|
|
359
346
|
const sidecar = await readSidecar(repoRoot);
|
|
@@ -394,7 +381,6 @@ async function installedEntryLocallyValid(repoRoot, entry) {
|
|
|
394
381
|
async function blockDependencyDeclared(repoRoot, entry) {
|
|
395
382
|
const dependencyName = entry.dependencyName;
|
|
396
383
|
if (!dependencyName) {
|
|
397
|
-
// Pre-2026-06-10 sidecars carry no package evidence; fail closed so the gap stays.
|
|
398
384
|
return false;
|
|
399
385
|
}
|
|
400
386
|
if (entry.platform === "flutter") {
|