sceneview-mcp 4.0.14 → 4.0.15
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/README.md +5 -5
- package/dist/generated/llms-txt.js +1 -1
- package/dist/generated/symbols.js +8622 -0
- package/dist/generated/version.js +2 -2
- package/dist/platform-setup.js +6 -14
- package/dist/symbols.js +204 -0
- package/dist/tools/definitions.js +1 -1
- package/dist/tools/handler.js +1 -1
- package/dist/validator.js +199 -0
- package/package.json +4 -4
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
// build time so the MCP server, telemetry, and the install-snippet
|
|
5
5
|
// generators all report the actually-published versions instead of
|
|
6
6
|
// stale hardcoded constants. See #941.
|
|
7
|
-
export const PACKAGE_VERSION = "4.0.
|
|
8
|
-
export const LATEST_SCENEVIEW_RELEASE = "4.
|
|
7
|
+
export const PACKAGE_VERSION = "4.0.15";
|
|
8
|
+
export const LATEST_SCENEVIEW_RELEASE = "4.25.0";
|
package/dist/platform-setup.js
CHANGED
|
@@ -414,18 +414,14 @@ SceneView Flutter uses **PlatformView** to embed native SceneView (Android: Fila
|
|
|
414
414
|
|
|
415
415
|
### 1. Dependencies
|
|
416
416
|
|
|
417
|
-
> **Note:** the
|
|
418
|
-
> (
|
|
419
|
-
>
|
|
417
|
+
> **Note:** the plugin is published on pub.dev as \`flutter_sceneview\`
|
|
418
|
+
> (the pub.dev names \`sceneview\` and \`sceneview_flutter\` are unrelated
|
|
419
|
+
> third-party uploads — do not use them).
|
|
420
420
|
|
|
421
421
|
\`\`\`yaml
|
|
422
422
|
# pubspec.yaml
|
|
423
423
|
dependencies:
|
|
424
|
-
|
|
425
|
-
git:
|
|
426
|
-
url: https://github.com/sceneview/sceneview
|
|
427
|
-
path: flutter/sceneview_flutter
|
|
428
|
-
ref: v${LATEST_SCENEVIEW_RELEASE}
|
|
424
|
+
flutter_sceneview: ^${LATEST_SCENEVIEW_RELEASE}
|
|
429
425
|
\`\`\`
|
|
430
426
|
|
|
431
427
|
### 2. Android Setup
|
|
@@ -483,13 +479,9 @@ const FLUTTER_AR = `## SceneView Flutter — AR Setup
|
|
|
483
479
|
### 1. Dependencies
|
|
484
480
|
|
|
485
481
|
\`\`\`yaml
|
|
486
|
-
#
|
|
482
|
+
# pubspec.yaml — published on pub.dev (the names sceneview / sceneview_flutter are unrelated third-party uploads)
|
|
487
483
|
dependencies:
|
|
488
|
-
|
|
489
|
-
git:
|
|
490
|
-
url: https://github.com/sceneview/sceneview
|
|
491
|
-
path: flutter/sceneview_flutter
|
|
492
|
-
ref: v${LATEST_SCENEVIEW_RELEASE}
|
|
484
|
+
flutter_sceneview: ^${LATEST_SCENEVIEW_RELEASE}
|
|
493
485
|
\`\`\`
|
|
494
486
|
|
|
495
487
|
### 2. Android Manifest
|
package/dist/symbols.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol-existence lookups over the generated public-API index (#2760).
|
|
3
|
+
*
|
|
4
|
+
* The index (`src/generated/symbols.ts`) is parsed from the committed
|
|
5
|
+
* binary-compatibility `.api` dumps at build time — see
|
|
6
|
+
* `scripts/generate-symbols.js` for the full provenance chain. Everything
|
|
7
|
+
* here answers one question for the validator: "does this identifier exist
|
|
8
|
+
* in the real Android/KMP SceneView API?", with edit-distance suggestions
|
|
9
|
+
* when it doesn't.
|
|
10
|
+
*
|
|
11
|
+
* Scope, stated honestly: the dumps cover `sceneview`, `arsceneview` and
|
|
12
|
+
* `sceneview-core` (Android/KMP). SceneViewSwift and sceneview-web have no
|
|
13
|
+
* committed dump, so Swift/Web snippets are NOT symbol-checked.
|
|
14
|
+
*/
|
|
15
|
+
import { SYMBOLS } from "./generated/symbols.js";
|
|
16
|
+
export const SCENEVIEW_NAMESPACE = "io.github.sceneview";
|
|
17
|
+
// ─── Lazily-built lookup sets ────────────────────────────────────────────────
|
|
18
|
+
let typeNamesCache = null;
|
|
19
|
+
let classBySimpleNameCache = null;
|
|
20
|
+
/**
|
|
21
|
+
* Every PascalCase identifier a Kotlin user can legitimately reference as a
|
|
22
|
+
* type or declarative factory:
|
|
23
|
+
* - class simple names (`ModelNode`, `WallPlacement`, …),
|
|
24
|
+
* - PascalCase top-level functions — the composables (`SceneView`,
|
|
25
|
+
* `ARSceneView`, `WallPlacementScene`, `DynamicSkyNode`, …),
|
|
26
|
+
* - PascalCase members of the `*Scope` classes — the declarative node
|
|
27
|
+
* factories (`SceneScope.ModelNode`, `ARSceneScope.…`).
|
|
28
|
+
*/
|
|
29
|
+
export function knownTypeNames() {
|
|
30
|
+
if (typeNamesCache)
|
|
31
|
+
return typeNamesCache;
|
|
32
|
+
const names = new Set();
|
|
33
|
+
for (const fqcn of Object.keys(SYMBOLS.classes)) {
|
|
34
|
+
const simple = fqcn.slice(fqcn.lastIndexOf(".") + 1);
|
|
35
|
+
if (/^[A-Z]/.test(simple))
|
|
36
|
+
names.add(simple);
|
|
37
|
+
}
|
|
38
|
+
for (const name of Object.keys(SYMBOLS.topLevel)) {
|
|
39
|
+
if (/^[A-Z]/.test(name))
|
|
40
|
+
names.add(name);
|
|
41
|
+
}
|
|
42
|
+
for (const [fqcn, entry] of Object.entries(SYMBOLS.classes)) {
|
|
43
|
+
if (!fqcn.endsWith("Scope"))
|
|
44
|
+
continue;
|
|
45
|
+
for (const member of entry.members) {
|
|
46
|
+
if (/^[A-Z]/.test(member))
|
|
47
|
+
names.add(member);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
typeNamesCache = names;
|
|
51
|
+
return names;
|
|
52
|
+
}
|
|
53
|
+
/** Class simple name → the FQCNs that declare it (usually exactly one). */
|
|
54
|
+
function classBySimpleName() {
|
|
55
|
+
if (classBySimpleNameCache)
|
|
56
|
+
return classBySimpleNameCache;
|
|
57
|
+
const map = new Map();
|
|
58
|
+
for (const fqcn of Object.keys(SYMBOLS.classes)) {
|
|
59
|
+
const simple = fqcn.slice(fqcn.lastIndexOf(".") + 1);
|
|
60
|
+
const list = map.get(simple);
|
|
61
|
+
if (list)
|
|
62
|
+
list.push(fqcn);
|
|
63
|
+
else
|
|
64
|
+
map.set(simple, [fqcn]);
|
|
65
|
+
}
|
|
66
|
+
classBySimpleNameCache = map;
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
export function isKnownTypeName(name) {
|
|
70
|
+
return knownTypeNames().has(name);
|
|
71
|
+
}
|
|
72
|
+
/** Top-level function (any case) — composables and extension helpers. */
|
|
73
|
+
export function isKnownTopLevelFunction(name) {
|
|
74
|
+
return name in SYMBOLS.topLevel;
|
|
75
|
+
}
|
|
76
|
+
/** Every real `remember*` composable helper — suggestion pool for typos. */
|
|
77
|
+
export function rememberHelperNames() {
|
|
78
|
+
return Object.keys(SYMBOLS.topLevel).filter((name) => name.startsWith("remember"));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve an `io.github.sceneview.*` import target.
|
|
82
|
+
*
|
|
83
|
+
* Returns "class" for an exact class FQCN, "package" for a package that
|
|
84
|
+
* exists (covers `import io.github.sceneview.node.*`), "member" for a
|
|
85
|
+
* `Class.member` or package-level function import, and null when nothing
|
|
86
|
+
* in the index matches. Callers must only pass SceneView-namespace paths —
|
|
87
|
+
* other namespaces are out of index scope.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveImport(path) {
|
|
90
|
+
if (path in SYMBOLS.classes)
|
|
91
|
+
return "class";
|
|
92
|
+
if (SYMBOLS.packages.includes(path))
|
|
93
|
+
return "package";
|
|
94
|
+
const lastDot = path.lastIndexOf(".");
|
|
95
|
+
if (lastDot === -1)
|
|
96
|
+
return null;
|
|
97
|
+
const parent = path.slice(0, lastDot);
|
|
98
|
+
const leaf = path.slice(lastDot + 1);
|
|
99
|
+
// `import io.github.sceneview.ar.rememberARCameraStream` — a top-level
|
|
100
|
+
// function imported from a package.
|
|
101
|
+
if (SYMBOLS.packages.includes(parent) && leaf in SYMBOLS.topLevel) {
|
|
102
|
+
return "member";
|
|
103
|
+
}
|
|
104
|
+
// A nested class member — accept when the parent class exists and
|
|
105
|
+
// declares it.
|
|
106
|
+
const parentClass = SYMBOLS.classes[parent];
|
|
107
|
+
if (parentClass?.members.includes(leaf))
|
|
108
|
+
return "member";
|
|
109
|
+
// `import io.github.sceneview.node.ModelNode.Companion` — the generator
|
|
110
|
+
// folds companion members into the host class and drops the `.Companion`
|
|
111
|
+
// entry, so resolve the companion itself through its host.
|
|
112
|
+
if (leaf === "Companion" && parent in SYMBOLS.classes)
|
|
113
|
+
return "member";
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Union of member/property names declared by classes with this simple name. */
|
|
117
|
+
export function membersOfClass(simpleName) {
|
|
118
|
+
const fqcns = classBySimpleName().get(simpleName);
|
|
119
|
+
if (!fqcns)
|
|
120
|
+
return null;
|
|
121
|
+
const members = new Set();
|
|
122
|
+
for (const fqcn of fqcns) {
|
|
123
|
+
for (const m of SYMBOLS.classes[fqcn].members)
|
|
124
|
+
members.add(m);
|
|
125
|
+
}
|
|
126
|
+
return members;
|
|
127
|
+
}
|
|
128
|
+
// ─── Edit-distance suggestions ───────────────────────────────────────────────
|
|
129
|
+
/** Classic Levenshtein with a band cutoff — returns Infinity past `max`. */
|
|
130
|
+
export function editDistance(a, b, max) {
|
|
131
|
+
if (Math.abs(a.length - b.length) > max)
|
|
132
|
+
return Number.POSITIVE_INFINITY;
|
|
133
|
+
const la = a.length;
|
|
134
|
+
const lb = b.length;
|
|
135
|
+
let prev = Array.from({ length: lb + 1 }, (_, i) => i);
|
|
136
|
+
let curr = new Array(lb + 1);
|
|
137
|
+
for (let i = 1; i <= la; i++) {
|
|
138
|
+
curr[0] = i;
|
|
139
|
+
let rowMin = i;
|
|
140
|
+
for (let j = 1; j <= lb; j++) {
|
|
141
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
142
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
143
|
+
if (curr[j] < rowMin)
|
|
144
|
+
rowMin = curr[j];
|
|
145
|
+
}
|
|
146
|
+
if (rowMin > max)
|
|
147
|
+
return Number.POSITIVE_INFINITY;
|
|
148
|
+
[prev, curr] = [curr, prev];
|
|
149
|
+
}
|
|
150
|
+
return prev[lb] <= max ? prev[lb] : Number.POSITIVE_INFINITY;
|
|
151
|
+
}
|
|
152
|
+
/** camelCase identifier → lowercased token set (`loadModelAsync` → load,model,async). */
|
|
153
|
+
function camelTokens(name) {
|
|
154
|
+
return new Set(name
|
|
155
|
+
.split(/(?=[A-Z])/)
|
|
156
|
+
.map((t) => t.toLowerCase())
|
|
157
|
+
.filter((t) => t.length > 0));
|
|
158
|
+
}
|
|
159
|
+
/** Jaccard similarity of the camelCase token sets of two identifiers. */
|
|
160
|
+
export function tokenSimilarity(a, b) {
|
|
161
|
+
const ta = camelTokens(a);
|
|
162
|
+
const tb = camelTokens(b);
|
|
163
|
+
let inter = 0;
|
|
164
|
+
for (const t of ta)
|
|
165
|
+
if (tb.has(t))
|
|
166
|
+
inter++;
|
|
167
|
+
const union = ta.size + tb.size - inter;
|
|
168
|
+
return union === 0 ? 0 : inter / union;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Closest candidates to `name`, nearest first, by a hybrid metric:
|
|
172
|
+
* - Levenshtein within a length-scaled band (2 for short names, 3 for long)
|
|
173
|
+
* catches character typos (`ModellNode` → `ModelNode`);
|
|
174
|
+
* - camelCase token overlap (Jaccard ≥ 0.5) catches structural
|
|
175
|
+
* hallucinations that raw edit distance can't reach —
|
|
176
|
+
* `createModelInstanceAsync` → `loadModelInstanceAsync` is distance 5,
|
|
177
|
+
* but shares 3 of 5 camelCase tokens.
|
|
178
|
+
* Token matches are scored `10 × (1 − similarity)` so an exact-ish edit
|
|
179
|
+
* match always outranks a merely structural one.
|
|
180
|
+
*/
|
|
181
|
+
export function suggestClosest(name, candidates, limit = 3) {
|
|
182
|
+
const max = name.length < 12 ? 2 : 3;
|
|
183
|
+
const lower = name.toLowerCase();
|
|
184
|
+
const scored = [];
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
const d = editDistance(lower, candidate.toLowerCase(), max);
|
|
187
|
+
if (d !== Number.POSITIVE_INFINITY) {
|
|
188
|
+
scored.push([candidate, d]);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const similarity = tokenSimilarity(name, candidate);
|
|
192
|
+
if (similarity >= 0.5)
|
|
193
|
+
scored.push([candidate, 10 * (1 - similarity)]);
|
|
194
|
+
}
|
|
195
|
+
scored.sort((x, y) => x[1] - y[1] || x[0].localeCompare(y[0]));
|
|
196
|
+
return scored.slice(0, limit).map(([candidate]) => candidate);
|
|
197
|
+
}
|
|
198
|
+
/** Human-readable " Did you mean …?" suffix, or "" when nothing is close. */
|
|
199
|
+
export function didYouMean(name, candidates) {
|
|
200
|
+
const close = suggestClosest(name, candidates);
|
|
201
|
+
if (close.length === 0)
|
|
202
|
+
return "";
|
|
203
|
+
return ` Did you mean ${close.map((c) => `\`${c}\``).join(", ")}?`;
|
|
204
|
+
}
|
|
@@ -72,7 +72,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: "validate_code",
|
|
75
|
-
description: "Checks a Kotlin or Swift SceneView snippet for common mistakes. For Kotlin: threading violations, wrong destroy order, missing null-checks, LightNode trailing-lambda bug, deprecated 2.x APIs. For Swift: missing @MainActor, async/await patterns, missing imports, RealityKit mistakes. Language is auto-detected. Always call this before presenting generated SceneView code to the user.",
|
|
75
|
+
description: "Checks a Kotlin or Swift SceneView snippet for common mistakes. For Kotlin: symbol existence against the real public API (unknown imports, made-up node types, nonexistent loader methods — with did-you-mean suggestions), threading violations, wrong destroy order, missing null-checks, LightNode trailing-lambda bug, deprecated 2.x APIs. For Swift: missing @MainActor, async/await patterns, missing imports, RealityKit mistakes. Language is auto-detected. Always call this before presenting generated SceneView code to the user.",
|
|
76
76
|
inputSchema: {
|
|
77
77
|
type: "object",
|
|
78
78
|
properties: {
|
package/dist/tools/handler.js
CHANGED
|
@@ -697,7 +697,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
|
|
|
697
697
|
{ platform: "visionOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "SceneViewSwift (SPM)", features: ["3D", "Immersive spaces", "Hand tracking (planned)"] },
|
|
698
698
|
{ platform: "Web", renderer: "Filament.js (WASM)", framework: "Kotlin/JS", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-web (npm)", features: ["3D", "WebXR AR/VR", "GLB models", "WebGL2"] },
|
|
699
699
|
{ platform: "Desktop", renderer: "Software / Filament JNI", framework: "Compose Desktop", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-desktop (local)", features: ["3D", "Software renderer", "Wireframe"] },
|
|
700
|
-
{ platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "flutter pub:
|
|
700
|
+
{ platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "flutter pub: flutter_sceneview (git ref until first publish, #2735)", features: ["3D", "AR", "Android + iOS bridge"] },
|
|
701
701
|
];
|
|
702
702
|
const lines = [
|
|
703
703
|
"## SceneView Supported Platforms\n",
|
package/dist/validator.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { didYouMean, isKnownTopLevelFunction, isKnownTypeName, knownTypeNames, membersOfClass, rememberHelperNames, resolveImport, } from "./symbols.js";
|
|
1
2
|
function findLines(lines, pattern) {
|
|
2
3
|
return lines
|
|
3
4
|
.map((l, i) => (pattern.test(l) ? i + 1 : -1))
|
|
4
5
|
.filter((n) => n !== -1);
|
|
5
6
|
}
|
|
7
|
+
// Symbols the snippet declares itself (`fun MyShowcaseScene(…)`, `class ShelfNode(…) :
|
|
8
|
+
// Node(engine)`) exist by definition — they are the caller's own wrappers, not calls into
|
|
9
|
+
// SceneView, and the SDK's naming idiom (`WallPlacementScene`, `PhysicsNode`) makes such
|
|
10
|
+
// suffixes natural. The optional `<…>` skips a generic parameter list before the name.
|
|
11
|
+
function locallyDeclaredNames(code) {
|
|
12
|
+
const names = new Set();
|
|
13
|
+
for (const m of code.matchAll(/\b(?:fun|class|interface|object)\s+(?:<[^>]*>\s+)?(\w+)/g)) {
|
|
14
|
+
names.add(m[1]);
|
|
15
|
+
}
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
6
18
|
const RULES = [
|
|
7
19
|
// ─── Threading ────────────────────────────────────────────────────────────
|
|
8
20
|
{
|
|
@@ -450,6 +462,193 @@ const RULES = [
|
|
|
450
462
|
return issues;
|
|
451
463
|
},
|
|
452
464
|
},
|
|
465
|
+
// ─── Symbol existence (#2760) — backed by the generated .api index ────────
|
|
466
|
+
//
|
|
467
|
+
// These rules answer "does this API actually exist?", the #1 failure mode
|
|
468
|
+
// of AI-generated snippets. The index covers the Android/KMP surface
|
|
469
|
+
// (sceneview, arsceneview, sceneview-core) — see `symbols.ts`. Each rule is
|
|
470
|
+
// deliberately narrow so its false-positive space is ~zero.
|
|
471
|
+
{
|
|
472
|
+
// `import io.github.sceneview.…` that resolves to nothing. Our own
|
|
473
|
+
// namespace: nothing outside the index can legitimately live there.
|
|
474
|
+
// Known tolerated false NEGATIVE: aliased imports (`import … as MN`)
|
|
475
|
+
// don't match the end-of-line anchor and are silently skipped — an
|
|
476
|
+
// unchecked alias can never become a false positive.
|
|
477
|
+
id: "symbols/unknown-import",
|
|
478
|
+
severity: "error",
|
|
479
|
+
check(code, lines) {
|
|
480
|
+
const issues = [];
|
|
481
|
+
const importRe = /^\s*import\s+(io\.github\.sceneview(?:\.\w+)*)(\.\*)?\s*$/;
|
|
482
|
+
lines.forEach((content, i) => {
|
|
483
|
+
const m = importRe.exec(content);
|
|
484
|
+
if (!m)
|
|
485
|
+
return;
|
|
486
|
+
const path = m[1];
|
|
487
|
+
const isWildcard = m[2] !== undefined;
|
|
488
|
+
const resolved = resolveImport(path);
|
|
489
|
+
const ok = isWildcard ? resolved === "package" : resolved !== null;
|
|
490
|
+
if (ok)
|
|
491
|
+
return;
|
|
492
|
+
const leaf = path.slice(path.lastIndexOf(".") + 1);
|
|
493
|
+
const suggestion = didYouMean(leaf, knownTypeNames());
|
|
494
|
+
issues.push({
|
|
495
|
+
severity: "error",
|
|
496
|
+
rule: "symbols/unknown-import",
|
|
497
|
+
message: `\`import ${path}${isWildcard ? ".*" : ""}\` does not exist in the SceneView public API.${suggestion} Check the class name and package against \`llms.txt\`.`,
|
|
498
|
+
line: i + 1,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
return issues;
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
// A made-up `…Node(` / `…Node {` / `…Scene(` type or declarative factory.
|
|
506
|
+
id: "symbols/unknown-type",
|
|
507
|
+
severity: "error",
|
|
508
|
+
check(code, lines) {
|
|
509
|
+
const issues = [];
|
|
510
|
+
// Names already handled (with a better, migration-specific message) by
|
|
511
|
+
// `migration/old-api` — don't double-report them here.
|
|
512
|
+
const migrationCovered = new Set([
|
|
513
|
+
"Scene",
|
|
514
|
+
"ARScene",
|
|
515
|
+
"ArSceneView",
|
|
516
|
+
"PlacementNode",
|
|
517
|
+
"TransformableNode",
|
|
518
|
+
]);
|
|
519
|
+
const callRe = /\b([A-Z]\w*(?:Node|Scene))\s*[({]/g;
|
|
520
|
+
const localNames = locallyDeclaredNames(code);
|
|
521
|
+
const seen = new Set();
|
|
522
|
+
for (const match of code.matchAll(callRe)) {
|
|
523
|
+
const name = match[1];
|
|
524
|
+
if (seen.has(name))
|
|
525
|
+
continue;
|
|
526
|
+
seen.add(name);
|
|
527
|
+
if (migrationCovered.has(name))
|
|
528
|
+
continue;
|
|
529
|
+
if (localNames.has(name))
|
|
530
|
+
continue;
|
|
531
|
+
if (isKnownTypeName(name))
|
|
532
|
+
continue;
|
|
533
|
+
const suggestion = didYouMean(name, knownTypeNames());
|
|
534
|
+
findLines(lines, new RegExp(`\\b${name}\\s*[({]`)).forEach((line) => issues.push({
|
|
535
|
+
severity: "error",
|
|
536
|
+
rule: "symbols/unknown-type",
|
|
537
|
+
message: `\`${name}\` does not exist in the SceneView public API.${suggestion} Node types and declarative factories are listed in \`llms.txt\`.`,
|
|
538
|
+
line,
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
return issues;
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
// A nonexistent member called on a canonically-named loader variable —
|
|
546
|
+
// e.g. `modelLoader.createModelInstanceAsync(...)` (the real API is
|
|
547
|
+
// `loadModelInstanceAsync`). Heuristic on the receiver NAME, which the
|
|
548
|
+
// docs and every sample use consistently.
|
|
549
|
+
id: "symbols/unknown-member",
|
|
550
|
+
severity: "error",
|
|
551
|
+
check(code, lines) {
|
|
552
|
+
const issues = [];
|
|
553
|
+
const receivers = {
|
|
554
|
+
modelLoader: "ModelLoader",
|
|
555
|
+
materialLoader: "MaterialLoader",
|
|
556
|
+
environmentLoader: "EnvironmentLoader",
|
|
557
|
+
};
|
|
558
|
+
// Kotlin stdlib scope/extension functions callable on any receiver.
|
|
559
|
+
const stdlib = new Set([
|
|
560
|
+
"apply",
|
|
561
|
+
"also",
|
|
562
|
+
"let",
|
|
563
|
+
"run",
|
|
564
|
+
"takeIf",
|
|
565
|
+
"takeUnless",
|
|
566
|
+
"to",
|
|
567
|
+
"use",
|
|
568
|
+
"toString",
|
|
569
|
+
]);
|
|
570
|
+
// A snippet may rebind a canonical name to its own type
|
|
571
|
+
// (`val modelLoader = MyRepository()`) — member-checking that variable
|
|
572
|
+
// against the SceneView loader would be a false positive
|
|
573
|
+
// (adversarial-review finding #3 on PR #2814). Keep checking only when
|
|
574
|
+
// the declaration is recognizably the real loader: a `remember<Class>`
|
|
575
|
+
// call, a direct constructor, a `: <Class>` type annotation, or a
|
|
576
|
+
// `.<receiver>` property access — or when there is no local
|
|
577
|
+
// declaration at all (the scope provides it, the canonical case).
|
|
578
|
+
const rebound = new Set();
|
|
579
|
+
for (const [receiver, className] of Object.entries(receivers)) {
|
|
580
|
+
const decl = new RegExp(`\\b(?:val|var)\\s+${receiver}\\s*(?::\\s*([^=\\n]+?))?\\s*=\\s*(.+)`).exec(code);
|
|
581
|
+
if (!decl)
|
|
582
|
+
continue;
|
|
583
|
+
const [, typeAnnotation, rhs] = decl;
|
|
584
|
+
const looksReal = (typeAnnotation?.includes(className) ?? false) ||
|
|
585
|
+
rhs.includes(`remember${className}`) ||
|
|
586
|
+
rhs.includes(`${className}(`) ||
|
|
587
|
+
rhs.trimEnd().endsWith(`.${receiver}`);
|
|
588
|
+
if (!looksReal)
|
|
589
|
+
rebound.add(receiver);
|
|
590
|
+
}
|
|
591
|
+
const callRe = /\b(modelLoader|materialLoader|environmentLoader)\.(\w+)\s*\(/g;
|
|
592
|
+
const reported = new Set();
|
|
593
|
+
for (const match of code.matchAll(callRe)) {
|
|
594
|
+
const [, receiver, member] = match;
|
|
595
|
+
const key = `${receiver}.${member}`;
|
|
596
|
+
if (reported.has(key))
|
|
597
|
+
continue;
|
|
598
|
+
reported.add(key);
|
|
599
|
+
if (rebound.has(receiver))
|
|
600
|
+
continue;
|
|
601
|
+
if (stdlib.has(member))
|
|
602
|
+
continue;
|
|
603
|
+
const members = membersOfClass(receivers[receiver]);
|
|
604
|
+
if (!members || members.has(member))
|
|
605
|
+
continue;
|
|
606
|
+
const suggestion = didYouMean(member, members);
|
|
607
|
+
findLines(lines, new RegExp(`\\b${receiver}\\.${member}\\s*\\(`)).forEach((line) => issues.push({
|
|
608
|
+
severity: "error",
|
|
609
|
+
rule: "symbols/unknown-member",
|
|
610
|
+
message: `\`${receivers[receiver]}.${member}()\` does not exist.${suggestion} The full member list is in \`llms.txt\`.`,
|
|
611
|
+
line,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
return issues;
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
// `remember<SceneView-ish>` helper that isn't a real top-level function —
|
|
619
|
+
// e.g. an invented `rememberModelInstanceAsync`. Warning, not error: a
|
|
620
|
+
// snippet may legitimately mix non-SceneView Compose libraries whose
|
|
621
|
+
// helpers match the keyword net (e.g. Maps `rememberCameraPositionState`).
|
|
622
|
+
id: "symbols/unknown-remember-helper",
|
|
623
|
+
severity: "warning",
|
|
624
|
+
check(code, lines) {
|
|
625
|
+
const issues = [];
|
|
626
|
+
const sceneViewish = /^(Engine|Model|Material|Environment|Scene|Node|Collision|Render|Skybox|View|AR|Camera|Splat)/;
|
|
627
|
+
const callRe = /\bremember([A-Z]\w*)\s*\(/g;
|
|
628
|
+
const localNames = locallyDeclaredNames(code);
|
|
629
|
+
const seen = new Set();
|
|
630
|
+
for (const match of code.matchAll(callRe)) {
|
|
631
|
+
const fullName = `remember${match[1]}`;
|
|
632
|
+
if (seen.has(fullName))
|
|
633
|
+
continue;
|
|
634
|
+
seen.add(fullName);
|
|
635
|
+
if (!sceneViewish.test(match[1]))
|
|
636
|
+
continue;
|
|
637
|
+
if (localNames.has(fullName))
|
|
638
|
+
continue;
|
|
639
|
+
if (isKnownTopLevelFunction(fullName))
|
|
640
|
+
continue;
|
|
641
|
+
const suggestion = didYouMean(fullName, rememberHelperNames());
|
|
642
|
+
findLines(lines, new RegExp(`\\b${fullName}\\s*\\(`)).forEach((line) => issues.push({
|
|
643
|
+
severity: "warning",
|
|
644
|
+
rule: "symbols/unknown-remember-helper",
|
|
645
|
+
message: `\`${fullName}\` is not a SceneView composable helper.${suggestion} If it comes from another library, add its import; otherwise check \`llms.txt\`.`,
|
|
646
|
+
line,
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
return issues;
|
|
650
|
+
},
|
|
651
|
+
},
|
|
453
652
|
];
|
|
454
653
|
// ─── Swift Validation Rules ──────────────────────────────────────────────────
|
|
455
654
|
const SWIFT_RULES = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sceneview-mcp",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.15",
|
|
4
4
|
"mcpName": "io.github.sceneview/mcp",
|
|
5
5
|
"description": "MCP server for SceneView — cross-platform 3D & AR SDK for Android and iOS. Give Claude the full SceneView SDK so it writes correct, compilable code.",
|
|
6
6
|
"keywords": [
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"node": ">=18"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"prebuild": "node scripts/generate-llms-txt.js && node scripts/generate-version.js",
|
|
62
|
+
"prebuild": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && node scripts/generate-symbols.js",
|
|
63
63
|
"build": "tsc",
|
|
64
|
-
"prepare": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && tsc",
|
|
64
|
+
"prepare": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && node scripts/generate-symbols.js && tsc",
|
|
65
65
|
"start": "node dist/index.js",
|
|
66
66
|
"dev": "tsx src/index.ts",
|
|
67
|
-
"test": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && vitest run",
|
|
67
|
+
"test": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && node scripts/generate-symbols.js && vitest run",
|
|
68
68
|
"biome": "cd .. && mcp/node_modules/.bin/biome check",
|
|
69
69
|
"biome:fix": "cd .. && mcp/node_modules/.bin/biome check --write"
|
|
70
70
|
},
|