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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg
| Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) |
| OCaml | `.ml .mli` (requires `uv tool install graphifyy[ocaml]`) |
| Common Lisp | `.lisp .cl .lsp .asd` (requires `uv tool install graphifyy[commonlisp]`) |
| systemd units | `.service .timer .socket .target .path .mount .slice` (INI, no grammar) — a timer/socket/path `activates` its unit, a service `runs` the script named by `ExecStart=`, `Documentation=file://` becomes `documented_by`, and `After=`/`Wants=`/`Requires=`/`WantedBy=`… become unit→unit edges (only to units in the same directory, so the host's `network.target` is never fabricated) |
| MCP configs | `.mcp.json` `mcp.json` `mcp_servers.json` `claude_desktop_config.json` — extracts server nodes, package refs, env var requirements |
| Package manifests | `apm.yml` `pyproject.toml` `go.mod` `pom.xml` — one canonical package node per package (by name) plus `depends_on` edges, so a package referenced from many manifests is a single hub |
| Docs | `.md .mdx .qmd .html .txt .rst .yaml .yml` (markdown `[text](./other.md)` links and `[[wikilinks]]` become `references` edges between docs) |
Expand Down
2 changes: 1 addition & 1 deletion graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class FileType(str, Enum):
_MTIME_COARSE_S = 2.0
_MTIME_SUBSECOND_S = 0.05

CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.lisp', '.cl', '.lsp', '.asd'}
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.ml', '.mli', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger', '.lisp', '.cl', '.lsp', '.asd', '.service', '.timer', '.socket', '.target', '.path', '.mount', '.slice'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
Expand Down
3 changes: 3 additions & 0 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from graphify.extractors.razor import extract_razor # noqa: F401
from graphify.extractors.rust import extract_rust # noqa: F401
from graphify.extractors.sln import extract_sln # noqa: F401
from graphify.extractors.systemd import SYSTEMD_UNIT_EXTENSIONS, extract_systemd # noqa: F401
from graphify.extractors.sql import extract_sql # noqa: F401
from graphify.extractors.terraform import extract_terraform # noqa: F401
from graphify.extractors.verilog import extract_verilog # noqa: F401
Expand Down Expand Up @@ -5252,6 +5253,8 @@ def add_existing_edge(edge: dict) -> None:
".dmf": extract_dmf,
".sln": extract_sln,
".slnx": extract_slnx,
# systemd units (#2848): .service .timer .socket .target .path .mount .slice
**{ext: extract_systemd for ext in sorted(SYSTEMD_UNIT_EXTENSIONS)},
".csproj": extract_csproj,
".fsproj": extract_csproj,
".vbproj": extract_csproj,
Expand Down
15 changes: 14 additions & 1 deletion graphify/extractors/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,19 @@ def _blank(s: str) -> str:
out.append(_blank(src[pos:]))
return "".join(out), lang

# Relations whose edges may carry a transient ``target_file`` stamp naming the
# file the target id was minted from. Import-style edges (#1814/#1983), plus
# the systemd unit edges (#2848): a `.timer` and its `.service` share the
# extension-less file-node id, so without the stamp the timer's `activates`
# edge would be disambiguated against the timer's own path — a self-loop.
_TARGET_FILE_RELATIONS: frozenset[str] = frozenset({
"imports", "imports_from", "re_exports",
"activates", "runs", "documented_by",
"after", "before", "wants", "requires", "binds_to", "part_of", "conflicts",
"wanted_by", "required_by",
})


def _source_key(source_file: str, root: Path) -> str:
if not source_file:
return ""
Expand Down Expand Up @@ -766,7 +779,7 @@ def _disambiguate_colliding_node_ids(
# every language and to re_exports. `pop` it as we consume it: this is the
# hint's only reader, and its absolute path must not persist into graph.json.
target_file = edge.pop("target_file", None)
if target_file and edge.get("relation") in ("imports", "imports_from", "re_exports"):
if target_file and edge.get("relation") in _TARGET_FILE_RELATIONS:
target_edge_key = _source_key(str(target_file), root)
else:
target_edge_key = edge_source_key
Expand Down
Loading
Loading