unity-open-mcp 0.5.0 → 0.7.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 (124) hide show
  1. package/README.md +24 -73
  2. package/dist/batch-spawn.d.ts +4 -0
  3. package/dist/batch-spawn.js +73 -18
  4. package/dist/batch-spawn.js.map +1 -1
  5. package/dist/capabilities/build-capabilities.d.ts +5 -5
  6. package/dist/capabilities/build-capabilities.js +2 -2
  7. package/dist/capabilities/cost-hints.js +20 -0
  8. package/dist/capabilities/cost-hints.js.map +1 -1
  9. package/dist/capabilities/lifecycle.js +1 -1
  10. package/dist/capabilities/lifecycle.js.map +1 -1
  11. package/dist/capabilities/tool-groups.d.ts +6 -9
  12. package/dist/capabilities/tool-groups.js +11 -13
  13. package/dist/capabilities/tool-groups.js.map +1 -1
  14. package/dist/cli/commands.js +9 -8
  15. package/dist/cli/commands.js.map +1 -1
  16. package/dist/compressible-router.js +6 -0
  17. package/dist/compressible-router.js.map +1 -1
  18. package/dist/constants.d.ts +89 -0
  19. package/dist/constants.js +109 -0
  20. package/dist/constants.js.map +1 -0
  21. package/dist/dialog-dismiss.d.ts +0 -7
  22. package/dist/dialog-dismiss.js +17 -19
  23. package/dist/dialog-dismiss.js.map +1 -1
  24. package/dist/hub-control.d.ts +6 -2
  25. package/dist/hub-control.js +8 -8
  26. package/dist/hub-control.js.map +1 -1
  27. package/dist/index.js +12 -13
  28. package/dist/index.js.map +1 -1
  29. package/dist/instance-discovery.js +2 -1
  30. package/dist/instance-discovery.js.map +1 -1
  31. package/dist/live-client.d.ts +49 -0
  32. package/dist/live-client.js +292 -255
  33. package/dist/live-client.js.map +1 -1
  34. package/dist/offline/api.d.ts +171 -0
  35. package/dist/offline/api.js +855 -0
  36. package/dist/offline/api.js.map +1 -0
  37. package/dist/offline/hierarchy.d.ts +24 -0
  38. package/dist/offline/hierarchy.js +242 -0
  39. package/dist/offline/hierarchy.js.map +1 -0
  40. package/dist/offline/index-builders.d.ts +11 -0
  41. package/dist/offline/index-builders.js +202 -0
  42. package/dist/offline/index-builders.js.map +1 -0
  43. package/dist/offline/model.d.ts +8 -0
  44. package/dist/offline/model.js +188 -0
  45. package/dist/offline/model.js.map +1 -0
  46. package/dist/offline/names.d.ts +7 -0
  47. package/dist/offline/names.js +20 -0
  48. package/dist/offline/names.js.map +1 -0
  49. package/dist/offline/overrides.d.ts +17 -0
  50. package/dist/offline/overrides.js +266 -0
  51. package/dist/offline/overrides.js.map +1 -0
  52. package/dist/offline/parse.d.ts +25 -0
  53. package/dist/offline/parse.js +236 -0
  54. package/dist/offline/parse.js.map +1 -0
  55. package/dist/offline/paths.d.ts +4 -0
  56. package/dist/offline/paths.js +23 -0
  57. package/dist/offline/paths.js.map +1 -0
  58. package/dist/offline/primitives.d.ts +17 -0
  59. package/dist/offline/primitives.js +195 -0
  60. package/dist/offline/primitives.js.map +1 -0
  61. package/dist/offline/references.d.ts +9 -0
  62. package/dist/offline/references.js +55 -0
  63. package/dist/offline/references.js.map +1 -0
  64. package/dist/offline/types.d.ts +65 -0
  65. package/dist/offline/types.js +154 -0
  66. package/dist/offline/types.js.map +1 -0
  67. package/dist/offline.d.ts +2 -180
  68. package/dist/offline.js +27 -2282
  69. package/dist/offline.js.map +1 -1
  70. package/dist/project-health.d.ts +1 -1
  71. package/dist/project-health.js +66 -3
  72. package/dist/project-health.js.map +1 -1
  73. package/dist/routers.js +6 -5
  74. package/dist/routers.js.map +1 -1
  75. package/dist/tool-router.d.ts +13 -0
  76. package/dist/tool-router.js +179 -251
  77. package/dist/tool-router.js.map +1 -1
  78. package/dist/tool-session-state.js +18 -9
  79. package/dist/tool-session-state.js.map +1 -1
  80. package/dist/tools/apply-fix.js +7 -2
  81. package/dist/tools/apply-fix.js.map +1 -1
  82. package/dist/tools/batch-execute.js +8 -2
  83. package/dist/tools/batch-execute.js.map +1 -1
  84. package/dist/tools/bridge-status.js +1 -2
  85. package/dist/tools/bridge-status.js.map +1 -1
  86. package/dist/tools/checkpoint-create.js +1 -0
  87. package/dist/tools/checkpoint-create.js.map +1 -1
  88. package/dist/tools/component-get.js +44 -11
  89. package/dist/tools/component-get.js.map +1 -1
  90. package/dist/tools/delta.js +1 -0
  91. package/dist/tools/delta.js.map +1 -1
  92. package/dist/tools/dependencies.js +4 -1
  93. package/dist/tools/dependencies.js.map +1 -1
  94. package/dist/tools/editor-status.js +3 -2
  95. package/dist/tools/editor-status.js.map +1 -1
  96. package/dist/tools/execute-csharp.js +3 -2
  97. package/dist/tools/execute-csharp.js.map +1 -1
  98. package/dist/tools/find-references.js +8 -1
  99. package/dist/tools/find-references.js.map +1 -1
  100. package/dist/tools/index.js +19 -17
  101. package/dist/tools/index.js.map +1 -1
  102. package/dist/tools/inputsystem-asset-create.js +4 -4
  103. package/dist/tools/inputsystem-asset-create.js.map +1 -1
  104. package/dist/tools/invoke-method.js +2 -1
  105. package/dist/tools/invoke-method.js.map +1 -1
  106. package/dist/tools/manage-tools.js +10 -10
  107. package/dist/tools/manage-tools.js.map +1 -1
  108. package/dist/tools/navigation-surface-add.js +6 -5
  109. package/dist/tools/navigation-surface-add.js.map +1 -1
  110. package/dist/tools/probuilder-create-shape.js +4 -4
  111. package/dist/tools/probuilder-create-shape.js.map +1 -1
  112. package/dist/tools/read-compile-errors.js +44 -31
  113. package/dist/tools/read-compile-errors.js.map +1 -1
  114. package/dist/tools/scene-get-data.js +10 -2
  115. package/dist/tools/scene-get-data.js.map +1 -1
  116. package/dist/tools/scene-save.js +16 -5
  117. package/dist/tools/scene-save.js.map +1 -1
  118. package/dist/tools/scene-set-active.js +22 -6
  119. package/dist/tools/scene-set-active.js.map +1 -1
  120. package/dist/tools/scene-unload.js +20 -6
  121. package/dist/tools/scene-unload.js.map +1 -1
  122. package/dist/tools/search-assets.js +3 -1
  123. package/dist/tools/search-assets.js.map +1 -1
  124. package/package.json +1 -1
@@ -0,0 +1,855 @@
1
+ // Public entry points for the offline asset reader.
2
+ //
3
+ // Extracted from the former monolithic offline.ts (M28-refactoring Plan 3,
4
+ // T3.1). These are the functions the compressible-tool router and tool-router
5
+ // call: readAssetOffline, searchAssetsOffline, listAssetsOffline,
6
+ // findReferencesOffline, dependenciesOffline, scanIntegrityOffline, plus the
7
+ // isOfflineAsset / countHierarchy helpers and the result interfaces.
8
+ //
9
+ // Composes all the offline submodules. Wires the overrides label-registry at
10
+ // import time so the overrides resolver can call back into the hierarchy/
11
+ // reference layers without an import cycle.
12
+ import { readFile } from "node:fs/promises";
13
+ import { basename, extname, join } from "node:path";
14
+ // HierarchyResult is re-exported indirectly via the hierarchy functions; the
15
+ // api module does not name the type directly.
16
+ import { OFFLINE_PARSEABLE_EXTENSIONS, kindForPath, normalizeKind, offlineParseKind, parseKindSet, } from "./types.js";
17
+ import { cleanScalar, findGUIDs, readGUIDField } from "./primitives.js";
18
+ import { parseAsset } from "./parse.js";
19
+ import { displayFieldName } from "./names.js";
20
+ import { buildAssetModel, buildJsonAssetModel, runYamlIntegrityChecks, } from "./model.js";
21
+ import { buildGUIDIndex, buildScriptIndex, buildScriptIndexForQuery, collectFiles, resolveGUIDPaths, safeReadMetaGUID, scriptGUIDs, walkFiles, walkMeta, } from "./index-builders.js";
22
+ import { toAssetPath, relativeDir } from "./paths.js";
23
+ import { OVERRIDE_LABEL_HELPERS } from "./references.js";
24
+ import { setLabelHelpers } from "./overrides.js";
25
+ import { buildHierarchy, componentsFor, flattenHierarchy, } from "./hierarchy.js";
26
+ // Wire the overrides label-registry once at module load. This breaks the
27
+ // overrides→hierarchy/references cycle by late-binding the real helpers.
28
+ setLabelHelpers(OVERRIDE_LABEL_HELPERS);
29
+ // ===========================================================================
30
+ // isOfflineAsset / countHierarchy helpers.
31
+ // ===========================================================================
32
+ /**
33
+ * M24 — full-hierarchy parity helper. The offline `objectCount` counts every
34
+ * YAML object in the file; the live `read_asset` bridge counts only GameObjects
35
+ * + the components attached to them (it walks the loaded Transform tree, not
36
+ * the raw YAML). This helper returns the live-comparable counts so an
37
+ * acceptance test can verify the offline-reconstructed tree matches a live read
38
+ * node-for-node and component-for-component, without depending on the bridge.
39
+ */
40
+ export function countHierarchy(model) {
41
+ let nodes = 0;
42
+ let components = 0;
43
+ const walk = (list) => {
44
+ for (const node of list) {
45
+ nodes++;
46
+ components += node.components.length;
47
+ walk(node.children);
48
+ }
49
+ };
50
+ walk(model.roots);
51
+ return { nodes, components };
52
+ }
53
+ export function isOfflineAsset(assetPath) {
54
+ return offlineParseKind(assetPath) !== null;
55
+ }
56
+ // ===========================================================================
57
+ // readAssetOffline — parse a text-serialized asset → AssetModel.
58
+ // ===========================================================================
59
+ export async function readAssetOffline(assetPath, opts) {
60
+ const absPath = join(opts.projectRoot, assetPath);
61
+ const data = await readFile(absPath, "utf-8");
62
+ const kind = kindForPath(assetPath);
63
+ const guid = await safeReadMetaGUID(absPath + ".meta");
64
+ const parseKind = offlineParseKind(assetPath);
65
+ // M24 — JSON asset kinds (.asmdef/.shadergraph/.shadersubgraph/.vfx) parse
66
+ // through a separate path; unity-scanner handles YAML only, so this is the
67
+ // coverage differentiator. vfx is YAML+binary, but its object headers + GUID
68
+ // refs still parse via the YAML path (binary blobs are skipped).
69
+ if (parseKind === "json") {
70
+ const guidIndex = await buildGUIDIndex(opts.projectRoot, new Set(findGUIDs(data)));
71
+ const model = buildJsonAssetModel(data, assetPath, kind, guid, guidIndex, opts.fieldLimit);
72
+ return { model, source: "offline" };
73
+ }
74
+ const parsed = parseAsset(data);
75
+ parsed.path = assetPath;
76
+ parsed.kind = kind;
77
+ parsed.guid = guid;
78
+ const wantedScripts = scriptGUIDs(parsed);
79
+ const scriptIndex = await buildScriptIndex(opts.projectRoot, wantedScripts);
80
+ const wantedGUIDs = collectFieldGUIDs(parsed);
81
+ const guidIndex = await buildGUIDIndex(opts.projectRoot, wantedGUIDs);
82
+ for (const [g, p] of scriptIndex)
83
+ guidIndex.set(g, p);
84
+ const integrity = runYamlIntegrityChecks(parsed, guidIndex);
85
+ return {
86
+ model: { ...buildAssetModel(parsed, scriptIndex, guidIndex, opts.fieldLimit), ...(integrity ? { integrity } : {}) },
87
+ source: "offline",
88
+ };
89
+ }
90
+ function collectFieldGUIDs(asset) {
91
+ const guids = new Set();
92
+ for (const obj of asset.objects) {
93
+ for (const line of obj.lines) {
94
+ if (!line.includes("guid:") || line.includes("m_Script"))
95
+ continue;
96
+ for (const g of findGUIDs(line))
97
+ guids.add(g);
98
+ }
99
+ }
100
+ return guids;
101
+ }
102
+ // ===========================================================================
103
+ // searchAssetsOffline — scan Assets/ → SearchModel.
104
+ // ===========================================================================
105
+ const SEARCHABLE_EXTENSIONS = OFFLINE_PARSEABLE_EXTENSIONS;
106
+ export async function searchAssetsOffline(opts) {
107
+ const nameQuery = (opts.name ?? "").trim().toLowerCase();
108
+ const componentQuery = (opts.component ?? "").trim().toLowerCase();
109
+ const guidQuery = (opts.guid ?? "").trim().toLowerCase();
110
+ const typeFilter = opts.type ? parseKindSet(opts.type) : null;
111
+ const folder = opts.folder ?? "Assets";
112
+ const maxResults = opts.maxResults ?? 50;
113
+ const searchDir = join(opts.projectRoot, folder);
114
+ const scriptIndex = componentQuery
115
+ ? await buildScriptIndexForQuery(opts.projectRoot, componentQuery)
116
+ : new Map();
117
+ const guidIndex = guidQuery
118
+ ? await resolveGUIDPaths(opts.projectRoot, guidQuery)
119
+ : new Map();
120
+ const matches = [];
121
+ const files = await collectFiles(searchDir);
122
+ for (const filePath of files) {
123
+ if (matches.length >= maxResults)
124
+ break;
125
+ const assetPath = toAssetPath(opts.projectRoot, filePath);
126
+ const ext = extname(filePath).toLowerCase();
127
+ if (!SEARCHABLE_EXTENSIONS.has(ext))
128
+ continue;
129
+ const kind = kindForPath(filePath);
130
+ if (typeFilter && typeFilter.size > 0 && !typeFilter.has(kind))
131
+ continue;
132
+ const guid = await safeReadMetaGUID(filePath + ".meta");
133
+ // M24 — JSON asset kinds route through a content-based match path (they
134
+ // have no GameObject/component tree for the YAML matcher to walk).
135
+ if (offlineParseKind(filePath) === "json") {
136
+ let content;
137
+ try {
138
+ content = await readFile(filePath, "utf-8");
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ const match = checkJsonMatch(content, guidIndex, assetPath, kind, guid, {
144
+ nameQuery, componentQuery, guidQuery,
145
+ });
146
+ if (match)
147
+ matches.push(match);
148
+ continue;
149
+ }
150
+ let parsed;
151
+ try {
152
+ parsed = parseAsset(await readFile(filePath, "utf-8"));
153
+ }
154
+ catch {
155
+ continue;
156
+ }
157
+ parsed.path = assetPath;
158
+ parsed.kind = kind;
159
+ const match = checkMatch(parsed, scriptIndex, guidIndex, assetPath, kind, guid, { nameQuery, componentQuery, guidQuery });
160
+ if (match)
161
+ matches.push(match);
162
+ }
163
+ return {
164
+ query: { name: opts.name, component: opts.component, guid: opts.guid, type: opts.type },
165
+ matchCount: matches.length,
166
+ matches,
167
+ truncated: 0,
168
+ };
169
+ }
170
+ function checkMatch(parsed, scriptIndex, guidIndex, assetPath, kind, guid, criteria) {
171
+ const reasons = [];
172
+ const objects = [];
173
+ const { nameQuery, componentQuery, guidQuery } = criteria;
174
+ const fileName = basename(assetPath);
175
+ if (nameQuery && fileName.toLowerCase().includes(nameQuery))
176
+ reasons.push("file-name");
177
+ if (guidQuery) {
178
+ if (guid.toLowerCase() === guidQuery)
179
+ reasons.push("guid");
180
+ let hasFieldGUID = false;
181
+ for (const obj of parsed.objects) {
182
+ if (fieldGUIDs(obj).has(guidQuery)) {
183
+ hasFieldGUID = true;
184
+ break;
185
+ }
186
+ }
187
+ if (hasFieldGUID)
188
+ reasons.push("guid");
189
+ }
190
+ const hierarchy = buildHierarchy(parsed);
191
+ if (nameQuery) {
192
+ for (const node of flattenHierarchy(hierarchy)) {
193
+ if (node.gameObject.name.toLowerCase().includes(nameQuery)) {
194
+ const comps = componentsFor(parsed, node.gameObject.id, scriptIndex);
195
+ objects.push({ path: node.path, components: comps.map((c) => c.name) });
196
+ }
197
+ }
198
+ if (objects.length > 0 && !reasons.includes("file-name"))
199
+ reasons.push("gameobject");
200
+ }
201
+ if (componentQuery) {
202
+ let componentMatch = false;
203
+ for (const node of flattenHierarchy(hierarchy)) {
204
+ const comps = componentsFor(parsed, node.gameObject.id, scriptIndex);
205
+ const matching = comps.filter((c) => c.name.toLowerCase().includes(componentQuery) ||
206
+ (c.scriptPath && c.scriptPath.toLowerCase().includes(componentQuery)));
207
+ if (matching.length > 0) {
208
+ componentMatch = true;
209
+ if (!objects.some((o) => o.path === node.path)) {
210
+ objects.push({ path: node.path, components: comps.map((c) => c.name) });
211
+ }
212
+ }
213
+ }
214
+ if (componentMatch && !reasons.includes("component"))
215
+ reasons.push("component");
216
+ }
217
+ if (!componentQuery && !nameQuery && !guidQuery)
218
+ reasons.push("file-name");
219
+ if (reasons.length === 0)
220
+ return null;
221
+ return { path: assetPath, guid: guid || undefined, kind, reasons, objects: objects.length > 0 ? objects : undefined };
222
+ }
223
+ /**
224
+ * M24 — content-based matcher for JSON asset kinds (.asmdef/.shadergraph).
225
+ * They have no GameObject tree, so matching is by file name, raw-text GUID
226
+ * presence, and content substring (name/component queries search the raw text
227
+ * since JSON is human-readable). This keeps `search_assets` offline-capable for
228
+ * the expanded type set.
229
+ */
230
+ function checkJsonMatch(content, guidIndex, assetPath, kind, guid, criteria) {
231
+ const reasons = [];
232
+ const { nameQuery, componentQuery, guidQuery } = criteria;
233
+ const fileName = basename(assetPath);
234
+ const lower = content.toLowerCase();
235
+ if (nameQuery && fileName.toLowerCase().includes(nameQuery))
236
+ reasons.push("file-name");
237
+ if (guidQuery) {
238
+ if (guid.toLowerCase() === guidQuery)
239
+ reasons.push("guid");
240
+ else if (lower.includes(guidQuery))
241
+ reasons.push("guid");
242
+ }
243
+ if (nameQuery && !reasons.includes("file-name") && lower.includes(nameQuery)) {
244
+ reasons.push("gameobject");
245
+ }
246
+ if (componentQuery && lower.includes(componentQuery)) {
247
+ reasons.push("component");
248
+ }
249
+ if (!componentQuery && !nameQuery && !guidQuery)
250
+ reasons.push("file-name");
251
+ if (reasons.length === 0)
252
+ return null;
253
+ void guidIndex;
254
+ return { path: assetPath, guid: guid || undefined, kind, reasons };
255
+ }
256
+ function fieldGUIDs(obj) {
257
+ const guids = new Set();
258
+ for (const line of obj.lines) {
259
+ if (!line.includes("guid:") || line.includes("m_Script"))
260
+ continue;
261
+ for (const guid of findGUIDs(line))
262
+ guids.add(guid);
263
+ }
264
+ return guids;
265
+ }
266
+ const LISTABLE_EXTENSIONS = new Set([
267
+ ".prefab", ".unity", ".asset", ".mat", ".controller", ".anim", ".playable",
268
+ ".cs", ".shader", ".spriteatlas", ".physicMaterial", ".physicsMaterial2D",
269
+ ".overrideController", ".uxml", ".uss",
270
+ // M24 — expanded offline-parseable kinds (also listable so they show up in
271
+ // directory listings; listing never parses, so they cost nothing to include).
272
+ ".asmdef", ".shadergraph", ".shadersubgraph", ".vfx", ".preset", ".terrainlayer",
273
+ ]);
274
+ export async function listAssetsOffline(opts) {
275
+ const folder = opts.folder ?? "Assets";
276
+ const typeFilter = opts.type
277
+ ? new Set(opts.type.split(",").map((t) => normalizeKind(t)).filter((t) => t !== ""))
278
+ : null;
279
+ const searchDir = join(opts.projectRoot, folder);
280
+ const folders = [];
281
+ const kindSummary = {};
282
+ let totalFiles = 0;
283
+ let truncated = 0;
284
+ const folderMap = new Map();
285
+ await walkFiles(searchDir, opts.projectRoot, async (absPath) => {
286
+ const ext = extname(absPath).toLowerCase();
287
+ if (!LISTABLE_EXTENSIONS.has(ext))
288
+ return;
289
+ const kind = kindForPath(absPath);
290
+ if (typeFilter && typeFilter.size > 0 && !typeFilter.has(kind))
291
+ return;
292
+ const relDir = relativeDir(opts.projectRoot, absPath);
293
+ const fileName = basename(absPath);
294
+ let listing = folderMap.get(relDir);
295
+ if (!listing) {
296
+ listing = { folder: relDir, kinds: {}, fileCount: 0 };
297
+ folderMap.set(relDir, listing);
298
+ folders.push(listing);
299
+ }
300
+ listing.fileCount++;
301
+ totalFiles++;
302
+ if (!listing.kinds[kind])
303
+ listing.kinds[kind] = { count: 0, sample: [] };
304
+ listing.kinds[kind].count++;
305
+ kindSummary[kind] = (kindSummary[kind] ?? 0) + 1;
306
+ const samples = listing.kinds[kind].sample;
307
+ if (samples.length < 5) {
308
+ const nameWithoutExt = ext ? fileName.slice(0, -(ext.length)) : fileName;
309
+ if (!samples.includes(nameWithoutExt))
310
+ samples.push(nameWithoutExt);
311
+ }
312
+ else {
313
+ truncated++;
314
+ }
315
+ });
316
+ folders.sort((a, b) => a.folder.localeCompare(b.folder));
317
+ return {
318
+ root: folder, typeFilter: opts.type, folders,
319
+ totalFiles, totalFolders: folders.length, kindSummary, truncated,
320
+ };
321
+ }
322
+ // ===========================================================================
323
+ // findReferencesOffline — offline reverse reference lookup (T1.4).
324
+ // ===========================================================================
325
+ const REFERENCEABLE_EXTENSIONS = OFFLINE_PARSEABLE_EXTENSIONS;
326
+ export async function findReferencesOffline(opts) {
327
+ const detail = (opts.detail ?? "normal");
328
+ const maxResults = opts.maxResults ?? 100;
329
+ const maxPerFile = opts.maxPerFile ?? 5;
330
+ const patternThreshold = opts.patternThreshold ?? 0;
331
+ // Resolve target GUID + path.
332
+ let targetGuid = "";
333
+ let targetPath = "";
334
+ if (opts.guid) {
335
+ targetGuid = opts.guid.toLowerCase();
336
+ }
337
+ else if (opts.assetPath) {
338
+ targetPath = opts.assetPath;
339
+ targetGuid = (await safeReadMetaGUID(join(opts.projectRoot, opts.assetPath + ".meta"))).toLowerCase();
340
+ }
341
+ if (targetGuid === "") {
342
+ return {
343
+ queriedAssetPath: targetPath,
344
+ queriedAssetGuid: "",
345
+ referencedBy: [],
346
+ totalCount: 0,
347
+ byKind: {},
348
+ byFolder: {},
349
+ detail,
350
+ truncated: 0,
351
+ };
352
+ }
353
+ // If only GUID was given, resolve target path from .meta index.
354
+ if (targetPath === "") {
355
+ const guidIndex = await buildGUIDIndex(opts.projectRoot, new Set([targetGuid]));
356
+ targetPath = guidIndex.get(targetGuid) ?? "";
357
+ }
358
+ // Scan all candidate files in Assets/.
359
+ const candidates = await collectFiles(join(opts.projectRoot, "Assets"));
360
+ const hits = [];
361
+ for (const absPath of candidates) {
362
+ const ext = extname(absPath).toLowerCase();
363
+ if (!REFERENCEABLE_EXTENSIONS.has(ext))
364
+ continue;
365
+ let content;
366
+ try {
367
+ content = await readFile(absPath, "utf-8");
368
+ }
369
+ catch {
370
+ continue;
371
+ }
372
+ // Fast filter: skip files that don't mention the GUID at all.
373
+ if (!content.includes(targetGuid))
374
+ continue;
375
+ const assetPath = toAssetPath(opts.projectRoot, absPath);
376
+ // Skip self-reference.
377
+ if (assetPath === targetPath)
378
+ continue;
379
+ const kind = kindForPath(absPath);
380
+ const folder = relativeDir(opts.projectRoot, absPath);
381
+ const guid = await safeReadMetaGUID(absPath + ".meta");
382
+ const entry = {
383
+ assetPath,
384
+ guid: guid || undefined,
385
+ kind,
386
+ folder,
387
+ };
388
+ if (detail === "verbose") {
389
+ entry.locations = extractReferenceLocations(content, targetGuid, maxPerFile);
390
+ }
391
+ hits.push(entry);
392
+ }
393
+ // Group by kind and folder.
394
+ const byKind = {};
395
+ const byFolder = {};
396
+ for (const hit of hits) {
397
+ byKind[hit.kind] = (byKind[hit.kind] ?? 0) + 1;
398
+ byFolder[hit.folder] = (byFolder[hit.folder] ?? 0) + 1;
399
+ }
400
+ // Pattern collapsing: folders with >= threshold files get collapsed.
401
+ let collapsedGroups;
402
+ let displayHits = hits;
403
+ if (patternThreshold > 0) {
404
+ collapsedGroups = [];
405
+ const collapsedFolders = new Set();
406
+ for (const [folder, count] of Object.entries(byFolder)) {
407
+ if (count < patternThreshold)
408
+ continue;
409
+ const kinds = {};
410
+ for (const hit of hits) {
411
+ if (hit.folder !== folder)
412
+ continue;
413
+ kinds[hit.kind] = (kinds[hit.kind] ?? 0) + 1;
414
+ }
415
+ collapsedGroups.push({ folder, count, kinds });
416
+ collapsedFolders.add(folder);
417
+ }
418
+ if (collapsedFolders.size > 0) {
419
+ displayHits = hits.filter((h) => !collapsedFolders.has(h.folder));
420
+ }
421
+ }
422
+ // Apply max_results cap and detail filtering.
423
+ const totalCount = hits.length;
424
+ let referencedBy;
425
+ let truncated = 0;
426
+ if (detail === "summary") {
427
+ referencedBy = [];
428
+ }
429
+ else {
430
+ referencedBy = displayHits.slice(0, maxResults);
431
+ truncated = Math.max(0, displayHits.length - maxResults);
432
+ }
433
+ return {
434
+ queriedAssetPath: targetPath,
435
+ queriedAssetGuid: targetGuid,
436
+ referencedBy,
437
+ totalCount,
438
+ byKind,
439
+ byFolder,
440
+ collapsedGroups: collapsedGroups && collapsedGroups.length > 0
441
+ ? collapsedGroups
442
+ : undefined,
443
+ detail,
444
+ truncated,
445
+ };
446
+ }
447
+ function extractReferenceLocations(content, targetGuid, maxPerFile) {
448
+ const locations = [];
449
+ const lines = content.split("\n");
450
+ for (let i = 0; i < lines.length; i++) {
451
+ if (!lines[i].includes(targetGuid))
452
+ continue;
453
+ if (locations.length >= maxPerFile)
454
+ break;
455
+ const trim = lines[i].trim();
456
+ const cleaned = trim.startsWith("- ") ? trim.slice(2) : trim;
457
+ const colonIdx = cleaned.indexOf(":");
458
+ const field = colonIdx > 0 ? cleaned.slice(0, colonIdx).trim() : cleaned;
459
+ if (field === "target") {
460
+ let label = "prefab modification";
461
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
462
+ const pp = lines[j].trim();
463
+ if (pp.startsWith("propertyPath:")) {
464
+ label = `prefab → ${displayFieldName(cleanScalar(pp.slice("propertyPath:".length).trim()))}`;
465
+ break;
466
+ }
467
+ if (pp.startsWith("- target:") || pp.startsWith("- ") || pp === "")
468
+ continue;
469
+ break;
470
+ }
471
+ locations.push(label);
472
+ }
473
+ else {
474
+ locations.push(displayFieldName(field));
475
+ }
476
+ }
477
+ return locations;
478
+ }
479
+ export async function dependenciesOffline(opts) {
480
+ const detail = (opts.detail ?? "normal");
481
+ const maxResults = opts.maxResults ?? 100;
482
+ const includeImpact = opts.includeImpact ?? false;
483
+ const maxImpactDepth = opts.maxImpactDepth ?? 5;
484
+ const maxCycleDepth = opts.maxCycleDepth ?? 8;
485
+ // Resolve target GUID + path (mirrors findReferencesOffline's resolution).
486
+ let targetGuid = "";
487
+ let targetPath = "";
488
+ if (opts.guid) {
489
+ targetGuid = opts.guid.toLowerCase();
490
+ }
491
+ else if (opts.assetPath) {
492
+ targetPath = opts.assetPath;
493
+ targetGuid = (await safeReadMetaGUID(join(opts.projectRoot, opts.assetPath + ".meta"))).toLowerCase();
494
+ }
495
+ if (targetGuid === "") {
496
+ return emptyDependencies(targetPath, "", detail);
497
+ }
498
+ if (targetPath === "") {
499
+ const idx = await buildGUIDIndex(opts.projectRoot, new Set([targetGuid]));
500
+ targetPath = idx.get(targetGuid) ?? "";
501
+ }
502
+ // ---- Forward edges ----
503
+ let forwardEdges = [];
504
+ let brokenGuids = [];
505
+ let cycles = [];
506
+ let forwardSkipped;
507
+ const absPath = join(opts.projectRoot, targetPath);
508
+ let data;
509
+ try {
510
+ data = await readFile(absPath, "utf-8");
511
+ }
512
+ catch {
513
+ forwardSkipped = "queried asset not readable on disk";
514
+ }
515
+ if (data !== undefined) {
516
+ try {
517
+ const parsed = parseAsset(data);
518
+ parsed.path = targetPath;
519
+ forwardEdges = collectForwardEdges(parsed);
520
+ const wanted = new Set(forwardEdges.map((e) => e.guid));
521
+ const guidIndex = await buildGUIDIndex(opts.projectRoot, wanted);
522
+ brokenGuids = resolveForwardEdges(forwardEdges, guidIndex);
523
+ if (forwardEdges.length > 0) {
524
+ cycles = await detectCyclesOffline(targetPath, forwardEdges, opts.projectRoot, maxCycleDepth);
525
+ }
526
+ }
527
+ catch (err) {
528
+ forwardSkipped = err instanceof Error ? err.message : String(err);
529
+ }
530
+ }
531
+ // ---- Reverse edges ---- (reuse the findReferencesOffline machinery).
532
+ const refResult = await findReferencesOffline({
533
+ assetPath: targetPath || undefined,
534
+ guid: targetGuid,
535
+ detail: "normal",
536
+ projectRoot: opts.projectRoot,
537
+ });
538
+ const reverseEdges = refResult.referencedBy.map((e) => ({
539
+ assetPath: e.assetPath,
540
+ guid: e.guid ?? "",
541
+ kind: e.kind,
542
+ }));
543
+ // ---- Transitive impact (optional) ----
544
+ let impact;
545
+ if (includeImpact) {
546
+ impact = await computeTransitiveImpact(targetGuid, targetPath, opts.projectRoot, maxImpactDepth);
547
+ }
548
+ // ---- Apply detail + caps ----
549
+ const displayForward = detail === "summary" ? [] : forwardEdges;
550
+ const totalCount = reverseEdges.length;
551
+ let displayReverse;
552
+ let truncated = 0;
553
+ if (detail === "summary") {
554
+ displayReverse = [];
555
+ }
556
+ else if (maxResults > 0 && totalCount > maxResults) {
557
+ displayReverse = reverseEdges.slice(0, maxResults);
558
+ truncated = totalCount - maxResults;
559
+ }
560
+ else {
561
+ displayReverse = reverseEdges;
562
+ }
563
+ return {
564
+ queriedAssetPath: targetPath,
565
+ queriedAssetGuid: targetGuid,
566
+ forwardDependencies: displayForward,
567
+ forwardCount: forwardEdges.length,
568
+ brokenForwardGuids: brokenGuids,
569
+ cycles,
570
+ reverseDependencies: displayReverse,
571
+ reverseCount: totalCount,
572
+ impact,
573
+ detail,
574
+ truncated,
575
+ ...(forwardSkipped ? { forwardSkipped } : {}),
576
+ // Inline tag is load-bearing — see the note on DependenciesOfflineResult._source.
577
+ _source: "offline",
578
+ };
579
+ }
580
+ function emptyDependencies(path, guid, detail) {
581
+ return {
582
+ queriedAssetPath: path,
583
+ queriedAssetGuid: guid,
584
+ forwardDependencies: [],
585
+ forwardCount: 0,
586
+ brokenForwardGuids: [],
587
+ cycles: [],
588
+ reverseDependencies: [],
589
+ reverseCount: 0,
590
+ detail,
591
+ truncated: 0,
592
+ // Inline tag is load-bearing — see the note on DependenciesOfflineResult._source.
593
+ _source: "offline",
594
+ };
595
+ }
596
+ /** Collect the forward edges declared by a single asset. */
597
+ function collectForwardEdges(parsed) {
598
+ const edges = [];
599
+ const seen = new Set();
600
+ // 1. PrefabInstance.m_SourcePrefab — the base-prefab edge of a variant.
601
+ for (const obj of parsed.objects) {
602
+ if (obj.type !== "PrefabInstance")
603
+ continue;
604
+ const sourceGuid = readGUIDField(obj.lines, "m_SourcePrefab");
605
+ if (sourceGuid !== "" && seen.add(`prefab:${sourceGuid}`)) {
606
+ edges.push({ guid: sourceGuid, assetPath: "", kind: "prefab_source", resolved: false });
607
+ }
608
+ }
609
+ // 2. m_Script GUIDs on MonoBehaviours — the script-class edge.
610
+ for (const obj of parsed.objects) {
611
+ if (obj.type !== "MonoBehaviour")
612
+ continue;
613
+ if (obj.scriptGUID !== "" && seen.add(`script:${obj.scriptGUID}`)) {
614
+ edges.push({ guid: obj.scriptGUID, assetPath: "", kind: "script", resolved: false });
615
+ }
616
+ }
617
+ // 3. Every other guid: field — material refs, asset refs, animation clips,
618
+ // etc. m_Script is handled above; m_SourcePrefab target: GUIDs inside
619
+ // PrefabInstance.m_Modifications are included here so variant overrides
620
+ // count as forward edges too.
621
+ for (const obj of parsed.objects) {
622
+ for (const line of obj.lines) {
623
+ if (!line.includes("guid:"))
624
+ continue;
625
+ if (line.includes("m_Script:"))
626
+ continue;
627
+ for (const g of findGUIDs(line)) {
628
+ if (!seen.add(`pptr:${g}`))
629
+ continue;
630
+ edges.push({ guid: g, assetPath: "", kind: "pptr", resolved: false });
631
+ }
632
+ }
633
+ }
634
+ return edges;
635
+ }
636
+ /** Resolve forward edges against the GUID→path index. Mutates each edge in
637
+ * place: sets assetPath + resolved. Returns the broken (unresolved) GUIDs. */
638
+ function resolveForwardEdges(edges, guidIndex) {
639
+ const broken = [];
640
+ const brokenSeen = new Set();
641
+ for (const edge of edges) {
642
+ const path = guidIndex.get(edge.guid);
643
+ if (path !== undefined) {
644
+ edge.assetPath = path;
645
+ edge.resolved = true;
646
+ }
647
+ else if (brokenSeen.add(edge.guid)) {
648
+ broken.push(edge.guid);
649
+ }
650
+ }
651
+ return broken;
652
+ }
653
+ /** Forward-cycle detection via DFS over the transitive forward closure. */
654
+ async function detectCyclesOffline(startPath, startEdges, projectRoot, maxDepth) {
655
+ const cycles = [];
656
+ const edgeCache = new Map();
657
+ edgeCache.set(startPath, startEdges);
658
+ async function edgesOf(path) {
659
+ const cached = edgeCache.get(path);
660
+ if (cached)
661
+ return cached;
662
+ try {
663
+ const data = await readFile(join(projectRoot, path), "utf-8");
664
+ const parsed = parseAsset(data);
665
+ const edges = collectForwardEdges(parsed);
666
+ const idx = await buildGUIDIndex(projectRoot, new Set(edges.map((e) => e.guid)));
667
+ resolveForwardEdges(edges, idx);
668
+ edgeCache.set(path, edges);
669
+ return edges;
670
+ }
671
+ catch {
672
+ edgeCache.set(path, []);
673
+ return [];
674
+ }
675
+ }
676
+ const visiting = new Set();
677
+ async function dfs(current, trail) {
678
+ if (trail.length > maxDepth)
679
+ return;
680
+ const edges = await edgesOf(current);
681
+ for (const edge of edges) {
682
+ if (!edge.resolved || edge.assetPath === "")
683
+ continue;
684
+ if (edge.assetPath === startPath) {
685
+ cycles.push([...trail, startPath]);
686
+ continue;
687
+ }
688
+ if (visiting.has(edge.assetPath))
689
+ continue;
690
+ visiting.add(edge.assetPath);
691
+ await dfs(edge.assetPath, [...trail, edge.assetPath]);
692
+ visiting.delete(edge.assetPath);
693
+ }
694
+ }
695
+ for (const edge of startEdges) {
696
+ if (!edge.resolved || edge.assetPath === "" || edge.assetPath === startPath)
697
+ continue;
698
+ visiting.clear();
699
+ visiting.add(edge.assetPath);
700
+ await dfs(edge.assetPath, [startPath, edge.assetPath]);
701
+ }
702
+ return cycles;
703
+ }
704
+ /** Transitive impact closure via BFS over reverse edges. */
705
+ async function computeTransitiveImpact(startGuid, startPath, projectRoot, maxDepth) {
706
+ const affected = [];
707
+ const seen = new Set([startPath]);
708
+ let frontier = [];
709
+ let truncated = false;
710
+ const seed = await findReferencesOffline({
711
+ assetPath: startPath || undefined,
712
+ guid: startGuid,
713
+ detail: "normal",
714
+ projectRoot,
715
+ });
716
+ for (const e of seed.referencedBy) {
717
+ if (seen.has(e.assetPath))
718
+ continue;
719
+ seen.add(e.assetPath);
720
+ frontier.push(e.assetPath);
721
+ affected.push({ assetPath: e.assetPath, depth: 1 });
722
+ }
723
+ for (let depth = 2; depth <= maxDepth; depth++) {
724
+ if (frontier.length === 0)
725
+ break;
726
+ const next = [];
727
+ for (const nodePath of frontier) {
728
+ const nodeGuid = await safeReadMetaGUID(join(projectRoot, nodePath + ".meta"));
729
+ if (nodeGuid === "")
730
+ continue;
731
+ const res = await findReferencesOffline({
732
+ guid: nodeGuid,
733
+ detail: "normal",
734
+ projectRoot,
735
+ });
736
+ for (const e of res.referencedBy) {
737
+ if (seen.has(e.assetPath))
738
+ continue;
739
+ seen.add(e.assetPath);
740
+ next.push(e.assetPath);
741
+ affected.push({ assetPath: e.assetPath, depth });
742
+ }
743
+ }
744
+ frontier = next;
745
+ if (depth === maxDepth && frontier.length > 0) {
746
+ truncated = true;
747
+ }
748
+ }
749
+ return {
750
+ affected,
751
+ affectedCount: affected.length,
752
+ maxDepth,
753
+ truncated,
754
+ };
755
+ }
756
+ export async function scanIntegrityOffline(opts) {
757
+ const issues = [];
758
+ const assetsDir = join(opts.projectRoot, "Assets");
759
+ const guidByPath = new Map();
760
+ const allAssetPaths = new Set();
761
+ const assetFiles = await collectFiles(assetsDir);
762
+ for (const absPath of assetFiles) {
763
+ const assetPath = toAssetPath(opts.projectRoot, absPath);
764
+ allAssetPaths.add(assetPath);
765
+ const metaGuid = await safeReadMetaGUID(absPath + ".meta");
766
+ if (metaGuid !== "") {
767
+ guidByPath.set(assetPath, metaGuid);
768
+ }
769
+ }
770
+ const metaOnlyPaths = new Set();
771
+ await walkMeta(assetsDir, async (metaPath) => {
772
+ const assetPath = toAssetPath(opts.projectRoot, metaPath.slice(0, -5));
773
+ if (!allAssetPaths.has(assetPath)) {
774
+ metaOnlyPaths.add(assetPath);
775
+ }
776
+ });
777
+ // ---- orphan_meta: .meta whose companion asset is gone ----
778
+ for (const path of metaOnlyPaths) {
779
+ issues.push({
780
+ path: path + ".meta",
781
+ code: "orphan_meta",
782
+ severity: "warning",
783
+ detail: `meta file has no companion asset at ${path}`,
784
+ });
785
+ }
786
+ // ---- duplicate_guid: GUID shared by 2+ assets ----
787
+ const pathsByGuid = new Map();
788
+ for (const [assetPath, guid] of guidByPath) {
789
+ const list = pathsByGuid.get(guid);
790
+ if (list)
791
+ list.push(assetPath);
792
+ else
793
+ pathsByGuid.set(guid, [assetPath]);
794
+ }
795
+ for (const [guid, paths] of pathsByGuid) {
796
+ if (paths.length < 2)
797
+ continue;
798
+ for (const p of paths) {
799
+ issues.push({
800
+ path: p,
801
+ code: "duplicate_guid",
802
+ severity: "error",
803
+ detail: `guid ${guid} shared by ${paths.length} assets`,
804
+ relatedPaths: paths.filter((other) => other !== p),
805
+ });
806
+ }
807
+ }
808
+ // ---- aggregated missing references (the per-read check, project-wide) ----
809
+ const fullGuidIndex = await buildGUIDIndex(opts.projectRoot);
810
+ for (const absPath of assetFiles) {
811
+ const ext = extname(absPath).toLowerCase();
812
+ if (!OFFLINE_PARSEABLE_EXTENSIONS.has(ext))
813
+ continue;
814
+ const assetPath = toAssetPath(opts.projectRoot, absPath);
815
+ let data;
816
+ try {
817
+ data = await readFile(absPath, "utf-8");
818
+ }
819
+ catch {
820
+ continue;
821
+ }
822
+ if (!data.includes("guid:"))
823
+ continue;
824
+ try {
825
+ const parsed = parseAsset(data);
826
+ parsed.path = assetPath;
827
+ const perAsset = runYamlIntegrityChecks(parsed, fullGuidIndex);
828
+ for (const issue of perAsset) {
829
+ issues.push({
830
+ path: assetPath,
831
+ code: issue.code,
832
+ severity: issue.severity,
833
+ detail: issue.detail,
834
+ });
835
+ }
836
+ }
837
+ catch {
838
+ // Not parseable as YAML — skip. JSON integrity is per-read only.
839
+ }
840
+ }
841
+ // ---- group by code ----
842
+ const byCode = {};
843
+ for (const issue of issues) {
844
+ byCode[issue.code] = (byCode[issue.code] ?? 0) + 1;
845
+ }
846
+ return {
847
+ issues,
848
+ byCode,
849
+ totalIssues: issues.length,
850
+ assetsScanned: allAssetPaths.size,
851
+ // Inline tag is load-bearing — see the note on IntegrityScanResult._source.
852
+ _source: "offline",
853
+ };
854
+ }
855
+ //# sourceMappingURL=api.js.map