@allansantos-dev/smart-tool 0.0.0-stage → 0.9.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 (77) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +32 -0
  4. package/README.md +116 -3
  5. package/atomic_io.py +94 -0
  6. package/autostart.py +199 -0
  7. package/bin/smart-tool.mjs +82 -0
  8. package/browser_service_client.py +232 -0
  9. package/capabilities.py +63 -0
  10. package/client_hooks.py +192 -0
  11. package/code_graph.py +403 -0
  12. package/code_graph_java.mjs +49 -0
  13. package/code_graph_js.cjs +135 -0
  14. package/config.py +241 -0
  15. package/daemon_launcher.py +165 -0
  16. package/directory_watch.py +131 -0
  17. package/document_text.py +69 -0
  18. package/duplicates.py +313 -0
  19. package/embedding_cache.py +99 -0
  20. package/endpoint_sync.py +179 -0
  21. package/folder_picker.py +73 -0
  22. package/freshness.py +278 -0
  23. package/gateway.py +220 -0
  24. package/hook_decision.py +96 -0
  25. package/hooks/pretool_router.py +45 -0
  26. package/index_inventory.py +75 -0
  27. package/index_profile.py +167 -0
  28. package/index_scope.py +458 -0
  29. package/index_views.py +121 -0
  30. package/indexer.py +878 -0
  31. package/install.py +251 -0
  32. package/install_runtime.py +249 -0
  33. package/integration_metrics.py +82 -0
  34. package/local_embedder.py +172 -0
  35. package/model_adapters/__init__.py +0 -0
  36. package/model_adapters/openai_compatible.py +57 -0
  37. package/model_catalog.py +74 -0
  38. package/model_client.py +38 -0
  39. package/model_defaults.py +70 -0
  40. package/package.json +35 -6
  41. package/paths.py +6 -0
  42. package/project_identity.py +42 -0
  43. package/project_insights_ui.py +38 -0
  44. package/project_maps_ui.py +96 -0
  45. package/project_monitor.py +204 -0
  46. package/project_scheduler.py +61 -0
  47. package/project_storage.py +165 -0
  48. package/project_store.py +220 -0
  49. package/projects_ui.py +146 -0
  50. package/requirements.txt +8 -0
  51. package/research_cache.py +171 -0
  52. package/research_metrics.py +110 -0
  53. package/router.py +582 -0
  54. package/secret_store.py +121 -0
  55. package/setup_ui.py +986 -0
  56. package/site_memory.py +53 -0
  57. package/smart_tool_daemon.py +3604 -0
  58. package/tray.py +245 -0
  59. package/update_check.py +22 -0
  60. package/usage_meter.py +124 -0
  61. package/version.py +18 -0
  62. package/view_compare.py +68 -0
  63. package/web_adapters/browser/browser_service.py +302 -0
  64. package/web_adapters/browser/crawl4ai_search.py +236 -0
  65. package/web_adapters/browser/requirements.txt +2 -0
  66. package/web_adapters/node/adapter-common.mjs +40 -0
  67. package/web_adapters/node/ddg-search.mjs +89 -0
  68. package/web_adapters/node/open-websearch-adapter.mjs +77 -0
  69. package/web_adapters/node/package-lock.json +2720 -0
  70. package/web_adapters/node/package.json +12 -0
  71. package/web_adapters/node/wikipedia-search.mjs +44 -0
  72. package/web_document_graph.py +71 -0
  73. package/web_fetch.py +242 -0
  74. package/web_search_adapters.py +193 -0
  75. package/web_search_ddgs.py +42 -0
  76. package/web_search_external.py +288 -0
  77. package/web_search_health.py +88 -0
package/code_graph.py ADDED
@@ -0,0 +1,403 @@
1
+ """Grafos estáticos do conteúdo indexado: cobertura, imports e chamadas resolvidas."""
2
+ import ast
3
+ import collections
4
+ import copy
5
+ import contextvars
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import posixpath
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ from pathlib import Path
14
+
15
+ import indexer
16
+ import index_inventory
17
+ import project_identity
18
+ import web_search_adapters
19
+ import web_document_graph
20
+ import document_text
21
+
22
+ VERSION = 2
23
+ MAX_FILES = 3000
24
+ MAX_TEXT = 24 * 1024 * 1024
25
+ MAX_SYMBOLS = 8000
26
+ MAX_EDGES = 20000
27
+ _CACHE = collections.OrderedDict()
28
+ _LOCK = threading.Lock()
29
+ _ANALYSIS_SLOTS = threading.BoundedSemaphore(2)
30
+ _WARM_SLOT = threading.BoundedSemaphore(1)
31
+ _WARMING = set()
32
+ _WARM_STATE = {}
33
+ WARM_RETRY_S = 600
34
+
35
+
36
+ def reconstruct(rows):
37
+ """Remove sobreposição de chunks e recusa buracos/conflitos; mantém linhas reais."""
38
+ lines = {}
39
+ previous = None
40
+ for start, end, text in rows:
41
+ parts = text.splitlines(keepends=True)
42
+ if len(parts) != end-start+1:
43
+ return None, 'Chunks do not reconstruct the full lines of the file.'
44
+ for offset, part in enumerate(parts):
45
+ line = start+offset
46
+ if line in lines and start == end == line and previous and previous[0] == previous[1] == line and not previous[2].endswith(('\n','\r')):
47
+ lines[line] += part
48
+ elif line in lines and lines[line] != part:
49
+ return None, 'Overlapping chunks contain conflicting versions.'
50
+ else:
51
+ lines[line] = part
52
+ previous = (start,end,text)
53
+ if not lines:
54
+ return '', None
55
+ if min(lines) != 1 or len(lines) != max(lines):
56
+ return None, 'The snapshot has gaps; relations were not inferred for this file.'
57
+ return ''.join(lines[i] for i in range(1,max(lines)+1)), None
58
+
59
+
60
+ def _snapshot(path):
61
+ conn = indexer._readonly(path)
62
+ try:
63
+ conn.execute('BEGIN')
64
+ columns = {row[1] for row in conn.execute('PRAGMA table_info(manifest)')}
65
+ size = 'm.size' if 'size' in columns else 'NULL'
66
+ mtime = 'm.mtime_ns' if 'mtime_ns' in columns else 'NULL'
67
+ records = conn.execute(f'SELECT m.path,m.hash,{size},{mtime},count(c.id),max(c.end_line) FROM manifest m LEFT JOIN chunks c ON c.path=m.path GROUP BY m.path ORDER BY m.path LIMIT ?', (MAX_FILES+1,)).fetchall()
68
+ files, sources, diagnostics, used = [], {}, [], 0
69
+ for original, digest, byte_size, stamp, chunks, line_count in records[:MAX_FILES]:
70
+ file_path = original.replace('\\','/')
71
+ record = {'id':file_path,'path':file_path,'hash':digest,'size':byte_size,'mtime_ns':stamp,
72
+ 'chunks':chunks,'lines':line_count or 0,'group':posixpath.dirname(file_path) or '(root)',
73
+ 'location_kind':document_text.location_kind(file_path),'format':posixpath.splitext(file_path)[1].lower().lstrip('.') or 'text'}
74
+ files.append(record)
75
+ if not file_path.lower().endswith(('.py','.java','.js','.jsx','.ts','.tsx','.mjs','.cjs','.mts','.cts','.json','.html','.htm','.css','.scss','.sass','.less','.md','.markdown')):
76
+ continue
77
+ rows = conn.execute('SELECT start_line,end_line,text FROM chunks WHERE path=? ORDER BY start_line,id', (original,)).fetchall()
78
+ text, error = reconstruct(rows)
79
+ if error:
80
+ diagnostics.append({'path':file_path,'reason':error})
81
+ elif used+len(text) > MAX_TEXT:
82
+ diagnostics.append({'path':file_path,'reason':'Content limit for this analysis reached.'})
83
+ else:
84
+ used += len(text)
85
+ sources[file_path] = text
86
+ return files, sources, diagnostics, len(records)>MAX_FILES
87
+ finally:
88
+ conn.close()
89
+
90
+
91
+ def _python_graph(sources):
92
+ trees, symbols, dependencies, calls, unresolved, diagnostics = {}, [], [], [], [], []
93
+ decls, aliases, scopes, classes, owners, shadow_scopes, node_symbols = {}, {}, {}, {}, {}, {}, {}
94
+ def bind_import(path,scope,name,binding):
95
+ table=aliases[path].setdefault(scope,{})
96
+ table[name]=binding if name not in table or table[name]==binding else None
97
+ def resolve_file(spec):
98
+ spec=posixpath.normpath(spec)
99
+ return next((p for p in (spec+'.py',spec+'/__init__.py','src/'+spec+'.py','src/'+spec+'/__init__.py') if p in sources), None)
100
+ for path, text in sources.items():
101
+ if not path.endswith('.py'):
102
+ continue
103
+ try:
104
+ tree=ast.parse(text,filename=path)
105
+ except (SyntaxError,ValueError,RecursionError) as exc:
106
+ diagnostics.append({'path':path,'reason':'Incomplete or invalid Python syntax in the snapshot.','line':getattr(exc,'lineno',None)})
107
+ continue
108
+ trees[path]=tree;aliases[path]={};decls[path]={};classes[path]={}
109
+ def collect(node,qual=(),owner=None):
110
+ if isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef)):
111
+ qual=qual+(node.name,)
112
+ sym={'id':path+'::'+'.'.join(qual)+'@'+str(node.lineno),'path':path,'name':'.'.join(qual),
113
+ 'start_line':node.lineno,'end_line':node.end_lineno,'kind':'class' if isinstance(node,ast.ClassDef) else 'function', 'language':'python'}
114
+ decls[path][qual]=sym if qual not in decls[path] else None
115
+ scopes[id(node)]=qual;node_symbols[id(node)]=sym
116
+ if isinstance(node,ast.ClassDef):classes[path][qual]=node
117
+ if len(symbols)<MAX_SYMBOLS:symbols.append(sym)
118
+ owner=sym
119
+ owners[id(node)]=qual
120
+ for child in ast.iter_child_nodes(node):collect(child,qual,owner)
121
+ collect(tree)
122
+ for node in ast.walk(tree):
123
+ if isinstance(node,ast.Import):
124
+ for item in node.names:
125
+ spec=item.name.replace('.','/');target=resolve_file(spec)
126
+ bind_import(path,owners[id(node)],item.asname or item.name,(target,None))
127
+ dependencies.append({'source':path,'target':target,'specifier':item.name,'line':node.lineno,'kind':'import','resolution':'resolved' if target else 'external'})
128
+ elif isinstance(node,ast.ImportFrom):
129
+ prefix=posixpath.dirname(path)
130
+ if node.level:
131
+ for _ in range(node.level-1):prefix=posixpath.dirname(prefix)
132
+ spec=posixpath.join(prefix,(node.module or '').replace('.','/'))
133
+ else:spec=(node.module or '').replace('.','/')
134
+ base=resolve_file(spec)
135
+ for item in node.names:
136
+ target=resolve_file(spec+'/'+item.name) if item.name!='*' else None
137
+ target=target or base
138
+ bind_import(path,owners[id(node)],item.asname or item.name,(target,None if target and target!=base else item.name))
139
+ dependencies.append({'source':path,'target':target,'specifier':('.'*node.level)+(node.module or '')+'.'+item.name,'line':node.lineno,'kind':'import','resolution':'resolved' if target else 'unresolved' if node.level else 'external'})
140
+ def chain(expr):
141
+ if isinstance(expr,ast.Name):return expr.id
142
+ if isinstance(expr,ast.Attribute):
143
+ base=chain(expr.value)
144
+ return base+'.'+expr.attr if base else None
145
+ return None
146
+ def resolve(path,qual,name,shadow,instances):
147
+ if not name:return None
148
+ parts=name.split('.')
149
+ if parts[0] in instances:
150
+ typ=instances[parts[0]]
151
+ return decls.get(typ['path'],{}).get(tuple(typ['name'].split('.'))+tuple(parts[1:]))
152
+ if parts[0] in ('self','cls') and len(parts)>1:
153
+ for i in range(len(qual),0,-1):
154
+ if qual[:i] in classes[path]:return decls[path].get(qual[:i]+tuple(parts[1:]))
155
+ if parts[0] in shadow:return None
156
+ for i in range(len(qual),-1,-1):
157
+ if i<len(qual) and qual[:i] in classes[path]:
158
+ continue # O namespace da classe não é escopo lexical de seus métodos.
159
+ if parts[0] in shadow_scopes.get((path,qual[:i]),set()):
160
+ return None
161
+ if qual[:i]+tuple(parts) in decls[path]:
162
+ return decls[path][qual[:i]+tuple(parts)]
163
+ for length in range(len(parts),0,-1):
164
+ table=aliases[path].get(qual[:i],{})
165
+ binding=table.get('.'.join(parts[:length]))
166
+ if '.'.join(parts[:length]) in table:
167
+ if binding is None:return None
168
+ target_path,imported=binding
169
+ rest=(([imported] if imported and imported!='*' else [])+parts[length:])
170
+ return decls.get(target_path,{}).get(tuple(rest))
171
+ return None
172
+ for path,tree in trees.items():
173
+ module={'id':path+'::<module>','path':path,'name':'<module>','start_line':1,'end_line':1,'kind':'module','language':'python'}
174
+ used_module=False
175
+ def visit_scope(node,qual=()):
176
+ nonlocal used_module
177
+ owner=node_symbols.get(id(node),module)
178
+ shadow=set();instances={}
179
+ args=getattr(node,'args',None)
180
+ if args:
181
+ params=[*args.posonlyargs,*args.args,*args.kwonlyargs]+([args.vararg] if args.vararg else [])+([args.kwarg] if args.kwarg else [])
182
+ shadow.update(arg.arg for arg in params)
183
+ for arg in params:
184
+ target=resolve(path,qual,chain(arg.annotation),set(),{}) if arg.annotation else None
185
+ if target and target['kind']=='class':instances[arg.arg]=target
186
+ # Levanta ligações locais sem atravessar escopos aninhados.
187
+ def local_nodes(n):
188
+ for child in ast.iter_child_nodes(n):
189
+ if isinstance(child,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef,ast.Lambda)):
190
+ continue
191
+ yield child
192
+ yield from local_nodes(child)
193
+ local=list(local_nodes(node))
194
+ assigned={}
195
+ for n in local:
196
+ if isinstance(n,(ast.Assign,ast.AnnAssign)):
197
+ targets=n.targets if isinstance(n,ast.Assign) else [n.target]
198
+ for target in targets:
199
+ if isinstance(target,ast.Name):
200
+ shadow.add(target.id);assigned.setdefault(target.id,[]).append(n.value)
201
+ for name,values in assigned.items():
202
+ if len(values)==1 and isinstance(values[0],ast.Call):
203
+ typ=resolve(path,qual,chain(values[0].func),shadow-set([name]),instances)
204
+ if typ and typ['kind']=='class':instances[name]=typ
205
+ shadow_scopes[(path,qual)]=shadow
206
+ for n in local:
207
+ if not isinstance(n,ast.Call):continue
208
+ expr=chain(n.func)
209
+ target=resolve(path,qual,expr,shadow,instances)
210
+ if target:
211
+ if owner is module:used_module=True
212
+ if len(calls)<MAX_EDGES:calls.append({'source':owner['id'],'target':target['id'],'line':n.lineno,'kind':'construct' if target['kind']=='class' else 'call','resolution':'static'})
213
+ elif len(unresolved)<1000:
214
+ unresolved.append({'path':path,'line':n.lineno,'expression':expr or '<dynamic expression>','reason':'External, dynamic or unresolved target in this snapshot.'})
215
+ for child in ast.iter_child_nodes(node):
216
+ if isinstance(child,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef)):
217
+ visit_scope(child,scopes[id(child)])
218
+ else:
219
+ # Definições em blocos if/try também podem declarar escopos.
220
+ def nested(n):
221
+ for c in ast.iter_child_nodes(n):
222
+ if isinstance(c,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef)):visit_scope(c,scopes[id(c)])
223
+ else:nested(c)
224
+ nested(child)
225
+ visit_scope(tree)
226
+ if used_module:symbols.append(module)
227
+ return {'symbols':symbols,'dependencies':dependencies[:MAX_EDGES],'calls':calls,'unresolved':unresolved,
228
+ 'diagnostics':diagnostics,'parsed':list(trees),'truncated':len(symbols)>=MAX_SYMBOLS or len(dependencies)>MAX_EDGES}
229
+
230
+
231
+ def _javascript_graph(sources, all_paths=None):
232
+ extensions=('.js','.jsx','.ts','.tsx','.mjs','.cjs','.mts','.cts')
233
+ relevant={path:text for path,text in sources.items() if path.endswith(extensions+('.json','.html','.htm'))}
234
+ if not any(path.endswith(extensions) for path in relevant):
235
+ return {}
236
+ try:
237
+ process=subprocess.run([web_search_adapters.NODE_EXECUTABLE,'--max-old-space-size=512',str(Path(__file__).with_name('code_graph_js.cjs'))],
238
+ input=json.dumps({'files':[{'path':path,'text':text} for path,text in relevant.items()],'paths':list(all_paths or sources)},ensure_ascii=False),
239
+ capture_output=True,text=True,encoding='utf-8',timeout=40,
240
+ creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0))
241
+ if process.returncode:
242
+ raise RuntimeError('The TypeScript analyzer did not finish. Check the local code-analysis runtime.')
243
+ return json.loads(process.stdout)
244
+ except (OSError,subprocess.TimeoutExpired,ValueError,RuntimeError) as exc:
245
+ return {'diagnostics':[{'path':'JavaScript/TypeScript','reason':str(exc)[:250]}],'parsed':[],'retryable':True}
246
+
247
+
248
+ def _java_graph(sources):
249
+ files=[{'path':path,'text':text} for path,text in sources.items() if path.lower().endswith('.java')]
250
+ if not files:return {}
251
+ try:
252
+ result=subprocess.run([web_search_adapters.NODE_EXECUTABLE,'--max-old-space-size=512',str(Path(__file__).with_name('code_graph_java.mjs'))],
253
+ input=json.dumps({'files':files},ensure_ascii=False),capture_output=True,text=True,encoding='utf-8',timeout=40,
254
+ creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0))
255
+ if result.returncode:raise RuntimeError('The Java analyzer did not finish. Check the local runtime.')
256
+ return json.loads(result.stdout)
257
+ except (OSError,ValueError,RuntimeError,subprocess.TimeoutExpired) as exc:
258
+ return {'diagnostics':[{'path':'Java','reason':str(exc)[:250]}],'parsed':[],'retryable':True}
259
+
260
+
261
+ def _analyze(path):
262
+ files,sources,diagnostics,truncated=_snapshot(path)
263
+ merged={'symbols':[],'dependencies':[],'calls':[],'unresolved':[],'diagnostics':diagnostics,'parsed':[]}
264
+ retryable=False
265
+ for output in (_python_graph(sources),_javascript_graph(sources,[f['path'] for f in files]),_java_graph(sources),web_document_graph.analyze(sources,[f['path'] for f in files])):
266
+ for key in merged:merged[key].extend(output.get(key,[]))
267
+ truncated |= output.get('truncated',False)
268
+ retryable |= output.get('retryable',False)
269
+ by_id={sym['id']:sym for sym in merged['symbols'][:MAX_SYMBOLS]}
270
+ # Relações repetidas preservam contagem e locais de chamada sem poluir o desenho.
271
+ def collapse(rows,fields):
272
+ result={}
273
+ for row in rows:
274
+ key=tuple(row.get(field) for field in fields)
275
+ if key not in result:result[key]={**row,'count':0,'lines':[]}
276
+ result[key]['count']+=1
277
+ if len(result[key]['lines'])<20:result[key]['lines'].append(row.get('line'))
278
+ return list(result.values())
279
+ deps=collapse(merged['dependencies'],('source','target','specifier','kind'))
280
+ calls=collapse([r for r in merged['calls'] if r['source'] in by_id and r['target'] in by_id],('source','target','kind'))
281
+ supported=set(merged['parsed'])
282
+ for file in files:file['analysis']='parsed' if file['path'] in supported else 'not_analyzed'
283
+ return {'files':files,'symbols':merged['symbols'][:MAX_SYMBOLS],
284
+ 'dependencies':deps[:MAX_EDGES],'calls':calls[:MAX_EDGES],
285
+ 'unresolved':merged['unresolved'][:1000],'diagnostics':merged['diagnostics'],
286
+ 'truncated':truncated or len(deps)>MAX_EDGES or len(calls)>MAX_EDGES,'retryable':retryable,
287
+ 'counts':{'files':len(files),'parsed_files':len(supported),'symbols':len(merged['symbols']),
288
+ 'resolved_imports':sum(bool(r['target']) for r in deps),
289
+ 'external_imports':sum(r['resolution']=='external' for r in deps),
290
+ 'unresolved_calls':len(merged['unresolved']),'calls':len(calls)}}
291
+
292
+
293
+ def _focus(data,file_path):
294
+ if not isinstance(file_path,str):
295
+ raise ValueError('file_path must be a string.')
296
+ target=file_path.replace('\\','/').removeprefix('./')
297
+ files=[f for f in data['files'] if f['path']==target]
298
+ if not files:
299
+ raise ValueError(f'File is outside the indexed view: {file_path}')
300
+ by_id={s['id']:s for s in data['symbols']}
301
+ own={s['id'] for s in data['symbols'] if s.get('path')==target}
302
+ calls=[c for c in data['calls'] if c['source'] in own or c['target'] in own]
303
+ linked=own|{c['source'] for c in calls}|{c['target'] for c in calls}
304
+ deps=[d for d in data['dependencies'] if target in (d['source'],d['target'])]
305
+ paths={f['path'] for f in data['files']}
306
+ data.update(files=files,symbols=[by_id[i] for i in linked if i in by_id],dependencies=deps,calls=calls,
307
+ unresolved=[u for u in data['unresolved'] if u.get('path')==target],
308
+ diagnostics=[d for d in data['diagnostics'] if d.get('path')==target or d.get('path') not in paths],
309
+ focus={'file_path':target,'symbols':len(own),'imported_by':sum(d['target']==target for d in deps),
310
+ 'imports':sum(d['source']==target for d in deps),
311
+ 'incoming_calls':sum(c['target'] in own and c['source'] not in own for c in calls),
312
+ 'outgoing_calls':sum(c['source'] in own and c['target'] not in own for c in calls)})
313
+ return data
314
+
315
+
316
+ def _by_file(data):
317
+ by_file={}
318
+ for symbol in data['symbols']:
319
+ by_file.setdefault(symbol['path'],[]).append(symbol)
320
+ parsed={f['path'] for f in data['files'] if f.get('analysis')=='parsed'}
321
+ note='Graph truncated by analysis limits; some files have no functions.' if data.get('truncated') else None
322
+ return {'symbols':by_file,'parsed':parsed,'note':note}
323
+
324
+
325
+ def cached_symbols(root):
326
+ path=indexer.existing_db_path(root)
327
+ if not path:
328
+ return None,None
329
+ stat=os.stat(path);key=(path,stat.st_mtime_ns,stat.st_size,VERSION)
330
+ with _LOCK:
331
+ data=_CACHE.get(key)
332
+ if data is not None:
333
+ _CACHE.move_to_end(key)
334
+ state=_WARM_STATE.get(root)
335
+ if data is None and state and state['key']==key and (state['result'] or time.time()-state['at']<WARM_RETRY_S):
336
+ return state['result'],state['note']
337
+ start=data is None and root not in _WARMING
338
+ if start:_WARMING.add(root)
339
+ if data is not None:
340
+ return _by_file(data),None
341
+ if start:
342
+ context=contextvars.copy_context()
343
+ threading.Thread(target=context.run,args=(_warm,root,key),daemon=True).start()
344
+ return None,None
345
+
346
+
347
+ def _warm(root,key):
348
+ result,note=None,None
349
+ try:
350
+ data=build(root,background=True)
351
+ if data.get('retryable'):
352
+ result=_by_file(data)
353
+ reasons='; '.join(d.get('reason','') for d in data.get('diagnostics',[])[:2])
354
+ note=f'Partial analysis: {reasons}'[:300]
355
+ except Exception as exc:
356
+ note=f'The last analysis failed: {type(exc).__name__}: {exc}'[:300]
357
+ finally:
358
+ with _LOCK:
359
+ if note:_WARM_STATE[root]={'key':key,'at':time.time(),'result':result,'note':note}
360
+ else:_WARM_STATE.pop(root,None)
361
+ _WARMING.discard(root)
362
+
363
+
364
+ def build(root,view_id=None,storage_id=None,file_path=None,background=False):
365
+ inventory=index_inventory.inspect(root,view_id,storage_id=storage_id)
366
+ if not inventory['selected']:
367
+ return {**inventory,'symbols':[],'dependencies':[],'calls':[],'diagnostics':[],'counts':{}}
368
+ path=inventory['selected']['path'];stat=os.stat(path)
369
+ key=(path,stat.st_mtime_ns,stat.st_size,VERSION)
370
+ with _LOCK:
371
+ cached=_CACHE.get(key)
372
+ if cached:_CACHE.move_to_end(key)
373
+ was_cached=cached is not None
374
+ if cached is None:
375
+ slots=_WARM_SLOT if background else _ANALYSIS_SLOTS
376
+ if not slots.acquire(timeout=None if background else 3):
377
+ raise RuntimeError('Two map analyses are in progress. Wait for them to finish and reload.')
378
+ try:
379
+ cached=_analyze(path)
380
+ finally:
381
+ slots.release()
382
+ after=os.stat(path)
383
+ if not cached['retryable'] and (after.st_mtime_ns,after.st_size)==(stat.st_mtime_ns,stat.st_size):
384
+ with _LOCK:
385
+ _CACHE[key]=cached
386
+ while len(_CACHE)>3:_CACHE.popitem(last=False)
387
+ data=copy.deepcopy(cached)
388
+ # Só compara metadados do diretório de trabalho quando a visão é a atual.
389
+ for file in data['files']:
390
+ file['status']='stored'
391
+ if inventory['selected']['current'] and file.get('mtime_ns'):
392
+ candidate=os.path.join(root,file['path'])
393
+ if project_identity.within_root(root,candidate):
394
+ try:
395
+ current=os.stat(candidate)
396
+ file['status']='indexed' if (current.st_mtime_ns,current.st_size)==(file['mtime_ns'],file['size']) else 'pending'
397
+ except OSError:file['status']='missing'
398
+ file.pop('mtime_ns',None)
399
+ data.update(views=inventory['views'],selected=inventory['selected'],current_view=inventory['current_view'],
400
+ message=inventory['message'],read_only=True,source='indexed_snapshot',generated_at=time.time(),
401
+ limits={'files':MAX_FILES,'symbols':MAX_SYMBOLS,'edges':MAX_EDGES,'display_nodes_default':120},
402
+ languages=['Java','Angular','Python','JavaScript','TypeScript','JSX','TSX','HTML','CSS','Markdown'],cache_hit=was_cached)
403
+ return _focus(data,file_path) if file_path else data
@@ -0,0 +1,49 @@
1
+ // Análise estática conservadora de Java a partir de snapshots; nenhum código é executado.
2
+ import fs from 'node:fs';
3
+ import {parse} from './web_adapters/node/node_modules/java-parser/src/index.js';
4
+ const input=JSON.parse(fs.readFileSync(0,'utf8'));
5
+ const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],units=[],classes=new Map(),methodNodes=new Map();
6
+ const MAX_SYMBOLS=8000,MAX_EDGES=20000;
7
+ function children(n){return n?.children?Object.values(n.children).flat().filter(x=>x.children):[]}
8
+ function all(n,name,stop=false){const result=[];function visit(node){if(node?.name===name){result.push(node);if(stop)return}for(const child of children(node))visit(child)}visit(n);return result}
9
+ function first(n,name){return all(n,name,true)[0]}
10
+ function identifiers(n){const out=[];function visit(node){if(node?.children)for(const [key,items] of Object.entries(node.children))for(const child of items){if(child.children)visit(child);else if(key==='Identifier')out.push(child)}}visit(n);return out.sort((a,b)=>a.startOffset-b.startOffset)}
11
+ function text(u,n){return n?.location?u.text.slice(n.location.startOffset,n.location.endOffset+1):''}
12
+ function addSymbol(u,n,name,kind){if(symbols.length>=MAX_SYMBOLS)return null;const s={id:u.path+'::'+name+'@'+n.location.startLine+':'+n.location.startOffset,path:u.path,name,kind,language:'java',start_line:n.location.startLine,end_line:n.location.endLine};symbols.push(s);return s}
13
+ const classKinds=new Set(['normalClassDeclaration','normalInterfaceDeclaration','enumDeclaration','recordDeclaration']);
14
+ for(const file of input.files){try{const tree=parse(file.text),pkg=first(tree,'packageDeclaration'),unit={...file,tree,package:pkg?identifiers(pkg).map(t=>t.image).join('.'): '',imports:[],classes:[]};
15
+ for(const n of all(tree,'importDeclaration')){const raw=text(unit,n).replace(/^import\s+/,'').replace(/;\s*$/,'').trim(),stat=raw.startsWith('static '),spec=(stat?raw.slice(7):raw).replace(/\s+/g,'');unit.imports.push({spec,static:stat,line:n.location.startLine})}
16
+ function scan(n,parent=null){let current=parent;if(classKinds.has(n.name)){const id=first(n,'typeIdentifier')||first(n,'typeName'),short=identifiers(id||n)[0]?.image;if(!short)return;const local=(parent?parent.local+'.':'')+short,fqn=(unit.package?unit.package+'.':'')+local;const symbol=addSymbol(unit,n,local,n.name==='normalInterfaceDeclaration'?'interface':'class');if(!symbol)return;current={unit,fqn,local,short,symbol,node:n,body:n.children.classBody?.[0]||n.children.interfaceBody?.[0]||n.children.recordBody?.[0]||n.children.enumBody?.[0],methods:[],fields:new Map(),extends:text(unit,n.children.classExtends?.[0]).replace(/^extends\s+/, '').trim()};classes.set(fqn,classes.has(fqn)?null:current);unit.classes.push(current)}
17
+ if(current&&n.name==='classBody'&&n!==current.body)return;
18
+ if(current&&['methodDeclaration','interfaceMethodDeclaration','constructorDeclaration','compactConstructorDeclaration'].includes(n.name)){const declarator=first(n,'methodDeclarator')||first(n,'constructorDeclarator'),ctor=n.name.includes('Constructor')||n.name==='constructorDeclaration',name=ctor?current.short:(declarator?.children.Identifier?.[0]?.image);if(name){const params=all(declarator||n,'formalParameter'),types=params.map(p=>text(unit,first(p,'unannType'))||'?');const signature=name+'('+types.join(',')+')';const sym=addSymbol(unit,n,current.local+'.'+signature,ctor?'constructor':'method');if(sym){sym.signature=signature;const method={symbol:sym,name,arity:params.length,types,node:n,cls:current,params,concrete:!!first(n,'block')||ctor};current.methods.push(method);methodNodes.set(n,method)}}}
19
+ for(const child of children(n))scan(child,current)}scan(tree);units.push(unit)
20
+ }catch{diagnostics.push({path:file.path,reason:'Invalid or fragmented Java, or syntax not supported by the parser.'})}}
21
+ function resolveType(unit,raw,context){let name=raw.replace(/@[\w.]+(?:\([^)]*\))?/g,'').replace(/<.*>/g,'').replace(/\[\]|\.\.\./g,'').trim();if(!name||['void','boolean','byte','short','int','long','float','double','char','var'].includes(name))return null;if(classes.has(name))return classes.get(name);const candidates=new Set();const local=[context?context.fqn+'.'+name:'',unit.package?unit.package+'.'+name:name];for(const fq of local)if(classes.has(fq))candidates.add(fq);for(const item of unit.imports.filter(i=>!i.static)){if(item.spec.endsWith('.'+name)&&classes.has(item.spec))candidates.add(item.spec);if(item.spec.endsWith('.*')){const fq=item.spec.slice(0,-1)+name;if(classes.has(fq))candidates.add(fq)}}return candidates.size===1?classes.get([...candidates][0]):null}
22
+ function lookupMethod(cls,name,arity,visited=new Set()){if(!cls||visited.has(cls.fqn))return null;visited.add(cls.fqn);const methods=cls.methods.filter(m=>m.name===name&&m.arity===arity&&m.concrete);if(methods.length===1)return methods[0].symbol;if(methods.length>1)return null;return cls.extends?lookupMethod(resolveType(cls.unit,cls.extends,cls),name,arity,visited):null}
23
+ function putBinding(map,name,type){if(name)map.set(name,map.has(name)&&map.get(name)!==type?null:type)}
24
+ function scoped(node,name){const out=[];function scan(n){if(n!==node&&(classKinds.has(n.name)||n.name==='lambdaExpression'||n.name==='classBody'))return;if(n.name===name)out.push(n);for(const c of children(n))scan(c)}scan(node);return out}
25
+ function collectVariables(unit,node,map){for(const parentName of ['fieldDeclaration','localVariableDeclaration','variableParaRegularParameter','variableArityParameter'])for(const declaration of scoped(node,parentName)){const type=text(unit,first(declaration,'unannType'));for(const id of all(declaration,'variableDeclaratorId'))putBinding(map,identifiers(id)[0]?.image,type)}}
26
+ for(const unit of units){for(const cl of unit.classes){const fieldNodes=scoped(cl.body,'fieldDeclaration');for(const node of fieldNodes){const type=text(unit,first(node,'unannType'));for(const id of all(node,'variableDeclaratorId'))putBinding(cl.fields,identifiers(id)[0]?.image,type)}}}
27
+ for(const unit of units){
28
+ for(const item of unit.imports){const spec=item.spec;let target=classes.get(item.static?spec.split('.').slice(0,-1).join('.'):spec);dependencies.push({source:unit.path,target:target?.unit.path||null,specifier:spec,line:item.line,kind:item.static?'java_static_import':'import',resolution:target?'resolved':'external'})}
29
+ function walk(n,cls=null,method=null,vars=new Map(),owner=null){if(classKinds.has(n.name))cls=unit.classes.find(c=>c.node===n)||cls;if(cls&&n.name==='classBody'&&n!==cls.body)return;
30
+ if(methodNodes.has(n)){method=methodNodes.get(n);vars=new Map(cls.fields);collectVariables(unit,n,vars);owner=method.symbol}
31
+ if(n.name==='lambdaExpression'){vars=new Map(vars);for(const param of identifiers(n.children.lambdaParameters?.[0]))vars.set(param.image,null);owner=addSymbol(unit,n,(cls?.local||'')+'.lambda@'+n.location.startLine,'function')||owner}
32
+ if(cls&&['unannType','classType','interfaceType'].includes(n.name)){const type=resolveType(unit,text(unit,n),cls);if(type&&type.unit.path!==unit.path&&dependencies.length<MAX_EDGES)dependencies.push({source:unit.path,target:type.unit.path,specifier:type.fqn,line:n.location.startLine,kind:'java_type',resolution:'resolved'})}
33
+ if(cls&&n.name==='primary'){
34
+ const prefix=n.children.primaryPrefix?.[0],prefixText=text(unit,prefix).trim(),suffixes=n.children.primarySuffix||[];let chain=prefixText,fluent=false;
35
+ const newMatch=/^new\s+([\w.$]+)/.exec(prefixText);let constructed=newMatch?resolveType(unit,newMatch[1],cls):null;
36
+ if(constructed&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:constructed.symbol.id,line:n.location.startLine,kind:'construct',resolution:'static'});
37
+ for(const suffix of suffixes){const invoke=suffix.children.methodInvocationSuffix?.[0];if(!invoke){chain+=text(unit,suffix);continue}const args=invoke.children.argumentList?.[0],arity=args?(args.children.expression||[]).length:0;const match=/^(?:([\w.$]+)\.)?([\w$]+)$/.exec(chain);let target=null;
38
+ if(match&&!fluent){const receiver=match[1],name=match[2];let type=null;if(!receiver||receiver==='this')type=cls;else if(receiver==='super')type=resolveType(unit,cls.extends,cls);else if(receiver.startsWith('this.'))type=resolveType(unit,cls.fields.get(receiver.slice(5))||'',cls);else if(vars.has(receiver))type=resolveType(unit,vars.get(receiver)||'',cls);else type=resolveType(unit,receiver,cls);target=lookupMethod(type,name,arity);if(!target&&!receiver){const candidates=unit.imports.filter(i=>i.static&&(i.spec.endsWith('.'+name)||i.spec.endsWith('.*'))).map(i=>lookupMethod(classes.get(i.spec.split('.').slice(0,-1).join('.')),name,arity)).filter(Boolean);if(candidates.length===1)target=candidates[0]}}
39
+ if(constructed&&!fluent){const methodName=/\.([\w$]+)$/.exec(chain)?.[1];if(methodName)target=lookupMethod(constructed,methodName,arity)}
40
+ if(target&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:target.id,line:invoke.location.startLine,kind:'call',resolution:'static'});
41
+ else if(unresolved.length<1000)unresolved.push({path:unit.path,line:invoke.location.startLine,expression:chain.slice(0,140),reason:'External/dynamic type, interface without implementation, or ambiguous overload in the Java snapshot.'});
42
+ fluent=true;chain+='<result>';
43
+ }
44
+ }
45
+ for(const child of children(n))walk(child,cls,method,vars,owner)
46
+ }walk(unit.tree)
47
+ }
48
+ const ids=new Set(symbols.map(s=>s.id));
49
+ process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,MAX_EDGES),calls:calls.filter(c=>ids.has(c.source)&&ids.has(c.target)),unresolved,diagnostics,parsed:units.map(u=>u.path),truncated:symbols.length>=MAX_SYMBOLS||dependencies.length>MAX_EDGES||calls.length>=MAX_EDGES}));
@@ -0,0 +1,135 @@
1
+ /* Análise estática de snapshots JS/TS. Não executa código nem lê arquivos do projeto. */
2
+ const fs = require('node:fs');
3
+ const path = require('node:path').posix;
4
+ const ts = require('./web_adapters/node/node_modules/typescript');
5
+ const input = JSON.parse(fs.readFileSync(0, 'utf8'));
6
+ const clean = name => path.normalize(name.replaceAll('\\', '/')).replace(/^\.\//, '');
7
+ const docs = new Map(input.files.map(f => ['/snapshot/' + clean(f.path), f]));
8
+ const knownFiles = new Set([...docs.keys(),...(input.paths||[]).map(p=>'/snapshot/'+clean(p))]);
9
+ const names = [...docs.keys()].filter(n => /\.(?:[cm]?[jt]s|[jt]sx)$/.test(n));
10
+ const options = {allowJs:true, checkJs:true, jsx:ts.JsxEmit.Preserve, target:ts.ScriptTarget.Latest,
11
+ module:ts.ModuleKind.ESNext, moduleResolution:ts.ModuleResolutionKind.Bundler,
12
+ noLib:true, noEmit:true, skipLibCheck:true, allowImportingTsExtensions:true};
13
+ let configPath = docs.has('/snapshot/tsconfig.json') ? '/snapshot/tsconfig.json' : '/snapshot/jsconfig.json';
14
+ const configSeen = new Set();
15
+ function readConfig(name, depth=0) {
16
+ if (depth>5 || configSeen.has(name) || !docs.has(name)) return {};
17
+ configSeen.add(name);
18
+ const raw = ts.parseConfigFileTextToJson(name, docs.get(name).text).config || {};
19
+ const base = typeof raw.extends==='string' && raw.extends.startsWith('.') ? readConfig(clean(path.join(path.dirname(name),raw.extends.endsWith('.json')?raw.extends:raw.extends+'.json')),depth+1) : {};
20
+ return {...base,...raw.compilerOptions};
21
+ }
22
+ const config=readConfig(configPath);
23
+ const aliases=config.paths||{},baseUrl=clean(path.join(path.dirname(configPath),config.baseUrl||'.'));
24
+ function candidate(base) {
25
+ const variants=[base,base.replace(/\.[cm]?jsx?$/,'.ts'),base.replace(/\.jsx?$/,'.tsx'),
26
+ ...['.ts','.tsx','.js','.jsx','.mts','.cts','.mjs','.cjs'].map(e=>base+e),
27
+ ...['.ts','.tsx','.js','.jsx'].map(e=>base+'/index'+e)];
28
+ return variants.find(name=>knownFiles.has(clean(name)));
29
+ }
30
+ function resolve(spec, from) {
31
+ if (spec.startsWith('.')) return candidate(clean(path.join(path.dirname(from),spec)));
32
+ for (const [pattern, targets] of Object.entries(aliases)) {
33
+ const [prefix,suffix='']=pattern.split('*');
34
+ if (pattern.includes('*') ? spec.startsWith(prefix)&&spec.endsWith(suffix) : spec===pattern) {
35
+ const middle=pattern.includes('*')?spec.slice(prefix.length,suffix? -suffix.length:undefined):'';
36
+ for (const target of Array.isArray(targets)?targets:[]) {
37
+ const found=candidate(clean(path.join(baseUrl,target.replace('*',middle))));
38
+ if(found)return found;
39
+ }
40
+ }
41
+ }
42
+ if(config.baseUrl)return candidate(clean(path.join(baseUrl,spec)));
43
+ return undefined;
44
+ }
45
+ const host={
46
+ getSourceFile:(name,version)=>docs.has(clean(name))?ts.createSourceFile(clean(name),docs.get(clean(name)).text,version,true):undefined,
47
+ getDefaultLibFileName:()=>'/lib.d.ts',writeFile:()=>{},getCurrentDirectory:()=>'/snapshot',
48
+ getDirectories:()=>[],fileExists:name=>docs.has(clean(name)),readFile:name=>docs.get(clean(name))?.text,
49
+ getCanonicalFileName:name=>clean(name),useCaseSensitiveFileNames:()=>true,getNewLine:()=> '\n',
50
+ resolveModuleNames:(modules,containing)=>modules.map(spec=>{const resolved=resolve(spec,containing);return resolved&&docs.has(resolved)&&/\.(?:[cm]?[jt]s|[jt]sx|json)$/.test(resolved)?{resolvedFileName:resolved,extension:ts.extensionFromPath(resolved),isExternalLibraryImport:false}:undefined;})
51
+ };
52
+ const program=ts.createProgram(names,options,host),checker=program.getTypeChecker();
53
+ const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],declarations=new Map(),moduleNodes=new Map();
54
+ const maxSymbols=8000,maxEdges=20000;
55
+ function filePath(source){return source.fileName.replace('/snapshot/','');}
56
+ function line(source,pos){return source.getLineAndCharacterOfPosition(pos).line+1;}
57
+ function displayName(node) {
58
+ if(node.name)return node.name.getText().replace(/^['"]|['"]$/g,'');
59
+ if(ts.isConstructorDeclaration(node))return 'constructor';
60
+ if(node.parent && ts.isVariableDeclaration(node.parent))return node.parent.name.getText();
61
+ if(node.parent && ts.isPropertyAssignment(node.parent))return node.parent.name.getText();
62
+ if(node.parent && ts.isPropertyDeclaration(node.parent))return node.parent.name.getText();
63
+ if(node.parent && ts.isExportAssignment(node.parent))return 'default';
64
+ return 'callback';
65
+ }
66
+ function isFunction(node){return !!node.body && (ts.isFunctionDeclaration(node)||ts.isMethodDeclaration(node)||ts.isFunctionExpression(node)||ts.isArrowFunction(node)||ts.isConstructorDeclaration(node)||ts.isGetAccessor(node)||ts.isSetAccessor(node));}
67
+ function addSymbol(node,source,owner){
68
+ if(symbols.length>=maxSymbols)return null;
69
+ let name=displayName(node),parent=node.parent;
70
+ const classParent=parent&&ts.isPropertyDeclaration(parent)?parent.parent:parent;
71
+ const cls=classParent && (ts.isClassDeclaration(classParent)||ts.isClassExpression(classParent)) ? classParent.name?.getText() : null;
72
+ if(cls)name=cls+'.'+name;
73
+ const start=line(source,node.getStart(source)),id=filePath(source)+'::'+name+'@'+start+':'+node.pos;
74
+ const symbol={id,path:filePath(source),name,start_line:start,end_line:line(source,node.end),kind:cls?'method':'function',language:'typescript',parent:owner?.id||null};
75
+ symbols.push(symbol);declarations.set(node,symbol);return symbol;
76
+ }
77
+ const parsed=[];
78
+ for(const source of program.getSourceFiles()){
79
+ if(!docs.has(source.fileName))continue;
80
+ if(source.parseDiagnostics.length){diagnostics.push({path:filePath(source),reason:'Incomplete or invalid syntax in the snapshot.',line:line(source,source.parseDiagnostics[0].start||0)});continue;}
81
+ parsed.push(source);
82
+ function collect(node,owner){const current=isFunction(node)?addSymbol(node,source,owner):owner;ts.forEachChild(node,child=>collect(child,current));}
83
+ collect(source,null);
84
+ }
85
+ function moduleSymbol(source){if(!moduleNodes.has(source.fileName)){const s={id:filePath(source)+'::<module>',path:filePath(source),name:'<module>',start_line:1,end_line:1,kind:'module',language:'typescript'};moduleNodes.set(source.fileName,s);symbols.push(s);}return moduleNodes.get(source.fileName);}
86
+ function targetFor(node){
87
+ const signature=checker.getResolvedSignature(node);let target=declarations.get(signature?.declaration);
88
+ if(target)return target;
89
+ let symbol=checker.getSymbolAtLocation(node.expression);
90
+ if(symbol?.flags & ts.SymbolFlags.Alias)symbol=checker.getAliasedSymbol(symbol);
91
+ for(const declaration of symbol?.declarations||[]){
92
+ if(declarations.has(declaration))return declarations.get(declaration);
93
+ if(ts.isVariableDeclaration(declaration)&&declarations.has(declaration.initializer))return declarations.get(declaration.initializer);
94
+ }
95
+ return null;
96
+ }
97
+ for(const source of parsed){
98
+ function visit(node,owner){
99
+ if(isFunction(node) && !declarations.has(node))return;
100
+ const active=declarations.get(node)||owner;
101
+ if((ts.isImportDeclaration(node)||ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
102
+ const spec=node.moduleSpecifier.text,target=resolve(spec,source.fileName);
103
+ if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:line(source,node.getStart(source)),kind:node.importClause?.isTypeOnly||node.isTypeOnly?'type_import':'import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
104
+ }
105
+ if(ts.isCallExpression(node)||ts.isNewExpression(node)){
106
+ const expression=node.expression,at=line(source,node.getStart(source));
107
+ if(ts.isCallExpression(node) && (expression.kind===ts.SyntaxKind.ImportKeyword || ts.isIdentifier(expression)&&expression.text==='require') && node.arguments?.length && ts.isStringLiteral(node.arguments[0])){
108
+ const spec=node.arguments[0].text,target=resolve(spec,source.fileName);
109
+ if(dependencies.length<maxEdges)dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:spec,line:at,kind:'dynamic_import',resolution:target?'resolved':spec.startsWith('.')?'unresolved':'external'});
110
+ }else{
111
+ const target=targetFor(node);
112
+ if(target && calls.length<maxEdges)calls.push({source:(active||moduleSymbol(source)).id,target:target.id,line:at,kind:ts.isNewExpression(node)?'construct':'call',resolution:'static'});
113
+ else if(unresolved.length<1000)unresolved.push({path:filePath(source),line:at,expression:expression.getText(source).slice(0,120),reason:'External or dynamic target, or no resolved implementation in the snapshot.'});
114
+ }
115
+ }
116
+ ts.forEachChild(node,child=>visit(child,active));
117
+ }
118
+ visit(source,null);
119
+ }
120
+ // Metadados Angular são lidos da AST; templates são analisados pelo compilador,
121
+ // sem inicializar a aplicação, resolver DI em runtime ou executar seus decorators.
122
+ const components=[],pipes=[],templatePaths=new Set();
123
+ for(const source of parsed){
124
+ const angularAliases=new Map(),angularNamespaces=new Set();
125
+ for(const statement of source.statements){if(ts.isImportDeclaration(statement)&&statement.moduleSpecifier.text==='@angular/core'){const bindings=statement.importClause?.namedBindings;if(bindings&&ts.isNamedImports(bindings))for(const spec of bindings.elements)angularAliases.set(spec.name.text,(spec.propertyName||spec.name).text);else if(bindings&&ts.isNamespaceImport(bindings))angularNamespaces.add(bindings.name.text)}}
126
+ function visitClasses(node){if(ts.isClassDeclaration(node))for(const decorator of ts.canHaveDecorators(node)?ts.getDecorators(node)||[]:[]){const call=decorator.expression;if(!ts.isCallExpression(call)||!call.arguments.length||!ts.isObjectLiteralExpression(call.arguments[0]))continue;let type=null;if(ts.isIdentifier(call.expression)){const symbol=checker.getSymbolAtLocation(call.expression);if(symbol?.declarations?.some(ts.isImportSpecifier))type=angularAliases.get(call.expression.text)}else if(ts.isPropertyAccessExpression(call.expression)&&angularNamespaces.has(call.expression.expression.getText(source)))type=call.expression.name.text;if(!['Component','Pipe'].includes(type))continue;const metadata=new Map(call.arguments[0].properties.filter(ts.isPropertyAssignment).map(p=>[p.name.getText(source).replace(/^['"]|['"]$/g,''),p.initializer]));const methods=new Map();for(const member of node.members){const symbol=declarations.get(member)||declarations.get(member.initializer);if(symbol&&member.name)methods.set(member.name.getText(source).replace(/^['"]|['"]$/g,''),symbol)}const literal=n=>n&&ts.isStringLiteralLike(n)?n.text:null;const entry={source,node,name:node.name?.text||'<component>',methods,selector:literal(metadata.get('selector'))};if(type==='Pipe'){entry.pipeName=literal(metadata.get('name'));pipes.push(entry);continue}const template=metadata.get('template'),url=literal(metadata.get('templateUrl'));entry.template=literal(template);entry.templatePath=source.fileName;entry.inlineLine=template?line(source,template.getStart(source)):1;if(url){const target=resolve(url.startsWith('.')?url:'./'+url,source.fileName);entry.templatePath=target;entry.template=target?docs.get(target)?.text:null;entry.inlineLine=1;dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:url,line:line(source,node.getStart(source)),kind:'angular_template',resolution:target?'resolved':'unresolved'})}for(const key of ['styleUrl','styleUrls']){const value=metadata.get(key),values=value&&ts.isArrayLiteralExpression(value)?value.elements:[value];for(const item of values){const style=literal(item);if(!style)continue;const target=resolve(style.startsWith('.')?style:'./'+style,source.fileName);dependencies.push({source:filePath(source),target:target?.replace('/snapshot/','')||null,specifier:style,line:line(source,item.getStart(source)),kind:'angular_style',resolution:target?'resolved':'unresolved'})}}components.push(entry)}ts.forEachChild(node,visitClasses)}visitClasses(source)
127
+ }
128
+ if(components.some(c=>c.template!=null)){
129
+ try{
130
+ const {parseTemplate}=require('./web_adapters/node/node_modules/@angular/compiler/fesm2022/compiler.mjs');
131
+ for(const component of components){if(component.template==null||!component.templatePath)continue;const pathName=component.templatePath.replace('/snapshot/',''),tree=parseTemplate(component.template,pathName);if(tree.errors?.length){diagnostics.push({path:pathName,reason:'Invalid Angular template or syntax not supported in this version.'});continue}templatePaths.add(pathName);const owner={id:pathName+'::'+filePath(component.source)+'::'+component.name+'.template@'+component.inlineLine,path:pathName,name:component.name+'.template',kind:'template',language:'angular',start_line:component.inlineLine,end_line:component.inlineLine+component.template.split('\n').length-1,location_kind:component.templatePath===component.source.fileName?'inline_template':'source_line'};if(symbols.length>=maxSymbols)break;symbols.push(owner);const shadow=new Set(),seen=new WeakSet();function walk(n,fn){if(!n||typeof n!=='object'||seen.has(n))return;seen.add(n);fn(n);for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(x=>walk(x,fn));else if(value&&typeof value==='object')walk(value,fn)}}walk(tree.nodes,n=>{if(['Variable','Reference','LetDeclaration'].includes(n.constructor.name)&&n.name)shadow.add(n.name);if(n.constructor.name==='ForLoopBlock'&&n.item?.name)shadow.add(n.item.name)});const seen2=new WeakSet();function inspect(n){if(!n||typeof n!=='object'||seen2.has(n))return;seen2.add(n);const start=n.sourceSpan?.start,at=(typeof start==='number'?component.template.slice(0,start).split('\n').length-1:(start?.line??0))+component.inlineLine;if(n.constructor.name==='Call'||n.constructor.name==='SafeCall'){const receiver=n.receiver;if(receiver?.constructor.name==='PropertyRead'&&['ImplicitReceiver','ThisReceiver'].includes(receiver.receiver?.constructor.name)){const target=shadow.has(receiver.name)?null:component.methods.get(receiver.name);if(target&&calls.length<maxEdges)calls.push({source:owner.id,target:target.id,line:at,kind:'template_call',resolution:'static'});else if(unresolved.length<1000)unresolved.push({path:pathName,line:at,expression:receiver.name+'()',reason:'Unresolved template method, or a local variable that shadows the component.'})}}if(n.constructor.name==='Element'){const matches=components.filter(c=>c.selector===n.name);if(matches.length===1&&filePath(matches[0].source)!==pathName)dependencies.push({source:pathName,target:filePath(matches[0].source),specifier:n.name,line:at,kind:'angular_component',resolution:'resolved'})}if(n.constructor.name==='BindingPipe'){const matches=pipes.filter(p=>p.pipeName===n.name&&p.methods.has('transform'));if(matches.length===1){const pipe=matches[0];dependencies.push({source:pathName,target:filePath(pipe.source),specifier:n.name,line:at,kind:'angular_pipe',resolution:'resolved'});calls.push({source:owner.id,target:pipe.methods.get('transform').id,line:at,kind:'template_pipe',resolution:'static'})}}for(const [key,value] of Object.entries(n)){if(['sourceSpan','span','startSourceSpan','endSourceSpan','keySpan','valueSpan','parent'].includes(key))continue;if(Array.isArray(value))value.forEach(inspect);else if(value&&typeof value==='object')inspect(value)}}inspect(tree.nodes)
132
+ }
133
+ }catch{diagnostics.push({path:'Angular',reason:'The Angular template analyzer did not finish. Check the local runtime.'})}
134
+ }
135
+ process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,maxEdges),calls:calls.slice(0,maxEdges),unresolved,diagnostics,parsed:[...parsed.map(filePath),...templatePaths],truncated:symbols.length>=maxSymbols||calls.length>=maxEdges||dependencies.length>=maxEdges}));