graphifyy 0.3.4__tar.gz → 0.3.6__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.
- {graphifyy-0.3.4 → graphifyy-0.3.6}/PKG-INFO +8 -1
- {graphifyy-0.3.4 → graphifyy-0.3.6}/README.md +1 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/detect.py +125 -2
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/extract.py +179 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/PKG-INFO +8 -1
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/requires.txt +7 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/pyproject.toml +4 -2
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_extract.py +1 -1
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_languages.py +34 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/LICENSE +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/__init__.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/__main__.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/analyze.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/benchmark.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/build.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/cache.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/cluster.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/export.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/hooks.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/ingest.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/manifest.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/report.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/security.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/serve.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-claw.md +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-codex.md +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-opencode.md +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-windows.md +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill.md +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/validate.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/watch.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/wiki.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/SOURCES.txt +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/dependency_links.txt +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/entry_points.txt +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/top_level.txt +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/setup.cfg +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_analyze.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_benchmark.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_build.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_cache.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_claude_md.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_cluster.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_confidence.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_detect.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_export.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_hooks.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_hypergraph.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_ingest.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_install.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_multilang.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_pipeline.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_rationale.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_report.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_security.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_semantic_similarity.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_serve.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_validate.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_watch.py +0 -0
- {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_wiki.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphifyy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-swift
|
|
|
50
50
|
Requires-Dist: tree-sitter-lua
|
|
51
51
|
Requires-Dist: tree-sitter-zig
|
|
52
52
|
Requires-Dist: tree-sitter-powershell
|
|
53
|
+
Requires-Dist: tree-sitter-elixir
|
|
53
54
|
Provides-Extra: mcp
|
|
54
55
|
Requires-Dist: mcp; extra == "mcp"
|
|
55
56
|
Provides-Extra: neo4j
|
|
@@ -61,6 +62,9 @@ Provides-Extra: watch
|
|
|
61
62
|
Requires-Dist: watchdog; extra == "watch"
|
|
62
63
|
Provides-Extra: leiden
|
|
63
64
|
Requires-Dist: graspologic; extra == "leiden"
|
|
65
|
+
Provides-Extra: office
|
|
66
|
+
Requires-Dist: python-docx; extra == "office"
|
|
67
|
+
Requires-Dist: openpyxl; extra == "office"
|
|
64
68
|
Provides-Extra: all
|
|
65
69
|
Requires-Dist: mcp; extra == "all"
|
|
66
70
|
Requires-Dist: neo4j; extra == "all"
|
|
@@ -68,6 +72,8 @@ Requires-Dist: pypdf; extra == "all"
|
|
|
68
72
|
Requires-Dist: html2text; extra == "all"
|
|
69
73
|
Requires-Dist: watchdog; extra == "all"
|
|
70
74
|
Requires-Dist: graspologic; extra == "all"
|
|
75
|
+
Requires-Dist: python-docx; extra == "all"
|
|
76
|
+
Requires-Dist: openpyxl; extra == "all"
|
|
71
77
|
Dynamic: license-file
|
|
72
78
|
|
|
73
79
|
# graphify
|
|
@@ -239,6 +245,7 @@ Works with any mix of file types:
|
|
|
239
245
|
|------|-----------|------------|
|
|
240
246
|
| Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
|
|
241
247
|
| Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
|
|
248
|
+
| Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
|
|
242
249
|
| Papers | `.pdf` | Citation mining + concept extraction |
|
|
243
250
|
| Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
|
|
244
251
|
|
|
@@ -167,6 +167,7 @@ Works with any mix of file types:
|
|
|
167
167
|
|------|-----------|------------|
|
|
168
168
|
| Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
|
|
169
169
|
| Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
|
|
170
|
+
| Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
|
|
170
171
|
| Papers | `.pdf` | Citation mining + concept extraction |
|
|
171
172
|
| Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
|
|
172
173
|
|
|
@@ -17,10 +17,11 @@ class FileType(str, Enum):
|
|
|
17
17
|
|
|
18
18
|
_MANIFEST_PATH = "graphify-out/manifest.json"
|
|
19
19
|
|
|
20
|
-
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1'}
|
|
20
|
+
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs'}
|
|
21
21
|
DOC_EXTENSIONS = {'.md', '.txt', '.rst'}
|
|
22
22
|
PAPER_EXTENSIONS = {'.pdf'}
|
|
23
23
|
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
|
24
|
+
OFFICE_EXTENSIONS = {'.docx', '.xlsx'}
|
|
24
25
|
|
|
25
26
|
CORPUS_WARN_THRESHOLD = 50_000 # words - below this, warn "you may not need a graph"
|
|
26
27
|
CORPUS_UPPER_THRESHOLD = 500_000 # words - above this, warn about token cost
|
|
@@ -86,6 +87,8 @@ def classify_file(path: Path) -> FileType | None:
|
|
|
86
87
|
if _looks_like_paper(path):
|
|
87
88
|
return FileType.PAPER
|
|
88
89
|
return FileType.DOCUMENT
|
|
90
|
+
if ext in OFFICE_EXTENSIONS:
|
|
91
|
+
return FileType.DOCUMENT
|
|
89
92
|
return None
|
|
90
93
|
|
|
91
94
|
|
|
@@ -104,10 +107,115 @@ def extract_pdf_text(path: Path) -> str:
|
|
|
104
107
|
return ""
|
|
105
108
|
|
|
106
109
|
|
|
110
|
+
def docx_to_markdown(path: Path) -> str:
|
|
111
|
+
"""Convert a .docx file to markdown text using python-docx."""
|
|
112
|
+
try:
|
|
113
|
+
from docx import Document
|
|
114
|
+
from docx.oxml.ns import qn
|
|
115
|
+
doc = Document(str(path))
|
|
116
|
+
lines = []
|
|
117
|
+
for para in doc.paragraphs:
|
|
118
|
+
style = para.style.name if para.style else ""
|
|
119
|
+
text = para.text.strip()
|
|
120
|
+
if not text:
|
|
121
|
+
lines.append("")
|
|
122
|
+
continue
|
|
123
|
+
if style.startswith("Heading 1"):
|
|
124
|
+
lines.append(f"# {text}")
|
|
125
|
+
elif style.startswith("Heading 2"):
|
|
126
|
+
lines.append(f"## {text}")
|
|
127
|
+
elif style.startswith("Heading 3"):
|
|
128
|
+
lines.append(f"### {text}")
|
|
129
|
+
elif style.startswith("List"):
|
|
130
|
+
lines.append(f"- {text}")
|
|
131
|
+
else:
|
|
132
|
+
lines.append(text)
|
|
133
|
+
# Tables
|
|
134
|
+
for table in doc.tables:
|
|
135
|
+
rows = [[cell.text.strip() for cell in row.cells] for row in table.rows]
|
|
136
|
+
if not rows:
|
|
137
|
+
continue
|
|
138
|
+
header = "| " + " | ".join(rows[0]) + " |"
|
|
139
|
+
sep = "| " + " | ".join("---" for _ in rows[0]) + " |"
|
|
140
|
+
lines.extend([header, sep])
|
|
141
|
+
for row in rows[1:]:
|
|
142
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
except ImportError:
|
|
145
|
+
return ""
|
|
146
|
+
except Exception:
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def xlsx_to_markdown(path: Path) -> str:
|
|
151
|
+
"""Convert an .xlsx file to markdown text using openpyxl."""
|
|
152
|
+
try:
|
|
153
|
+
import openpyxl
|
|
154
|
+
wb = openpyxl.load_workbook(str(path), read_only=True, data_only=True)
|
|
155
|
+
sections = []
|
|
156
|
+
for sheet_name in wb.sheetnames:
|
|
157
|
+
ws = wb[sheet_name]
|
|
158
|
+
rows = []
|
|
159
|
+
for row in ws.iter_rows(values_only=True):
|
|
160
|
+
# Skip entirely empty rows
|
|
161
|
+
if all(cell is None for cell in row):
|
|
162
|
+
continue
|
|
163
|
+
rows.append([str(cell) if cell is not None else "" for cell in row])
|
|
164
|
+
if not rows:
|
|
165
|
+
continue
|
|
166
|
+
sections.append(f"## Sheet: {sheet_name}")
|
|
167
|
+
if len(rows) >= 1:
|
|
168
|
+
header = "| " + " | ".join(rows[0]) + " |"
|
|
169
|
+
sep = "| " + " | ".join("---" for _ in rows[0]) + " |"
|
|
170
|
+
sections.extend([header, sep])
|
|
171
|
+
for row in rows[1:]:
|
|
172
|
+
sections.append("| " + " | ".join(row) + " |")
|
|
173
|
+
wb.close()
|
|
174
|
+
return "\n".join(sections)
|
|
175
|
+
except ImportError:
|
|
176
|
+
return ""
|
|
177
|
+
except Exception:
|
|
178
|
+
return ""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def convert_office_file(path: Path, out_dir: Path) -> Path | None:
|
|
182
|
+
"""Convert a .docx or .xlsx to a markdown sidecar in out_dir.
|
|
183
|
+
|
|
184
|
+
Returns the path of the converted .md file, or None if conversion failed
|
|
185
|
+
or the required library is not installed.
|
|
186
|
+
"""
|
|
187
|
+
ext = path.suffix.lower()
|
|
188
|
+
if ext == ".docx":
|
|
189
|
+
text = docx_to_markdown(path)
|
|
190
|
+
elif ext == ".xlsx":
|
|
191
|
+
text = xlsx_to_markdown(path)
|
|
192
|
+
else:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
if not text.strip():
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
# Use a stable name derived from the original path to avoid collisions
|
|
200
|
+
import hashlib
|
|
201
|
+
name_hash = hashlib.sha256(str(path.resolve()).encode()).hexdigest()[:8]
|
|
202
|
+
out_path = out_dir / f"{path.stem}_{name_hash}.md"
|
|
203
|
+
out_path.write_text(
|
|
204
|
+
f"<!-- converted from {path.name} -->\n\n{text}",
|
|
205
|
+
encoding="utf-8",
|
|
206
|
+
)
|
|
207
|
+
return out_path
|
|
208
|
+
|
|
209
|
+
|
|
107
210
|
def count_words(path: Path) -> int:
|
|
108
211
|
try:
|
|
109
|
-
|
|
212
|
+
ext = path.suffix.lower()
|
|
213
|
+
if ext == ".pdf":
|
|
110
214
|
return len(extract_pdf_text(path).split())
|
|
215
|
+
if ext == ".docx":
|
|
216
|
+
return len(docx_to_markdown(path).split())
|
|
217
|
+
if ext == ".xlsx":
|
|
218
|
+
return len(xlsx_to_markdown(path).split())
|
|
111
219
|
return len(path.read_text(errors="ignore").split())
|
|
112
220
|
except Exception:
|
|
113
221
|
return 0
|
|
@@ -224,6 +332,8 @@ def detect(root: Path) -> dict:
|
|
|
224
332
|
seen.add(p)
|
|
225
333
|
all_files.append(p)
|
|
226
334
|
|
|
335
|
+
converted_dir = root / "graphify-out" / "converted"
|
|
336
|
+
|
|
227
337
|
for p in all_files:
|
|
228
338
|
# For memory dir files, skip hidden/noise filtering
|
|
229
339
|
in_memory = memory_dir.exists() and str(p).startswith(str(memory_dir))
|
|
@@ -232,6 +342,9 @@ def detect(root: Path) -> dict:
|
|
|
232
342
|
# but catch hidden files at the root level
|
|
233
343
|
if p.name.startswith("."):
|
|
234
344
|
continue
|
|
345
|
+
# Skip files inside our own converted/ dir (avoid re-processing sidecars)
|
|
346
|
+
if str(p).startswith(str(converted_dir)):
|
|
347
|
+
continue
|
|
235
348
|
if _is_ignored(p, root, ignore_patterns):
|
|
236
349
|
continue
|
|
237
350
|
if _is_sensitive(p):
|
|
@@ -239,6 +352,16 @@ def detect(root: Path) -> dict:
|
|
|
239
352
|
continue
|
|
240
353
|
ftype = classify_file(p)
|
|
241
354
|
if ftype:
|
|
355
|
+
# Office files: convert to markdown sidecar so subagents can read them
|
|
356
|
+
if p.suffix.lower() in OFFICE_EXTENSIONS:
|
|
357
|
+
md_path = convert_office_file(p, converted_dir)
|
|
358
|
+
if md_path:
|
|
359
|
+
files[ftype].append(str(md_path))
|
|
360
|
+
total_words += count_words(md_path)
|
|
361
|
+
else:
|
|
362
|
+
# Conversion failed (library not installed) - skip with note
|
|
363
|
+
skipped_sensitive.append(str(p) + " [office conversion failed - pip install graphifyy[office]]")
|
|
364
|
+
continue
|
|
242
365
|
files[ftype].append(str(p))
|
|
243
366
|
total_words += count_words(p)
|
|
244
367
|
|
|
@@ -1928,6 +1928,183 @@ def _resolve_cross_file_imports(
|
|
|
1928
1928
|
return new_edges
|
|
1929
1929
|
|
|
1930
1930
|
|
|
1931
|
+
def extract_elixir(path: Path) -> dict:
|
|
1932
|
+
"""Extract modules, functions, imports, and calls from a .ex/.exs file."""
|
|
1933
|
+
try:
|
|
1934
|
+
import tree_sitter_elixir as tselixir
|
|
1935
|
+
from tree_sitter import Language, Parser
|
|
1936
|
+
except ImportError:
|
|
1937
|
+
return {"nodes": [], "edges": [], "error": "tree_sitter_elixir not installed"}
|
|
1938
|
+
|
|
1939
|
+
try:
|
|
1940
|
+
language = Language(tselixir.language())
|
|
1941
|
+
parser = Parser(language)
|
|
1942
|
+
source = path.read_bytes()
|
|
1943
|
+
tree = parser.parse(source)
|
|
1944
|
+
root = tree.root_node
|
|
1945
|
+
except Exception as e:
|
|
1946
|
+
return {"nodes": [], "edges": [], "error": str(e)}
|
|
1947
|
+
|
|
1948
|
+
stem = path.stem
|
|
1949
|
+
str_path = str(path)
|
|
1950
|
+
nodes: list[dict] = []
|
|
1951
|
+
edges: list[dict] = []
|
|
1952
|
+
seen_ids: set[str] = set()
|
|
1953
|
+
function_bodies: list[tuple[str, Any]] = []
|
|
1954
|
+
|
|
1955
|
+
def add_node(nid: str, label: str, line: int) -> None:
|
|
1956
|
+
if nid not in seen_ids:
|
|
1957
|
+
seen_ids.add(nid)
|
|
1958
|
+
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
1959
|
+
"source_file": str_path, "source_location": f"L{line}"})
|
|
1960
|
+
|
|
1961
|
+
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
1962
|
+
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
|
1963
|
+
edges.append({"source": src, "target": tgt, "relation": relation,
|
|
1964
|
+
"confidence": confidence, "source_file": str_path,
|
|
1965
|
+
"source_location": f"L{line}", "weight": weight})
|
|
1966
|
+
|
|
1967
|
+
file_nid = _make_id(stem)
|
|
1968
|
+
add_node(file_nid, path.name, 1)
|
|
1969
|
+
|
|
1970
|
+
_IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"})
|
|
1971
|
+
|
|
1972
|
+
def _get_alias_text(node) -> str | None:
|
|
1973
|
+
for child in node.children:
|
|
1974
|
+
if child.type == "alias":
|
|
1975
|
+
return source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
1976
|
+
return None
|
|
1977
|
+
|
|
1978
|
+
def walk(node, parent_module_nid: str | None = None) -> None:
|
|
1979
|
+
if node.type != "call":
|
|
1980
|
+
for child in node.children:
|
|
1981
|
+
walk(child, parent_module_nid)
|
|
1982
|
+
return
|
|
1983
|
+
|
|
1984
|
+
identifier_node = None
|
|
1985
|
+
arguments_node = None
|
|
1986
|
+
do_block_node = None
|
|
1987
|
+
for child in node.children:
|
|
1988
|
+
if child.type == "identifier":
|
|
1989
|
+
identifier_node = child
|
|
1990
|
+
elif child.type == "arguments":
|
|
1991
|
+
arguments_node = child
|
|
1992
|
+
elif child.type == "do_block":
|
|
1993
|
+
do_block_node = child
|
|
1994
|
+
|
|
1995
|
+
if identifier_node is None:
|
|
1996
|
+
for child in node.children:
|
|
1997
|
+
walk(child, parent_module_nid)
|
|
1998
|
+
return
|
|
1999
|
+
|
|
2000
|
+
keyword = source[identifier_node.start_byte:identifier_node.end_byte].decode("utf-8", errors="replace")
|
|
2001
|
+
line = node.start_point[0] + 1
|
|
2002
|
+
|
|
2003
|
+
if keyword == "defmodule":
|
|
2004
|
+
module_name = _get_alias_text(arguments_node) if arguments_node else None
|
|
2005
|
+
if not module_name:
|
|
2006
|
+
return
|
|
2007
|
+
module_nid = _make_id(stem, module_name)
|
|
2008
|
+
add_node(module_nid, module_name, line)
|
|
2009
|
+
add_edge(file_nid, module_nid, "contains", line)
|
|
2010
|
+
if do_block_node:
|
|
2011
|
+
for child in do_block_node.children:
|
|
2012
|
+
walk(child, parent_module_nid=module_nid)
|
|
2013
|
+
return
|
|
2014
|
+
|
|
2015
|
+
if keyword in ("def", "defp"):
|
|
2016
|
+
func_name = None
|
|
2017
|
+
if arguments_node:
|
|
2018
|
+
for child in arguments_node.children:
|
|
2019
|
+
if child.type == "call":
|
|
2020
|
+
for sub in child.children:
|
|
2021
|
+
if sub.type == "identifier":
|
|
2022
|
+
func_name = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
|
|
2023
|
+
break
|
|
2024
|
+
elif child.type == "identifier":
|
|
2025
|
+
func_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2026
|
+
break
|
|
2027
|
+
if not func_name:
|
|
2028
|
+
return
|
|
2029
|
+
container = parent_module_nid or file_nid
|
|
2030
|
+
func_nid = _make_id(container, func_name)
|
|
2031
|
+
add_node(func_nid, f"{func_name}()", line)
|
|
2032
|
+
if parent_module_nid:
|
|
2033
|
+
add_edge(parent_module_nid, func_nid, "method", line)
|
|
2034
|
+
else:
|
|
2035
|
+
add_edge(file_nid, func_nid, "contains", line)
|
|
2036
|
+
if do_block_node:
|
|
2037
|
+
function_bodies.append((func_nid, do_block_node))
|
|
2038
|
+
return
|
|
2039
|
+
|
|
2040
|
+
if keyword in _IMPORT_KEYWORDS and arguments_node:
|
|
2041
|
+
module_name = _get_alias_text(arguments_node)
|
|
2042
|
+
if module_name:
|
|
2043
|
+
tgt_nid = _make_id(module_name)
|
|
2044
|
+
add_edge(file_nid, tgt_nid, "imports", line)
|
|
2045
|
+
return
|
|
2046
|
+
|
|
2047
|
+
for child in node.children:
|
|
2048
|
+
walk(child, parent_module_nid)
|
|
2049
|
+
|
|
2050
|
+
walk(root)
|
|
2051
|
+
|
|
2052
|
+
label_to_nid: dict[str, str] = {}
|
|
2053
|
+
for n in nodes:
|
|
2054
|
+
normalised = n["label"].strip("()").lstrip(".")
|
|
2055
|
+
label_to_nid[normalised.lower()] = n["id"]
|
|
2056
|
+
|
|
2057
|
+
seen_call_pairs: set[tuple[str, str]] = set()
|
|
2058
|
+
_SKIP_KEYWORDS = frozenset({
|
|
2059
|
+
"def", "defp", "defmodule", "defmacro", "defmacrop",
|
|
2060
|
+
"defstruct", "defprotocol", "defimpl", "defguard",
|
|
2061
|
+
"alias", "import", "require", "use",
|
|
2062
|
+
"if", "unless", "case", "cond", "with", "for",
|
|
2063
|
+
})
|
|
2064
|
+
|
|
2065
|
+
def walk_calls(node, caller_nid: str) -> None:
|
|
2066
|
+
if node.type != "call":
|
|
2067
|
+
for child in node.children:
|
|
2068
|
+
walk_calls(child, caller_nid)
|
|
2069
|
+
return
|
|
2070
|
+
for child in node.children:
|
|
2071
|
+
if child.type == "identifier":
|
|
2072
|
+
kw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2073
|
+
if kw in _SKIP_KEYWORDS:
|
|
2074
|
+
for c in node.children:
|
|
2075
|
+
walk_calls(c, caller_nid)
|
|
2076
|
+
return
|
|
2077
|
+
break
|
|
2078
|
+
callee_name: str | None = None
|
|
2079
|
+
for child in node.children:
|
|
2080
|
+
if child.type == "dot":
|
|
2081
|
+
dot_text = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2082
|
+
parts = dot_text.rstrip(".").split(".")
|
|
2083
|
+
if parts:
|
|
2084
|
+
callee_name = parts[-1]
|
|
2085
|
+
break
|
|
2086
|
+
if child.type == "identifier":
|
|
2087
|
+
callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
|
|
2088
|
+
break
|
|
2089
|
+
if callee_name:
|
|
2090
|
+
tgt_nid = label_to_nid.get(callee_name.lower())
|
|
2091
|
+
if tgt_nid and tgt_nid != caller_nid:
|
|
2092
|
+
pair = (caller_nid, tgt_nid)
|
|
2093
|
+
if pair not in seen_call_pairs:
|
|
2094
|
+
seen_call_pairs.add(pair)
|
|
2095
|
+
add_edge(caller_nid, tgt_nid, "calls",
|
|
2096
|
+
node.start_point[0] + 1, confidence="INFERRED", weight=0.8)
|
|
2097
|
+
for child in node.children:
|
|
2098
|
+
walk_calls(child, caller_nid)
|
|
2099
|
+
|
|
2100
|
+
for caller_nid, body in function_bodies:
|
|
2101
|
+
walk_calls(body, caller_nid)
|
|
2102
|
+
|
|
2103
|
+
clean_edges = [e for e in edges if e["source"] in seen_ids and
|
|
2104
|
+
(e["target"] in seen_ids or e["relation"] == "imports")]
|
|
2105
|
+
return {"nodes": nodes, "edges": clean_edges, "input_tokens": 0, "output_tokens": 0}
|
|
2106
|
+
|
|
2107
|
+
|
|
1931
2108
|
# ── Main extract and collect_files ────────────────────────────────────────────
|
|
1932
2109
|
|
|
1933
2110
|
def extract(paths: list[Path]) -> dict:
|
|
@@ -1980,6 +2157,8 @@ def extract(paths: list[Path]) -> dict:
|
|
|
1980
2157
|
".toc": extract_lua,
|
|
1981
2158
|
".zig": extract_zig,
|
|
1982
2159
|
".ps1": extract_powershell,
|
|
2160
|
+
".ex": extract_elixir,
|
|
2161
|
+
".exs": extract_elixir,
|
|
1983
2162
|
}
|
|
1984
2163
|
|
|
1985
2164
|
for path in paths:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphifyy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-swift
|
|
|
50
50
|
Requires-Dist: tree-sitter-lua
|
|
51
51
|
Requires-Dist: tree-sitter-zig
|
|
52
52
|
Requires-Dist: tree-sitter-powershell
|
|
53
|
+
Requires-Dist: tree-sitter-elixir
|
|
53
54
|
Provides-Extra: mcp
|
|
54
55
|
Requires-Dist: mcp; extra == "mcp"
|
|
55
56
|
Provides-Extra: neo4j
|
|
@@ -61,6 +62,9 @@ Provides-Extra: watch
|
|
|
61
62
|
Requires-Dist: watchdog; extra == "watch"
|
|
62
63
|
Provides-Extra: leiden
|
|
63
64
|
Requires-Dist: graspologic; extra == "leiden"
|
|
65
|
+
Provides-Extra: office
|
|
66
|
+
Requires-Dist: python-docx; extra == "office"
|
|
67
|
+
Requires-Dist: openpyxl; extra == "office"
|
|
64
68
|
Provides-Extra: all
|
|
65
69
|
Requires-Dist: mcp; extra == "all"
|
|
66
70
|
Requires-Dist: neo4j; extra == "all"
|
|
@@ -68,6 +72,8 @@ Requires-Dist: pypdf; extra == "all"
|
|
|
68
72
|
Requires-Dist: html2text; extra == "all"
|
|
69
73
|
Requires-Dist: watchdog; extra == "all"
|
|
70
74
|
Requires-Dist: graspologic; extra == "all"
|
|
75
|
+
Requires-Dist: python-docx; extra == "all"
|
|
76
|
+
Requires-Dist: openpyxl; extra == "all"
|
|
71
77
|
Dynamic: license-file
|
|
72
78
|
|
|
73
79
|
# graphify
|
|
@@ -239,6 +245,7 @@ Works with any mix of file types:
|
|
|
239
245
|
|------|-----------|------------|
|
|
240
246
|
| Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
|
|
241
247
|
| Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
|
|
248
|
+
| Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
|
|
242
249
|
| Papers | `.pdf` | Citation mining + concept extraction |
|
|
243
250
|
| Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
|
|
244
251
|
|
|
@@ -17,6 +17,7 @@ tree-sitter-swift
|
|
|
17
17
|
tree-sitter-lua
|
|
18
18
|
tree-sitter-zig
|
|
19
19
|
tree-sitter-powershell
|
|
20
|
+
tree-sitter-elixir
|
|
20
21
|
|
|
21
22
|
[all]
|
|
22
23
|
mcp
|
|
@@ -25,6 +26,8 @@ pypdf
|
|
|
25
26
|
html2text
|
|
26
27
|
watchdog
|
|
27
28
|
graspologic
|
|
29
|
+
python-docx
|
|
30
|
+
openpyxl
|
|
28
31
|
|
|
29
32
|
[leiden]
|
|
30
33
|
graspologic
|
|
@@ -35,6 +38,10 @@ mcp
|
|
|
35
38
|
[neo4j]
|
|
36
39
|
neo4j
|
|
37
40
|
|
|
41
|
+
[office]
|
|
42
|
+
python-docx
|
|
43
|
+
openpyxl
|
|
44
|
+
|
|
38
45
|
[pdf]
|
|
39
46
|
pypdf
|
|
40
47
|
html2text
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphifyy"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.6"
|
|
8
8
|
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
"tree-sitter-lua",
|
|
31
31
|
"tree-sitter-zig",
|
|
32
32
|
"tree-sitter-powershell",
|
|
33
|
+
"tree-sitter-elixir",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
@@ -43,7 +44,8 @@ neo4j = ["neo4j"]
|
|
|
43
44
|
pdf = ["pypdf", "html2text"]
|
|
44
45
|
watch = ["watchdog"]
|
|
45
46
|
leiden = ["graspologic"]
|
|
46
|
-
|
|
47
|
+
office = ["python-docx", "openpyxl"]
|
|
48
|
+
all = ["mcp", "neo4j", "pypdf", "html2text", "watchdog", "graspologic", "python-docx", "openpyxl"]
|
|
47
49
|
|
|
48
50
|
[project.scripts]
|
|
49
51
|
graphify = "graphify.__main__:main"
|
|
@@ -61,7 +61,7 @@ def test_collect_files_from_dir():
|
|
|
61
61
|
supported = {".py", ".js", ".ts", ".tsx", ".go", ".rs",
|
|
62
62
|
".java", ".c", ".cpp", ".cc", ".cxx", ".rb",
|
|
63
63
|
".cs", ".kt", ".kts", ".scala", ".php", ".h", ".hpp",
|
|
64
|
-
".swift", ".lua", ".toc", ".zig", ".ps1"}
|
|
64
|
+
".swift", ".lua", ".toc", ".zig", ".ps1", ".ex", ".exs"}
|
|
65
65
|
assert all(f.suffix in supported for f in files)
|
|
66
66
|
assert len(files) > 0
|
|
67
67
|
|
|
@@ -337,3 +337,37 @@ def test_swift_emits_calls():
|
|
|
337
337
|
r = extract_swift(FIXTURES / "sample.swift")
|
|
338
338
|
calls = _calls(r)
|
|
339
339
|
assert any("process" in src and "validate" in tgt for src, tgt in calls)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ── Elixir ────────────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
from graphify.extract import extract_elixir
|
|
345
|
+
|
|
346
|
+
def test_elixir_finds_module():
|
|
347
|
+
r = extract_elixir(FIXTURES / "sample.ex")
|
|
348
|
+
assert "error" not in r
|
|
349
|
+
labels = [n["label"] for n in r["nodes"]]
|
|
350
|
+
assert any("MyApp.Accounts.User" in l for l in labels)
|
|
351
|
+
|
|
352
|
+
def test_elixir_finds_functions():
|
|
353
|
+
r = extract_elixir(FIXTURES / "sample.ex")
|
|
354
|
+
labels = [n["label"] for n in r["nodes"]]
|
|
355
|
+
assert any("create" in l for l in labels)
|
|
356
|
+
assert any("find" in l for l in labels)
|
|
357
|
+
assert any("validate" in l for l in labels)
|
|
358
|
+
|
|
359
|
+
def test_elixir_finds_imports():
|
|
360
|
+
r = extract_elixir(FIXTURES / "sample.ex")
|
|
361
|
+
import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
|
|
362
|
+
assert len(import_edges) >= 2
|
|
363
|
+
|
|
364
|
+
def test_elixir_finds_calls():
|
|
365
|
+
r = extract_elixir(FIXTURES / "sample.ex")
|
|
366
|
+
calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
|
|
367
|
+
labels = {n["id"]: n["label"] for n in r["nodes"]}
|
|
368
|
+
assert any("create" in labels.get(src, "") and "validate" in labels.get(tgt, "") for src, tgt in calls)
|
|
369
|
+
|
|
370
|
+
def test_elixir_method_edges():
|
|
371
|
+
r = extract_elixir(FIXTURES / "sample.ex")
|
|
372
|
+
methods = [e for e in r["edges"] if e["relation"] == "method"]
|
|
373
|
+
assert len(methods) >= 3
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|