cortex-local 2.6.0__tar.gz → 2.7.0__tar.gz
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.
- {cortex_local-2.6.0 → cortex_local-2.7.0}/PKG-INFO +2 -1
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/chunker.py +56 -1
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/code_graph.py +18 -0
- cortex_local-2.7.0/cortex/core/godot.py +296 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/mcp_server.py +2 -1
- {cortex_local-2.6.0 → cortex_local-2.7.0}/pyproject.toml +1 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/.gitignore +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/README.md +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/__init__.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/cli.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/config.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/__init__.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/cache.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/formatters.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/global_graph.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/graph.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/notes_graph.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_analyzer.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_tool/CortexAnalyzer.csproj +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_tool/Program.cs +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/db.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/embedder.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/indexer.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/state.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/watcher.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/hatch_build.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/tests/test_imports.py +0 -0
- {cortex_local-2.6.0 → cortex_local-2.7.0}/tests/test_stdio_init.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortex-local
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: Local semantic search MCP server — notes + code, embedded Qdrant, fastembed
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.0.0
|
|
@@ -10,6 +10,7 @@ Requires-Dist: networkx>=3.0
|
|
|
10
10
|
Requires-Dist: qdrant-client>=1.17.0
|
|
11
11
|
Requires-Dist: tqdm>=4.0
|
|
12
12
|
Requires-Dist: tree-sitter-c-sharp
|
|
13
|
+
Requires-Dist: tree-sitter-gdscript
|
|
13
14
|
Requires-Dist: tree-sitter-javascript
|
|
14
15
|
Requires-Dist: tree-sitter-kotlin
|
|
15
16
|
Requires-Dist: tree-sitter-python
|
|
@@ -6,6 +6,11 @@ import os
|
|
|
6
6
|
import re as _re
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
try:
|
|
10
|
+
from . import godot as _godot
|
|
11
|
+
except ImportError: # server: core/ is on sys.path as top-level modules
|
|
12
|
+
import godot as _godot
|
|
13
|
+
|
|
9
14
|
_RAZOR_BLOCK_RE = _re.compile(r'@(?:code|functions)\s*\{', _re.IGNORECASE)
|
|
10
15
|
|
|
11
16
|
|
|
@@ -32,7 +37,8 @@ def _extract_razor_cs_blocks(source: str) -> list:
|
|
|
32
37
|
|
|
33
38
|
REPOS_DIR = Path(os.environ.get("REPOS_DIR", "/tmp/repos"))
|
|
34
39
|
|
|
35
|
-
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml"
|
|
40
|
+
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml",
|
|
41
|
+
".tscn", ".tres", ".godot", ".gdshader", ".dialogue"}
|
|
36
42
|
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target", "bin", "obj", ".godot"}
|
|
37
43
|
LANG_MAP = {
|
|
38
44
|
".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
|
|
@@ -41,6 +47,8 @@ LANG_MAP = {
|
|
|
41
47
|
".kt": "kotlin", ".kts": "kotlin", ".gd": "gdscript",
|
|
42
48
|
".yml": "yaml", ".yaml": "yaml", ".cs": "csharp",
|
|
43
49
|
".razor": "razor", ".cshtml": "cshtml",
|
|
50
|
+
".tscn": "godot-scene", ".tres": "godot-resource", ".godot": "godot-project",
|
|
51
|
+
".gdshader": "gdshader", ".dialogue": "dialogue",
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
def _is_excluded(rel: str, patterns: list) -> bool:
|
|
@@ -297,6 +305,8 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
|
|
|
297
305
|
rel = path.name
|
|
298
306
|
language = LANG_MAP.get(ext, ext.lstrip("."))
|
|
299
307
|
extra = _extra_tokens(rel, lines)
|
|
308
|
+
if ext == ".gd":
|
|
309
|
+
extra = (extra + " " + _godot.gd_context(source_text)).strip()
|
|
300
310
|
|
|
301
311
|
def _header(rel: str, s: int, e: int, tokens: str) -> str:
|
|
302
312
|
h = f"# {repo_name}/{rel} (lines {s}-{e})\n# {tokens}"
|
|
@@ -331,6 +341,29 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
|
|
|
331
341
|
})
|
|
332
342
|
return chunks
|
|
333
343
|
|
|
344
|
+
# Godot scenes/resources/project: index a readable summary, not the raw serialization
|
|
345
|
+
if ext in _godot.GODOT_SUMMARY_EXTS:
|
|
346
|
+
summary = _godot.summarize(path, source_text).splitlines()
|
|
347
|
+
if not summary:
|
|
348
|
+
return []
|
|
349
|
+
tokens = _path_tokens(repo_name, rel)
|
|
350
|
+
n_file = len(lines)
|
|
351
|
+
windows = [summary[i:i + MAX_CHUNK_LINES] for i in range(0, len(summary), MAX_CHUNK_LINES)]
|
|
352
|
+
k = len(windows)
|
|
353
|
+
out = []
|
|
354
|
+
for i, w in enumerate(windows):
|
|
355
|
+
# distinct pseudo line ranges per window (chunk ids are keyed on start_line)
|
|
356
|
+
s_line = 1 + (i * n_file) // k
|
|
357
|
+
e_line = max(s_line, ((i + 1) * n_file) // k)
|
|
358
|
+
url = f"{github_url_base}/{rel}#L{s_line}-L{e_line}" if github_url_base else ""
|
|
359
|
+
out.append({
|
|
360
|
+
"repo": repo_name, "file": rel, "language": language,
|
|
361
|
+
"start_line": s_line, "end_line": e_line,
|
|
362
|
+
"text": f"{_header(rel, s_line, e_line, tokens)}\n\n" + "\n".join(w),
|
|
363
|
+
"github_url": url,
|
|
364
|
+
})
|
|
365
|
+
return out
|
|
366
|
+
|
|
334
367
|
# Small files: keep as single chunk to avoid diluting path/filename signal
|
|
335
368
|
if len(lines) <= MAX_CHUNK_LINES:
|
|
336
369
|
body = "\n".join(lines).strip()
|
|
@@ -371,6 +404,28 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
|
|
|
371
404
|
if chunks:
|
|
372
405
|
return chunks
|
|
373
406
|
|
|
407
|
+
# GDScript: functions / inner classes / member blocks, annotations kept with their decl
|
|
408
|
+
if ext == ".gd":
|
|
409
|
+
spans = _godot.gd_spans(raw)
|
|
410
|
+
if spans:
|
|
411
|
+
chunks = []
|
|
412
|
+
tokens = _path_tokens(repo_name, rel)
|
|
413
|
+
for s0, e, _kind in spans:
|
|
414
|
+
if e - s0 > MAX_CHUNK_LINES:
|
|
415
|
+
chunks.extend(_sliding_window(lines, s0, e, repo_name, rel, language, github_url_base, extra))
|
|
416
|
+
continue
|
|
417
|
+
body = "\n".join(lines[s0:e]).strip()
|
|
418
|
+
if body:
|
|
419
|
+
url = f"{github_url_base}/{rel}#L{s0+1}-L{e}" if github_url_base else ""
|
|
420
|
+
chunks.append({
|
|
421
|
+
"repo": repo_name, "file": rel, "language": language,
|
|
422
|
+
"start_line": s0 + 1, "end_line": e,
|
|
423
|
+
"text": f"{_header(rel, s0+1, e, tokens)}\n\n{body}",
|
|
424
|
+
"github_url": url,
|
|
425
|
+
})
|
|
426
|
+
if chunks:
|
|
427
|
+
return chunks
|
|
428
|
+
|
|
374
429
|
if _TS_AVAILABLE and ext in _TS_LANGUAGES:
|
|
375
430
|
try:
|
|
376
431
|
parser = _TSParser(_TS_LANGUAGES[ext])
|
|
@@ -8,6 +8,13 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
log = logging.getLogger("cortex.graph")
|
|
10
10
|
|
|
11
|
+
try:
|
|
12
|
+
from . import godot as _godot
|
|
13
|
+
except ImportError: # server: core/ is on sys.path as top-level modules
|
|
14
|
+
import godot as _godot
|
|
15
|
+
|
|
16
|
+
_GODOT_EXTS = (".gd", ".tscn", ".tres", ".godot")
|
|
17
|
+
|
|
11
18
|
try:
|
|
12
19
|
import networkx as nx
|
|
13
20
|
_NX_AVAILABLE = True
|
|
@@ -336,6 +343,17 @@ def build_code_graph(repo_path: Path, all_files: set):
|
|
|
336
343
|
_add_edge(G, rel, target, "call")
|
|
337
344
|
for target in _extract_inheritance_targets(path, ext, all_files, rel, symbol_to_file):
|
|
338
345
|
_add_edge(G, rel, target, "inherits")
|
|
346
|
+
godot_files = [f for f in all_files if f.endswith(_GODOT_EXTS)]
|
|
347
|
+
if godot_files:
|
|
348
|
+
proj = _godot.GodotProject(repo_path, all_files)
|
|
349
|
+
for rel in godot_files:
|
|
350
|
+
try:
|
|
351
|
+
text = (repo_path / rel).read_text(encoding="utf-8", errors="replace")
|
|
352
|
+
except Exception:
|
|
353
|
+
continue
|
|
354
|
+
for target, kind in _godot.gd_edges(rel, text, proj):
|
|
355
|
+
if target in all_files:
|
|
356
|
+
_add_edge(G, rel, target, kind)
|
|
339
357
|
return G
|
|
340
358
|
|
|
341
359
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Godot support: GDScript semantic chunks, scene/resource/project summaries, graph edges.
|
|
2
|
+
|
|
3
|
+
Used by chunker.chunk_file (chunks) and code_graph.build_code_graph (edges). Paths handed in
|
|
4
|
+
and out are repo-relative POSIX; `res://` paths are resolved against the directory that holds
|
|
5
|
+
project.godot (the Godot project root, not necessarily the repo root).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from tree_sitter import Language, Parser
|
|
13
|
+
import tree_sitter_gdscript as _tsgd
|
|
14
|
+
_GD_LANG = Language(_tsgd.language())
|
|
15
|
+
except Exception: # grammar missing -> chunker falls back to the sliding window
|
|
16
|
+
_GD_LANG = None
|
|
17
|
+
|
|
18
|
+
GODOT_SUMMARY_EXTS = {".tscn", ".tres", ".godot"}
|
|
19
|
+
|
|
20
|
+
# ── res:// / uid:// resolution ───────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
_SECTION_RE = re.compile(r'^\[(\w+)([^\]]*)\]\s*$', re.MULTILINE)
|
|
23
|
+
_ATTR_RE = re.compile(r'(\w+)=("([^"]*)"|[^\s\]]+)')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _attrs(s: str) -> dict:
|
|
27
|
+
return {m.group(1): (m.group(3) if m.group(3) is not None else m.group(2)) for m in _ATTR_RE.finditer(s)}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GodotProject:
|
|
31
|
+
"""Repo-level Godot context: project roots, autoloads, uid map, class_name map."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, repo_path: Path, all_files: set):
|
|
34
|
+
self.roots = sorted({str(Path(f).parent.as_posix()) for f in all_files if Path(f).name == "project.godot"},
|
|
35
|
+
key=len, reverse=True)
|
|
36
|
+
self.autoloads: dict = {} # Name -> repo-relative file
|
|
37
|
+
self.uids: dict = {} # uid://x -> repo-relative file
|
|
38
|
+
self.classes: dict = {} # class_name -> repo-relative file
|
|
39
|
+
for f in all_files:
|
|
40
|
+
p = repo_path / f
|
|
41
|
+
if Path(f).name == "project.godot":
|
|
42
|
+
for name, target in _parse_autoloads(_read(p)).items():
|
|
43
|
+
resolved = self.res(target, f)
|
|
44
|
+
if resolved:
|
|
45
|
+
self.autoloads[name] = resolved
|
|
46
|
+
elif f.endswith(".gd"):
|
|
47
|
+
m = re.search(r'^class_name\s+(\w+)', _read(p), re.MULTILINE)
|
|
48
|
+
if m:
|
|
49
|
+
self.classes.setdefault(m.group(1), f)
|
|
50
|
+
if f.endswith((".tscn", ".tres")):
|
|
51
|
+
head = _read(p, 400)
|
|
52
|
+
m = re.search(r'uid="(uid://[^"]+)"', head.split("\n", 1)[0])
|
|
53
|
+
if m:
|
|
54
|
+
self.uids[m.group(1)] = f
|
|
55
|
+
# `.uid` twin files (Godot 4.4+): script.gd.uid contains the script's uid
|
|
56
|
+
for uidfile in repo_path.rglob("*.uid"):
|
|
57
|
+
try:
|
|
58
|
+
rel = uidfile.relative_to(repo_path).as_posix()[:-4]
|
|
59
|
+
if rel in all_files:
|
|
60
|
+
self.uids[uidfile.read_text(errors="replace").strip()] = rel
|
|
61
|
+
except Exception:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
def project_root(self, rel: str) -> str:
|
|
65
|
+
for r in self.roots:
|
|
66
|
+
if r == "." or rel.startswith(r + "/"):
|
|
67
|
+
return r
|
|
68
|
+
return "."
|
|
69
|
+
|
|
70
|
+
def res(self, path: str, from_file: str) -> str | None:
|
|
71
|
+
"""res://a/b.gd (or uid://...) -> repo-relative path, None if unknown."""
|
|
72
|
+
path = path.strip().lstrip("*")
|
|
73
|
+
if path.startswith("uid://"):
|
|
74
|
+
return self.uids.get(path)
|
|
75
|
+
if not path.startswith("res://"):
|
|
76
|
+
return None
|
|
77
|
+
root = self.project_root(from_file)
|
|
78
|
+
tail = path[len("res://"):]
|
|
79
|
+
return tail if root == "." else f"{root}/{tail}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read(p: Path, limit: int | None = None) -> str:
|
|
83
|
+
try:
|
|
84
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
85
|
+
return text[:limit] if limit else text
|
|
86
|
+
except Exception:
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_autoloads(text: str) -> dict:
|
|
91
|
+
out, in_section = {}, False
|
|
92
|
+
for line in text.splitlines():
|
|
93
|
+
s = line.strip()
|
|
94
|
+
if s.startswith("["):
|
|
95
|
+
in_section = s == "[autoload]"
|
|
96
|
+
continue
|
|
97
|
+
if in_section and "=" in s:
|
|
98
|
+
k, v = s.split("=", 1)
|
|
99
|
+
out[k.strip()] = v.strip().strip('"')
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── GDScript chunking ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
_DECL_TYPES = {"signal_statement", "variable_statement", "const_statement", "enum_definition",
|
|
106
|
+
"export_variable_statement", "onready_variable_statement"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def gd_context(source: str) -> str:
|
|
110
|
+
"""'class_name X extends Y' for embedding headers ('' when the file declares neither)."""
|
|
111
|
+
cn = re.search(r'^class_name\s+(\w+)', source, re.MULTILINE)
|
|
112
|
+
ex = re.search(r'^extends\s+(\S+)', source, re.MULTILINE)
|
|
113
|
+
parts = []
|
|
114
|
+
if cn:
|
|
115
|
+
parts.append(f"class_name {cn.group(1)}")
|
|
116
|
+
if ex:
|
|
117
|
+
parts.append(f"extends {ex.group(1)}")
|
|
118
|
+
return " ".join(parts)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def gd_spans(raw: bytes) -> list | None:
|
|
122
|
+
"""Top-level semantic spans [(start_line0, end_line_excl, kind)], or None without the grammar.
|
|
123
|
+
|
|
124
|
+
Functions and inner classes are their own spans, with preceding annotations (@rpc,
|
|
125
|
+
@warning_ignore ...) attached. Runs of signals/vars/consts/enums form one 'members' span.
|
|
126
|
+
"""
|
|
127
|
+
if _GD_LANG is None:
|
|
128
|
+
return None
|
|
129
|
+
try:
|
|
130
|
+
tree = Parser(_GD_LANG).parse(raw)
|
|
131
|
+
except Exception:
|
|
132
|
+
return None
|
|
133
|
+
spans: list = []
|
|
134
|
+
pending_ann = None # start line of annotations waiting for their declaration
|
|
135
|
+
decl = None # [start, end] of the current members run
|
|
136
|
+
for node in tree.root_node.children:
|
|
137
|
+
t = node.type
|
|
138
|
+
s, e = node.start_point[0], node.end_point[0] + 1
|
|
139
|
+
if t == "annotation":
|
|
140
|
+
pending_ann = s if pending_ann is None else pending_ann
|
|
141
|
+
continue
|
|
142
|
+
start = pending_ann if pending_ann is not None else s
|
|
143
|
+
pending_ann = None
|
|
144
|
+
if t in ("function_definition", "class_definition", "constructor_definition"):
|
|
145
|
+
if decl:
|
|
146
|
+
spans.append((decl[0], decl[1], "members")); decl = None
|
|
147
|
+
spans.append((start, e, "class" if t == "class_definition" else "function"))
|
|
148
|
+
elif t in _DECL_TYPES:
|
|
149
|
+
decl = [decl[0], e] if decl else [start, e]
|
|
150
|
+
# class_name / extends / comments: covered by the header context
|
|
151
|
+
if decl:
|
|
152
|
+
spans.append((decl[0], decl[1], "members"))
|
|
153
|
+
return spans
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Scene / resource / project summaries (indexed instead of raw text) ──────
|
|
157
|
+
|
|
158
|
+
def _ext_resources(text: str) -> dict:
|
|
159
|
+
out = {}
|
|
160
|
+
for m in _SECTION_RE.finditer(text):
|
|
161
|
+
if m.group(1) == "ext_resource":
|
|
162
|
+
a = _attrs(m.group(2))
|
|
163
|
+
if "id" in a:
|
|
164
|
+
out[a["id"]] = a.get("path") or a.get("uid", "")
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _sections(text: str):
|
|
169
|
+
"""Yield (kind, attrs, body_lines) for each [section] in a .tscn/.tres file."""
|
|
170
|
+
matches = list(_SECTION_RE.finditer(text))
|
|
171
|
+
for i, m in enumerate(matches):
|
|
172
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
173
|
+
body = text[m.end():end].strip("\n").splitlines()
|
|
174
|
+
yield m.group(1), _attrs(m.group(2)), body
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_EXTRES_REF = re.compile(r'ExtResource\(\s*"?([^")\s]+)"?\s*\)')
|
|
178
|
+
_SKIP_VALUE = re.compile(r'^(Packed\w*Array|PoolByteArray|Image)\(')
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def summarize_tscn(text: str) -> str:
|
|
182
|
+
ext = _ext_resources(text)
|
|
183
|
+
lines = ["Godot scene", "Nodes:"]
|
|
184
|
+
conns = []
|
|
185
|
+
for kind, a, body in _sections(text):
|
|
186
|
+
if kind == "node":
|
|
187
|
+
name, parent = a.get("name", "?"), a.get("parent")
|
|
188
|
+
path = name if parent is None else (name if parent == "." else f"{parent}/{name}")
|
|
189
|
+
desc = f" {path}"
|
|
190
|
+
if a.get("type"):
|
|
191
|
+
desc += f" ({a['type']})"
|
|
192
|
+
inst = _EXTRES_REF.search(a.get("instance", ""))
|
|
193
|
+
if inst:
|
|
194
|
+
desc += f" instance={ext.get(inst.group(1), inst.group(1))}"
|
|
195
|
+
for line in body:
|
|
196
|
+
if line.startswith("script ="):
|
|
197
|
+
ref = _EXTRES_REF.search(line)
|
|
198
|
+
if ref:
|
|
199
|
+
desc += f" script={ext.get(ref.group(1), ref.group(1))}"
|
|
200
|
+
lines.append(desc)
|
|
201
|
+
elif kind == "connection":
|
|
202
|
+
conns.append(f" {a.get('from', '?')}.{a.get('signal', '?')} -> {a.get('method', '?')} (on {a.get('to', '?')})")
|
|
203
|
+
if conns:
|
|
204
|
+
lines.append("Signal connections:")
|
|
205
|
+
lines += conns
|
|
206
|
+
if ext:
|
|
207
|
+
lines.append("Uses: " + ", ".join(sorted(set(v for v in ext.values() if v))))
|
|
208
|
+
return "\n".join(lines)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def summarize_tres(text: str) -> str:
|
|
212
|
+
ext = _ext_resources(text)
|
|
213
|
+
lines = []
|
|
214
|
+
for kind, a, body in _sections(text):
|
|
215
|
+
if kind == "gd_resource":
|
|
216
|
+
head = f"Godot resource type={a.get('type', '?')}"
|
|
217
|
+
if a.get("script_class"):
|
|
218
|
+
head += f" script_class={a['script_class']}"
|
|
219
|
+
lines.append(head)
|
|
220
|
+
elif kind in ("resource", "sub_resource"):
|
|
221
|
+
if kind == "sub_resource":
|
|
222
|
+
lines.append(f"Sub-resource {a.get('type', '')} {a.get('id', '')}".rstrip())
|
|
223
|
+
for line in body:
|
|
224
|
+
if "=" not in line:
|
|
225
|
+
continue
|
|
226
|
+
k, v = (x.strip() for x in line.split("=", 1))
|
|
227
|
+
if _SKIP_VALUE.match(v):
|
|
228
|
+
continue
|
|
229
|
+
v = _EXTRES_REF.sub(lambda m: ext.get(m.group(1), m.group(1)), v)
|
|
230
|
+
lines.append(f" {k} = {v[:200]}")
|
|
231
|
+
if ext:
|
|
232
|
+
lines.append("Uses: " + ", ".join(sorted(set(v for v in ext.values() if v))))
|
|
233
|
+
return "\n".join(lines)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def summarize_project(text: str) -> str:
|
|
237
|
+
lines, section, actions = ["Godot project.godot"], "", []
|
|
238
|
+
for raw in text.splitlines():
|
|
239
|
+
s = raw.strip()
|
|
240
|
+
if s.startswith("["):
|
|
241
|
+
section = s.strip("[]")
|
|
242
|
+
continue
|
|
243
|
+
if "=" not in s or s.startswith(";"):
|
|
244
|
+
continue
|
|
245
|
+
k, v = (x.strip() for x in s.split("=", 1))
|
|
246
|
+
if section == "application" and k in ("config/name", "run/main_scene", "config/features"):
|
|
247
|
+
lines.append(f"{k} = {v}")
|
|
248
|
+
elif section == "autoload":
|
|
249
|
+
lines.append(f"autoload {k} = {v.strip(chr(34)).lstrip('*')}")
|
|
250
|
+
elif section == "input" and not k.startswith(" "):
|
|
251
|
+
actions.append(k)
|
|
252
|
+
if actions:
|
|
253
|
+
lines.append("Input actions: " + ", ".join(actions))
|
|
254
|
+
return "\n".join(lines)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def summarize(path: Path, text: str) -> str:
|
|
258
|
+
if path.suffix == ".tscn":
|
|
259
|
+
return summarize_tscn(text)
|
|
260
|
+
if path.suffix == ".tres":
|
|
261
|
+
return summarize_tres(text)
|
|
262
|
+
return summarize_project(text)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ── Graph edges ──────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
_RES_STR = re.compile(r'"\*?((?:res|uid)://[^"]+)"')
|
|
268
|
+
_LOAD_RE = re.compile(r'\b(?:preload|load|ResourceLoader\.load)\s*\(\s*"((?:res|uid)://[^"]+)"')
|
|
269
|
+
_EXTENDS_RE = re.compile(r'^extends\s+("([^"]+)"|(\w+))', re.MULTILINE)
|
|
270
|
+
_USE_RE = re.compile(r'\b([A-Z]\w*)\s*(?:\.|\(|$)|(?::|\bas|\bis|->)\s*([A-Z]\w*)', re.MULTILINE)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def gd_edges(rel: str, text: str, proj: GodotProject) -> list:
|
|
274
|
+
"""[(target, edge_type)] for a .gd / .tscn / .tres / project.godot file."""
|
|
275
|
+
edges: list = []
|
|
276
|
+
if rel.endswith(".gd"):
|
|
277
|
+
m = _EXTENDS_RE.search(text)
|
|
278
|
+
if m:
|
|
279
|
+
tgt = proj.res(m.group(2), rel) if m.group(2) else proj.classes.get(m.group(3))
|
|
280
|
+
if tgt:
|
|
281
|
+
edges.append((tgt, "inherits"))
|
|
282
|
+
for m in _LOAD_RE.finditer(text):
|
|
283
|
+
tgt = proj.res(m.group(1), rel)
|
|
284
|
+
if tgt:
|
|
285
|
+
edges.append((tgt, "import"))
|
|
286
|
+
used = {a or b for a, b in _USE_RE.findall(text)}
|
|
287
|
+
for name in used:
|
|
288
|
+
tgt = proj.classes.get(name) or proj.autoloads.get(name)
|
|
289
|
+
if tgt:
|
|
290
|
+
edges.append((tgt, "call"))
|
|
291
|
+
else: # scenes/resources/project: every res:// reference is a dependency
|
|
292
|
+
for m in _RES_STR.finditer(text):
|
|
293
|
+
tgt = proj.res(m.group(1), rel)
|
|
294
|
+
if tgt:
|
|
295
|
+
edges.append((tgt, "import"))
|
|
296
|
+
return [(t, k) for t, k in edges if t != rel]
|
|
@@ -369,9 +369,10 @@ def get_neighbors(file: str, repo: str) -> str:
|
|
|
369
369
|
meta = get_graph_meta(repo)
|
|
370
370
|
if not meta:
|
|
371
371
|
return f"No graph data for '{repo}'. Run reindex() first."
|
|
372
|
+
file = file.removeprefix("res://") # Godot paths: res:// is relative to the project root
|
|
372
373
|
file_meta = meta.get(file)
|
|
373
374
|
if file_meta is None:
|
|
374
|
-
matches =
|
|
375
|
+
matches = sorted((f for f in meta if f.endswith("/" + file) or file in f), key=len)
|
|
375
376
|
if not matches:
|
|
376
377
|
return f"File '{file}' not found in graph for '{repo}'."
|
|
377
378
|
file = matches[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|