Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,9 @@ def main() -> None:
if "--force" in argv[2:]:
force = True
argv = [a for a in argv if a != "--force"]
semantic = "--semantic" in argv[2:]
if semantic:
argv = [a for a in argv if a != "--semantic"]
if len(argv) > 2:
watch_path = Path(argv[2])
else:
Expand All @@ -1534,12 +1537,17 @@ def main() -> None:
print(f"error: path not found: {watch_path}", file=sys.stderr)
sys.exit(1)
from graphify.watch import _rebuild_code
print(f"Re-extracting code files in {watch_path} (no LLM needed)...")
ok = _rebuild_code(watch_path, force=force)
msg = f"Re-extracting code files in {watch_path}"
if semantic:
msg += " with semantic LLM..."
else:
msg += " (no LLM needed)..."
print(msg)
ok = _rebuild_code(watch_path, force=force, semantic=semantic)
if ok:
print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.")
if not os.environ.get("MOONSHOT_API_KEY") and not os.environ.get("GRAPHIFY_NO_TIPS"):
print("Tip: set MOONSHOT_API_KEY to use Kimi K2.6 for semantic extraction — 3x cheaper, richer graphs. pip install 'graphifyy[kimi]'")
if not os.environ.get("MOONSHOT_API_KEY") and not os.environ.get("OPENAI_API_KEY") and not os.environ.get("GRAPHIFY_NO_TIPS"):
print("Tip: set OPENAI_API_KEY (gpt-5.4-mini) or MOONSHOT_API_KEY (kimi-k2.6) for semantic extraction — richer graphs. pip install 'graphifyy[all]'")
else:
print("Nothing to update or rebuild failed — check output above.", file=sys.stderr)
sys.exit(1)
Expand Down
2 changes: 1 addition & 1 deletion graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class FileType(str, Enum):

_MANIFEST_PATH = "graphify-out/manifest.json"

CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv', '.sql', '.r'}
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv', '.sql', '.r', '.pas', '.pp', '.inc', '.lpr', '.lfm', '.lpi'}
DOC_EXTENSIONS = {'.md', '.mdx', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
Expand Down
189 changes: 181 additions & 8 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,39 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st
break


def _import_pascal(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
"""Extract moduleName from declUses in Pascal."""
for child in node.children:
if child.type == "moduleName":
raw = _read_text(child, source)
if raw:
tgt_nid = _make_id(raw)
edges.append({
"source": file_nid,
"target": tgt_nid,
"relation": "imports",
"context": "import",
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": f"L{node.start_point[0] + 1}",
"weight": 1.0,
})


_PASCAL_CONFIG = LanguageConfig(
ts_module="tree_sitter_language_pack",
ts_language_fn="pascal",
class_types=frozenset({"declClass"}),
function_types=frozenset({"declFunc", "defFunc", "declProc", "defProc"}),
import_types=frozenset({"declUses"}),
call_types=frozenset({"exprCall"}),
name_fallback_child_types=("identifier", "moduleName"),
body_fallback_child_types=("block", "interface", "implementation"),
function_boundary_types=frozenset({"defFunc", "defProc"}),
import_handler=_import_pascal,
)


def _read_csharp_type_name(node, source: bytes) -> str | None:
"""Resolve a readable C# type name from a field/type node."""
if node is None:
Expand Down Expand Up @@ -852,15 +885,19 @@ def _read_csharp_type_name(node, source: bytes) -> str | None:
def _extract_generic(path: Path, config: LanguageConfig) -> dict:
"""Generic AST extractor driven by LanguageConfig."""
try:
mod = importlib.import_module(config.ts_module)
from tree_sitter import Language, Parser
lang_fn = getattr(mod, config.ts_language_fn, None)
if lang_fn is None:
# Fallback for PHP: try "language_php" then "language"
lang_fn = getattr(mod, "language", None)
if lang_fn is None:
return {"nodes": [], "edges": [], "error": f"No language function in {config.ts_module}"}
language = Language(lang_fn())
if config.ts_module == "tree_sitter_language_pack":
import tree_sitter_language_pack
language = tree_sitter_language_pack.get_language(config.ts_language_fn)
else:
mod = importlib.import_module(config.ts_module)
lang_fn = getattr(mod, config.ts_language_fn, None)
if lang_fn is None:
# Fallback for PHP: try "language_php" then "language"
lang_fn = getattr(mod, "language", None)
if lang_fn is None:
return {"nodes": [], "edges": [], "error": f"No language function in {config.ts_module}"}
language = Language(lang_fn())
except ImportError:
return {"nodes": [], "edges": [], "error": f"{config.ts_module} not installed"}
except Exception as e:
Expand Down Expand Up @@ -1743,6 +1780,136 @@ def extract_blade(path: Path) -> dict:
return {"nodes": nodes, "edges": edges}


def extract_pascal(path: Path) -> dict:
"""Extract units, classes, functions, and imports from a .pas/.pp file."""
try:
return _extract_generic(path, _PASCAL_CONFIG)
except Exception as e:
return {"nodes": [], "edges": [], "error": str(e)}


def extract_lfm(path: Path) -> dict:
"""Extract UI components and event handlers from Lazarus Form files (.lfm)."""
nodes: list[dict] = []
edges: list[dict] = []
try:
content = path.read_text(encoding="utf-8", errors="replace")
str_path = str(path)
file_nid = _make_id(str_path)
nodes.append({
"id": file_nid,
"label": path.name,
"file_type": "code",
"source_file": str_path,
"source_location": "L1",
})

for i, line in enumerate(content.splitlines()):
trimmed = line.strip()
# Match object/inherited definitions: "object Form1: TForm1"
m = re.match(r"^\s*(object|inherited)\s+(\w+)\s*:\s*(\w+)", trimmed, re.I)
if m:
obj_name, obj_type = m.group(2), m.group(3)
obj_nid = _make_id(str_path, obj_name)
nodes.append({
"id": obj_nid,
"label": f"{obj_name}: {obj_type}",
"file_type": "code",
"source_file": str_path,
"source_location": f"L{i+1}",
})
edges.append({
"source": file_nid,
"target": obj_nid,
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": f"L{i+1}",
"weight": 1.0
})
continue

# Match references to event handlers: "OnClick = Button1Click"
m = re.match(r"^\s*(\w+)\s*=\s*(\w+)", trimmed)
if m:
prop, handler = m.group(1), m.group(2)
if prop.startswith("On"):
handler_nid = _make_id(handler)
edges.append({
"source": file_nid,
"target": handler_nid,
"relation": "triggers",
"context": prop,
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": f"L{i+1}",
"weight": 1.0
})
return {"nodes": nodes, "edges": edges}
except Exception as e:
return {"nodes": [], "edges": [], "error": str(e)}


def extract_lpi(path: Path) -> dict:
"""Extract units and project dependencies from Lazarus Project files (.lpi)."""
nodes: list[dict] = []
edges: list[dict] = []
try:
import xml.etree.ElementTree as ET
tree = ET.parse(path)
root = tree.getroot()
str_path = str(path)
file_nid = _make_id(str_path)
nodes.append({
"id": file_nid,
"label": path.name,
"file_type": "code",
"source_file": str_path,
"source_location": "L1",
})

# Find units in the project
for unit in root.findall(".//Units/Unit"):
filename = unit.get("Filename")
if filename:
unit_nid = _make_id(str(path.parent / filename))
edges.append({
"source": file_nid,
"target": unit_nid,
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": None,
"weight": 1.0
})

# Find required packages
for pkg in root.findall(".//RequiredPackages/Item"):
pkg_name = pkg.get("PackageName")
if pkg_name:
pkg_nid = _make_id(pkg_name)
nodes.append({
"id": pkg_nid,
"label": pkg_name,
"file_type": "code",
"source_file": str_path,
"source_location": None,
})
edges.append({
"source": file_nid,
"target": pkg_nid,
"relation": "depends_on",
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": None,
"weight": 1.0
})

return {"nodes": nodes, "edges": edges}
except Exception as e:
return {"nodes": [], "edges": [], "error": str(e)}


def extract_dart(path: Path) -> dict:
"""Extract classes, mixins, functions, imports, and calls from a .dart file using regex."""
try:
Expand Down Expand Up @@ -3685,6 +3852,12 @@ def _check_tree_sitter_version() -> None:
".vue": extract_js,
".svelte": extract_js,
".dart": extract_dart,
".pas": extract_pascal,
".pp": extract_pascal,
".inc": extract_pascal,
".lpr": extract_pascal,
".lfm": extract_lfm,
".lpi": extract_lpi,
".v": extract_verilog,
".sv": extract_verilog,
".sql": extract_sql,
Expand Down
Loading