feat(ocaml): add OCaml .ml/.mli extractor (optional tree-sitter-ocaml extra)

New graphify/extractors/ocaml.py handles both the implementation grammar
(language_ocaml, .ml) and the interface grammar (language_ocaml_interface,
.mli). Emits nodes for modules, top-level/module-level values and functions,
types and their variant constructors; edges for defines/contains, open ->
imports_from, and application -> calls. Qualified paths (Geo.area) resolve to
the final value name, not the module qualifier; local let ... in bindings do
not mint nodes or steal call attribution. Cross-file open/call targets are
sourceless stubs so the corpus rewire collapses them onto the unique real
definition (no #1402 sourced-stub leak).

Wired into detect.py (CODE_EXTENSIONS), extract.py (dispatch +
_EXTRA_FOR_EXTENSION), pyproject.toml ([ocaml] extra + all + dev dep), and
README. Adds tests/test_ocaml.py (behind importorskip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-08-14 14:17:07 +01:00
co-authored by Claude Opus 4.8
parent 7fe58b0b0f
commit 0302bfa7af
8 changed files with 431 additions and 5 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.9.42 (unreleased)
- Feature: OCaml `.ml`/`.mli` extraction via tree-sitter-ocaml (optional `[ocaml]` extra). Extracts modules, top-level and module-level values/functions, types and their variant constructors, `open` imports, and function calls; qualified calls (`Geo.area`) resolve to the value, and cross-file `open`/call targets collapse onto the unique real definition via the corpus stub rewire.
- Fix: a JS/TS `for...of` / `for...in` loop binding is now shadowed, so passing it as a call argument no longer fabricates an `indirect_call` edge to an unrelated same-named callable (#2685, thanks @ousamabenyounes); completes the loop/closure/catch shadow family (#2568/#2569/#2517).
- Fix: graph provenance (`built_at_commit`) is stamped from the analysed repository rather than the shell's working directory, so `graphify extract` run from elsewhere records the target's commit, not the caller's (#2534 family; #2699, thanks @C0KERNEL).
- Fix: `affected` resolves a seed passed as a `./`-relative path (or an absolute path when run from the repo root) instead of silently returning nothing (#2707, thanks @phudayyy). Note: an absolute-path seed still requires the working directory to be the analysed repo root.
+3 -1
View File
@@ -266,6 +266,7 @@ Codex users also need `multi_agent = true` under `[features]` in `~/.codex/confi
| `dm` | BYOND DreamMaker `.dm`/`.dme` AST extraction (may need a C compiler + `python3-dev` if no wheel matches your platform) | `uv tool install "graphifyy[dm]"` |
| `terraform` | Terraform / HCL `.tf`/`.tfvars`/`.hcl` AST extraction | `uv tool install "graphifyy[terraform]"` |
| `pascal` | Pascal / Delphi `.pas`/`.dpr`/`.dpk`/`.inc` AST extraction (more accurate `calls`/`inherits` edges; falls back to a regex extractor when absent) | `uv tool install "graphifyy[pascal]"` |
| `ocaml` | OCaml `.ml`/`.mli` AST extraction | `uv tool install "graphifyy[ocaml]"` |
| `chinese` | Chinese query segmentation (jieba) | `uv tool install "graphifyy[chinese]"` |
| `all` | Everything above | `uv tool install "graphifyy[all]"` |
@@ -335,9 +336,10 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg
| Type | Extensions |
|------|-----------|
| Code (36 tree-sitter grammars) | `.py .ts .mts .cts .js .jsx .tsx .mjs .go .rs .java .c .cpp .cc .cxx .h .hpp .cu .cuh .metal .rb .cs .kt .kts .scala .php .swift .lua .luau .toc .zig .ps1 .psm1 .psd1 .ex .exs .m .mm .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`; `.mts`/`.cts` reuse the TypeScript grammar, `.cc`/`.cxx` and CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) |
| Code (37 tree-sitter grammars) | `.py .ts .mts .cts .js .jsx .tsx .mjs .go .rs .java .c .cpp .cc .cxx .h .hpp .cu .cuh .metal .rb .cs .kt .kts .scala .php .swift .lua .luau .toc .zig .ps1 .psm1 .psd1 .ex .exs .m .mm .ml .mli .jl .vue .svelte .astro .groovy .gradle .dart .v .sv .svh .sql .f .f90 .f95 .f03 .f08 .pas .pp .dpr .dpk .lpr .inc .dfm .lfm .lpk .sh .bash .json .dm .dme .dmi .dmm .dmf .sln .slnx .csproj .fsproj .vbproj .xaml .razor .cshtml` (`.dm`/`.dme` requires `uv tool install graphifyy[dm]`, `.ml`/`.mli` requires `uv tool install graphifyy[ocaml]`; `.mts`/`.cts` reuse the TypeScript grammar, `.cc`/`.cxx` and CUDA `.cu`/`.cuh` and Metal `.metal` reuse the C++ grammar) |
| Salesforce Apex | `.cls .trigger` (regex-based; classes, interfaces, enums, methods, triggers, SOQL/DML edges) |
| Terraform / HCL | `.tf .tfvars .hcl` (requires `uv tool install graphifyy[terraform]`) |
| OCaml | `.ml .mli` (requires `uv tool install graphifyy[ocaml]`) |
| 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) |
+1 -1
View File
@@ -41,7 +41,7 @@ _MANIFEST_PATH = str(out_path("manifest.json"))
_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', '.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'}
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'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
+5
View File
@@ -46,6 +46,7 @@ from graphify.extractors.fortran import _cpp_preprocess, extract_fortran # noqa
from graphify.extractors.go import _GO_PREDECLARED_FUNCS, extract_go # noqa: F401
from graphify.extractors.json_config import extract_json # noqa: F401
from graphify.extractors.markdown import extract_markdown # noqa: F401
from graphify.extractors.ocaml import extract_ocaml # noqa: F401
from graphify.extractors.pascal_forms import extract_delphi_form, extract_lazarus_form # noqa: F401
from graphify.extractors.powershell import extract_powershell, extract_powershell_manifest # noqa: F401
from graphify.extractors.razor import extract_razor # noqa: F401
@@ -4823,6 +4824,8 @@ _DISPATCH: dict[str, Any] = {
".svelte": extract_svelte,
".astro": extract_astro,
".dart": extract_dart,
".ml": extract_ocaml,
".mli": extract_ocaml,
".v": extract_verilog,
".sv": extract_verilog,
".svh": extract_verilog,
@@ -4875,6 +4878,8 @@ _EXTRA_FOR_EXTENSION = {
".hcl": "terraform",
".dm": "dm",
".dme": "dm",
".ml": "ocaml",
".mli": "ocaml",
}
# Substrings an extractor's error carries to classify why a dependency-backed
+249
View File
@@ -0,0 +1,249 @@
"""OCaml extractor (own module, optional tree-sitter-ocaml dependency).
Handles implementation files (.ml, `language_ocaml`) and interface files
(.mli, `language_ocaml_interface`). Interfaces have no expression bodies, so
they contribute definitions and `open` imports but no `calls` edges.
"""
from __future__ import annotations
from pathlib import Path
from graphify.extractors.base import _file_stem, _make_id, _read_text
def extract_ocaml(path: Path) -> dict:
"""Extract modules, values, functions, types, variant constructors, `open`
imports, and (for .ml) function calls from an OCaml source file."""
try:
import tree_sitter_ocaml as tsocaml
from tree_sitter import Language, Parser
except ImportError:
return {"nodes": [], "edges": [], "error": "tree-sitter-ocaml not installed"}
try:
if path.suffix == ".mli":
language = Language(tsocaml.language_ocaml_interface())
else:
language = Language(tsocaml.language_ocaml())
parser = Parser(language)
source = path.read_bytes()
tree = parser.parse(source)
root = tree.root_node
except Exception as e:
return {"nodes": [], "edges": [], "error": str(e)}
stem = _file_stem(path)
str_path = str(path)
nodes: list[dict] = []
edges: list[dict] = []
seen_ids: set[str] = set()
# Same-file definition table for call resolution. A name defined more than
# once in the file is marked ambiguous and never resolved locally.
local_defs: dict[str, str] = {}
ambiguous: set[str] = set()
# (caller_nid, callee_name, line) recorded on pass 1, resolved on pass 2 so
# that forward references (e.g. `let rec ... and ...`) resolve correctly.
call_sites: list[tuple[str, str, int]] = []
def add_node(nid: str, label: str, line: int) -> None:
if nid not in seen_ids:
seen_ids.add(nid)
nodes.append({
"id": nid,
"label": label,
"file_type": "code",
"source_file": str_path,
"source_location": f"L{line}",
})
def add_edge(src: str, tgt: str, relation: str, line: int,
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
edges.append({
"source": src,
"target": tgt,
"relation": relation,
"confidence": confidence,
"source_file": str_path,
"source_location": f"L{line}",
"weight": weight,
})
file_nid = _make_id(str(path))
add_node(file_nid, path.name, 1)
def ref_stub(name: str) -> str:
"""Mint a SOURCELESS stub for a cross-file reference target (an `open`ed
module or a call to a name not defined in this file). The corpus-level
rewire collapses it onto the unique real definition of that name;
external names (Stdlib, Core, ...) dangle and are pruned. A *sourced*
stub here would bake this file's path into the id and block the rewire
(the #1402 phantom-duplicate bug)."""
nid = _make_id(name)
if nid not in seen_ids:
seen_ids.add(nid)
nodes.append({
"id": nid,
"label": name,
"file_type": "code",
"source_file": "",
"source_location": "",
"origin_file": str_path,
})
return nid
def line_of(node) -> int:
return node.start_point[0] + 1
def named_child_text(node, child_type: str) -> str | None:
for child in node.children:
if child.type == child_type:
return _read_text(child, source)
return None
def last_name(path_node) -> str | None:
"""Final component of a *_path node: `Geo.area` -> `area`,
`Stdlib.List` -> `List`. The qualifier is nested under a child
`module_path`, so the final name is always a *direct* child of the path
node -- a deep search would wrongly return the module qualifier."""
found: str | None = None
for n in path_node.children:
if n.type in ("value_name", "module_name", "constructor_name"):
found = _read_text(n, source)
return found
def register_def(name: str, nid: str) -> None:
if name in ambiguous:
return
if name in local_defs and local_defs[name] != nid:
ambiguous.add(name)
local_defs.pop(name, None)
return
local_defs[name] = nid
def emit_type(binding, container_nid: str) -> None:
type_name = named_child_text(binding, "type_constructor")
if not type_name:
return
line = line_of(binding)
nid = _make_id(stem, type_name)
add_node(nid, type_name, line)
add_edge(container_nid, nid, "defines" if container_nid == file_nid else "contains", line)
register_def(type_name, nid)
# Variant constructors: variant_declaration -> constructor_declaration -> constructor_name
for vd in binding.children:
if vd.type != "variant_declaration":
continue
for cd in vd.children:
if cd.type != "constructor_declaration":
continue
cname = named_child_text(cd, "constructor_name")
if not cname:
continue
cnid = _make_id(stem, type_name, cname)
add_node(cnid, cname, line_of(cd))
add_edge(nid, cnid, "contains", line_of(cd))
def walk(node, container_nid: str, enclosing_value: str) -> None:
t = node.type
if t == "open_module":
mp = next((c for c in node.children if c.type == "module_path"), None)
name = last_name(mp) if mp is not None else None
if name:
add_edge(container_nid, ref_stub(name), "imports_from", line_of(node),
confidence="INFERRED")
return
if t == "module_definition":
binding = next((c for c in node.children if c.type == "module_binding"), None)
if binding is not None:
mname = named_child_text(binding, "module_name")
if mname:
line = line_of(node)
mnid = _make_id(stem, mname)
add_node(mnid, mname, line)
add_edge(container_nid, mnid,
"defines" if container_nid == file_nid else "contains", line)
register_def(mname, mnid)
for child in binding.children:
walk(child, mnid, enclosing_value)
return
if t == "module_type_definition": # .mli
mname = named_child_text(node, "module_type_name")
if mname:
line = line_of(node)
mnid = _make_id(stem, mname)
add_node(mnid, mname, line)
add_edge(container_nid, mnid,
"defines" if container_nid == file_nid else "contains", line)
register_def(mname, mnid)
for child in node.children:
walk(child, mnid, enclosing_value)
return
if t == "value_definition": # .ml
# Only structure/top-level bindings are real definitions. A local
# `let x = e in body` is also a value_definition, but nested under a
# let_expression; it must NOT mint a node or steal call attribution
# from the enclosing named function.
is_toplevel = node.parent is not None and node.parent.type in (
"compilation_unit", "structure")
for lb in node.children:
if lb.type != "let_binding":
continue
vname = named_child_text(lb, "value_name")
new_scope = enclosing_value
if vname and is_toplevel:
line = line_of(lb)
nid = _make_id(stem, vname)
add_node(nid, vname, line)
add_edge(container_nid, nid,
"defines" if container_nid == file_nid else "contains", line)
register_def(vname, nid)
new_scope = nid
# Descend into the body (unit/pattern/local bindings keep the
# outer scope) to collect nested definitions and call sites.
for child in lb.children:
walk(child, container_nid, new_scope)
return
if t == "value_specification": # .mli
vname = named_child_text(node, "value_name")
if vname:
line = line_of(node)
nid = _make_id(stem, vname)
add_node(nid, vname, line)
add_edge(container_nid, nid,
"defines" if container_nid == file_nid else "contains", line)
register_def(vname, nid)
return
if t == "type_definition":
for binding in node.children:
if binding.type == "type_binding":
emit_type(binding, container_nid)
return
if t == "application_expression":
fn = node.named_children[0] if node.named_children else None
if fn is not None and fn.type == "value_path":
callee = last_name(fn)
if callee:
caller = enclosing_value if enclosing_value else file_nid
call_sites.append((caller, callee, line_of(node)))
# Fall through: arguments may contain further applications/definitions.
for child in node.children:
walk(child, container_nid, enclosing_value)
walk(root, file_nid, "")
for caller, callee, line in call_sites:
if callee in local_defs:
add_edge(caller, local_defs[callee], "calls", line)
else:
add_edge(caller, ref_stub(callee), "calls", line, confidence="INFERRED")
return {"nodes": nodes, "edges": edges}
+5 -1
View File
@@ -85,7 +85,10 @@ pascal = ["tree-sitter-pascal"]
# avoids breaking the default `uv tool install graphifyy` for everyone (#1104).
dm = ["tree-sitter-dm"]
terraform = ["tree-sitter-hcl"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal"]
# tree-sitter-ocaml ships prebuilt abi3 wheels for every platform, so no C
# toolchain is needed; kept optional because OCaml is a niche corpus language.
ocaml = ["tree-sitter-ocaml"]
all = ["mcp>=1,<3", "starlette>=1.3.1,<2", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal", "tree-sitter-ocaml"]
[project.scripts]
graphify = "graphify.__main__:main"
@@ -108,6 +111,7 @@ dev = [
"wheel>=0.47.0",
"tomli>=2.0 ; python_version < '3.11'",
"tree-sitter-hcl>=1.2.0",
"tree-sitter-ocaml>=0.25.0",
]
[tool.uv]
+141
View File
@@ -0,0 +1,141 @@
"""Tests for the OCaml extractor (graphify/extractors/ocaml.py)."""
from __future__ import annotations
from pathlib import Path
import pytest
pytest.importorskip("tree_sitter_ocaml")
from graphify.extract import extract_ocaml
def _write(tmp_path: Path, name: str, body: str) -> Path:
p = tmp_path / name
p.write_text(body, encoding="utf-8")
return p
def _labels(r) -> set[str]:
return {n["label"] for n in r["nodes"]}
def _rel_pairs(r, relation: str) -> set[tuple[str, str]]:
lab = {n["id"]: n["label"] for n in r["nodes"]}
return {
(lab.get(e["source"], e["source"]), lab.get(e["target"], e["target"]))
for e in r["edges"]
if e["relation"] == relation
}
IMPL = """\
open Stdlib
module Shapes = struct
type shape = Circle | Square | Triangle
let pi = 3.14159
let area_of radius =
let squared = radius *. radius in
pi *. squared
let describe r =
let a = area_of r in
print_float a
end
let main () =
let a = Shapes.area_of 2.0 in
print_float a
"""
def test_impl_defines_module_values_and_types(tmp_path):
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
assert "error" not in r
labels = _labels(r)
# module, values/functions, type, variant constructors
assert {"Shapes", "pi", "area_of", "describe", "main", "shape"} <= labels
assert {"Circle", "Square", "Triangle"} <= labels
def test_impl_containment_and_defines(tmp_path):
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
# file defines the top-level module and top-level `main`
defines = _rel_pairs(r, "defines")
assert ("shapes.ml", "Shapes") in defines
assert ("shapes.ml", "main") in defines
# module contains its members
contains = _rel_pairs(r, "contains")
assert ("Shapes", "area_of") in contains
assert ("Shapes", "shape") in contains
# variant constructors are contained by their type
assert ("shape", "Circle") in contains
def test_impl_calls_resolve_same_file(tmp_path):
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
calls = _rel_pairs(r, "calls")
# describe -> area_of is a same-file, unambiguous resolution
assert ("describe", "area_of") in calls
# a qualified call `Shapes.area_of` resolves to the value `area_of`, NOT the
# module qualifier `Shapes`.
assert ("main", "area_of") in calls
assert ("main", "Shapes") not in calls
def test_impl_open_emits_import(tmp_path):
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
imports = _rel_pairs(r, "imports_from")
assert ("shapes.ml", "Stdlib") in imports
def test_open_stub_is_sourceless(tmp_path):
# An `open`ed external module must be a SOURCELESS stub so the corpus rewire
# can collapse/prune it without baking this file's path into the id (#1402).
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
stubs = [n for n in r["nodes"] if n["label"] == "Stdlib"]
assert stubs and all(n["source_file"] == "" for n in stubs)
# origin_file is an internal rewire hint, never a real source path.
assert all(n.get("source_location") == "" for n in stubs)
INTERFACE = """\
open Base
module type Store = sig
type t
val make : int -> t
val size : t -> int
end
type color = Red | Green | Blue
val hello : string -> unit
"""
def test_interface_defines_signatures(tmp_path):
r = extract_ocaml(_write(tmp_path, "store.mli", INTERFACE))
assert "error" not in r
labels = _labels(r)
assert {"Store", "make", "size", "color", "hello"} <= labels
assert {"Red", "Green", "Blue"} <= labels
# interfaces have no expression bodies -> no calls
assert not [e for e in r["edges"] if e["relation"] == "calls"]
def test_no_dangling_edges(tmp_path):
r = extract_ocaml(_write(tmp_path, "shapes.ml", IMPL))
ids = {n["id"] for n in r["nodes"]}
for e in r["edges"]:
assert e["source"] in ids, e
assert e["target"] in ids, e
def test_missing_file_returns_error(tmp_path):
r = extract_ocaml(tmp_path / "nope.ml")
assert r["nodes"] == [] and r["edges"] == []
assert "error" in r
Generated
+26 -2
View File
@@ -1090,7 +1090,7 @@ wheels = [
[[package]]
name = "graphifyy"
version = "0.9.41"
version = "0.9.42"
source = { editable = "." }
dependencies = [
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -1147,6 +1147,7 @@ all = [
{ name = "tiktoken" },
{ name = "tree-sitter-dm" },
{ name = "tree-sitter-hcl" },
{ name = "tree-sitter-ocaml" },
{ name = "tree-sitter-pascal" },
{ name = "tree-sitter-sql" },
{ name = "watchdog" },
@@ -1188,6 +1189,9 @@ mcp = [
neo4j = [
{ name = "neo4j" },
]
ocaml = [
{ name = "tree-sitter-ocaml" },
]
office = [
{ name = "openpyxl" },
{ name = "python-docx" },
@@ -1243,6 +1247,7 @@ dev = [
{ name = "setuptools" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "tree-sitter-hcl" },
{ name = "tree-sitter-ocaml" },
{ name = "wheel" },
]
@@ -1312,6 +1317,8 @@ requires-dist = [
{ name = "tree-sitter-kotlin", specifier = ">=1.0,<2.0" },
{ name = "tree-sitter-lua", specifier = ">=0.2,<0.6" },
{ name = "tree-sitter-objc", specifier = ">=3.0,<4.0" },
{ name = "tree-sitter-ocaml", marker = "extra == 'all'" },
{ name = "tree-sitter-ocaml", marker = "extra == 'ocaml'" },
{ name = "tree-sitter-pascal", marker = "extra == 'all'" },
{ name = "tree-sitter-pascal", marker = "extra == 'pascal'" },
{ name = "tree-sitter-php", specifier = ">=0.23,<0.25" },
@@ -1331,7 +1338,7 @@ requires-dist = [
{ name = "yt-dlp", marker = "extra == 'all'", specifier = ">=2026.6.9" },
{ name = "yt-dlp", marker = "extra == 'video'", specifier = ">=2026.6.9" },
]
provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "all"]
provides-extras = ["mcp", "neo4j", "falkordb", "pdf", "watch", "svg", "leiden", "office", "google", "postgres", "video", "kimi", "ollama", "bedrock", "anthropic", "gemini", "openai", "chinese", "sql", "pascal", "dm", "terraform", "ocaml", "all"]
[package.metadata.requires-dev]
dev = [
@@ -1349,6 +1356,7 @@ dev = [
{ name = "setuptools", specifier = ">=82.0.1" },
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" },
{ name = "tree-sitter-hcl", specifier = ">=1.2.0" },
{ name = "tree-sitter-ocaml", specifier = ">=0.25.0" },
{ name = "wheel", specifier = ">=0.47.0" },
]
@@ -4734,6 +4742,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/ec/34de4da134f48373d2986137e785da86f4df2b70f688307856588a473cff/tree_sitter_objc-3.0.2-cp39-abi3-win_arm64.whl", hash = "sha256:9a99d9b81a4e507bd33329be136928b3ebe424ce8b9d6b8a8339083ceb453b5b", size = 301378, upload-time = "2024-12-16T00:37:36.424Z" },
]
[[package]]
name = "tree-sitter-ocaml"
version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/51/eab58652cc54221192a82bb0cb2c7ce50954e2b18db4fcfb8791d6dfd7fe/tree_sitter_ocaml-0.25.0.tar.gz", hash = "sha256:f5833261dd85cf170da2bb4dd0273e138bf488f1d6937d38c511b2918c19a51e", size = 1642387, upload-time = "2026-05-09T19:26:40.773Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/c6/0184e6872f24e8ad1eab26e7c9dee6e5ac7980b1cfe4885095453f946579/tree_sitter_ocaml-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ee544bb2c28926e173d78de2ba210c8093fc73cc897da9d93fc9062a9be9695f", size = 663463, upload-time = "2026-05-09T19:26:28.795Z" },
{ url = "https://files.pythonhosted.org/packages/07/04/bdd1257dae21cc1172aed235f7f4e908a21d826d303d5d961e9c566ab2e5/tree_sitter_ocaml-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b376fda33b092befd9cc889b2d1f71d5fca643daff04255990580b563ccea63f", size = 711155, upload-time = "2026-05-09T19:26:30.566Z" },
{ url = "https://files.pythonhosted.org/packages/1b/c8/8edd26733d1fe67eb781fd35910c8f22e2f115a941993d8367076f78222c/tree_sitter_ocaml-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2035b643ff801c0a1023026284bb462bbc002f98f577395ade660faf21ff65db", size = 713718, upload-time = "2026-05-09T19:26:32.119Z" },
{ url = "https://files.pythonhosted.org/packages/34/b7/0c67840dfa5e2f3a1c5536c0c9f946a7f573a42fdfd2b2a57fd3d88cabdc/tree_sitter_ocaml-0.25.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7897e35905f851a4465674f88cfe116e58b99d0e60630ad82a211a115402a7d", size = 715599, upload-time = "2026-05-09T19:26:33.76Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e1/0f7fc18d3b2e02630b266fee6c2f3416460623746da786cd712a82012036/tree_sitter_ocaml-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8fa32599619e21026d21145d0a1af4a6ef17b36d51c2b9ba9dc470ea5d301047", size = 712176, upload-time = "2026-05-09T19:26:35.094Z" },
{ url = "https://files.pythonhosted.org/packages/3b/03/1e0563fff25f21d9d67b7f6edb60a260c19dc9a22f5b0b03700f6b585471/tree_sitter_ocaml-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:72ead1435b43d8434c93e9e71c0429d74e90b5005dfcc43cb7776ae4842a110a", size = 714160, upload-time = "2026-05-09T19:26:36.406Z" },
{ url = "https://files.pythonhosted.org/packages/8d/59/5263f0209a9fc31a53aeaa2d7be02bb0cab6420b04fa5dee58800929d6c7/tree_sitter_ocaml-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e0b41127fb602878e757c25f2ecc451688f82f909102f422e782a9b7d4fb3ff0", size = 665343, upload-time = "2026-05-09T19:26:38.029Z" },
{ url = "https://files.pythonhosted.org/packages/4f/05/ea0517c683cb5572dd8bd688e119bddbb8d2fd3974fcb1db00e724fa836b/tree_sitter_ocaml-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:e370b50ddbc1de652966a78697258d21a26d59a618ff5807cd343811a480372e", size = 661149, upload-time = "2026-05-09T19:26:39.331Z" },
]
[[package]]
name = "tree-sitter-pascal"
version = "0.11.0"