diff --git a/CHANGELOG.md b/CHANGELOG.md index 70170db56..fe0bc82e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) -## Unreleased +## 0.9.7 (2026-07-06) +- Fix: Java standard-library types are no longer emitted as `references` noise (#1603, thanks @NydiaChung). A `_JAVA_BUILTIN_TYPES` skip list now suppresses ubiquitous `java.lang`/`java.util`/`java.io`/`java.time`/`java.math`/`java.nio.file` type names (`String`, `List`, `Map`, `Optional`, `Integer`, `Exception`, ...) at the type-ref walker; they never resolve to a project node, so edges to them were pure noise (mirrors `_GO_PREDECLARED_TYPES`/`_PYTHON_ANNOTATION_NOISE`). Nested user-type generic arguments still resolve: `List` drops the `List` edge but keeps `Item`. +- Feat: added a `pascal` optional extra for AST-quality Pascal/Delphi extraction (#1616, thanks @vinicius-l-machado). `extract_pascal` already used tree-sitter-pascal when present (with a regex fallback), but the grammar was never declared in the package metadata, so the AST path never ran out of the box. `uv tool install "graphifyy[pascal]"` now opts into it (also included in `[all]`); tree-sitter-pascal ships prebuilt wheels for every platform, so no C toolchain is needed. On a mid-size Delphi codebase the AST path yields notably more accurate `calls`/`inherits` edges than the regex fallback. +- Feat: JS/TS rationale comments and ADR/RFC citations are now extracted (#1599, thanks @niltonmourafilho-arch). Python already turned `# NOTE:`/`# WHY:`/`# HACK:` comments and docstrings into `rationale` nodes, but JS/TS comments were discarded. `extract_js` now runs the same post-pass: `// NOTE:`-style and block-comment rationale become `rationale` nodes with `rationale_for` edges, and `ADR-NNNN` / `RFC NNNN` citations become `doc_ref` nodes with `cites` edges from the file, closing the code-to-design-doc gap in mixed corpora at zero LLM cost (pure line scan). Files with no such comments are unaffected. +- Fix: extensionless executables with a shebang (CLI entry points like `devctl`, `manage`) are now extracted (#1683, thanks @Stashub). `detect` already classified a `#!/usr/bin/env bash`/`python`/`node`/... file as code, but `_get_extractor` dispatched on the suffix alone and returned nothing, so the file was silently dropped from the graph and its doc-referenced symbols stayed dangling stubs. Extensionless files now resolve their extractor from the shebang interpreter (`_SHEBANG_DISPATCH`), mirroring detection. Only interpreters with a real extractor are mapped (python, bash-family, node, ruby, lua, php, julia); others (perl, fish, Rscript) stay skipped rather than mis-parsed by a wrong grammar. - Feat: Ruby `include`/`extend`/`prepend ` now emits a `mixes_in` edge to the module (#1668, thanks @krishnateja7). Concerns/mixins are the composition mechanism in Rails, but they produced no edges, so the blast radius of editing a shared concern was invisible to `affected`. A constant-argument mixin inside a class or module body now resolves to the module node (reusing the #1634 candidate logic and the #1640 module nodes, under the single-owner guard) and emits `Class --mixes_in--> Module`, which `affected` already traverses. `extend self` and non-constant arguments are skipped; an ambiguous or undefined module produces no edge. - Feat: `affected ` now reaches callers that bind to the class's method nodes (#1669, thanks @krishnateja7). Since #1634 binds `Service.call` precisely to the `def self.call` method node, a class-level `affected` query missed those callers because `method`/`contains` are (correctly) not general-traversal relations. The reverse walk now seeds from the root's own member nodes (one `method`/`contains` hop outward) so method-bound callers are reachable from the class, with no change to the general traversal (no forward noise) and the member nodes themselves are not reported as hits. - Fix: capitalized/mixed-case file extensions are no longer silently skipped (#1671, thanks @raman118). `collect_files` and `_get_extractor` matched suffixes case-sensitively, so `App.PY`, `script.JS`, `Lib.Ts`, etc. fell through and were never extracted. Suffix matching now falls back to the lowercased form for both file discovery and extractor dispatch (including `.blade.php`); an unsupported extension like `.xyz` is still skipped. diff --git a/README.md b/README.md index febe20e9b..1923d17c0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Graphify

+

+ Graphify-Labs%2Fgraphify | Trendshift +

+

🇺🇸 English | 🇨🇳 简体中文 | 🇯🇵 日本語 | 🇰🇷 한국어 | 🇩🇪 Deutsch | 🇫🇷 Français | 🇪🇸 Español | 🇮🇳 हिन्दी | 🇧🇷 Português | 🇷🇺 Русский | 🇸🇦 العربية | 🇮🇷 فارسی | 🇮🇹 Italiano | 🇵🇱 Polski | 🇳🇱 Nederlands | 🇹🇷 Türkçe | 🇺🇦 Українська | 🇻🇳 Tiếng Việt | 🇮🇩 Bahasa Indonesia | 🇸🇪 Svenska | 🇬🇷 Ελληνικά | 🇷🇴 Română | 🇨🇿 Čeština | 🇫🇮 Suomi | 🇩🇰 Dansk | 🇳🇴 Norsk | 🇭🇺 Magyar | 🇹🇭 ภาษาไทย | 🇺🇿 Oʻzbekcha | 🇹🇼 繁體中文 | 🇵🇭 Filipino

@@ -18,15 +22,22 @@ X

-Type `/graphify` in your AI coding assistant and it maps your entire project — code, docs, PDFs, images, videos — into a knowledge graph you can query instead of grepping through files. +Type `/graphify` in your AI coding assistant and it maps your entire project (code, docs, PDFs, images, videos) into a **knowledge graph** you can **query instead of grepping** through files. + +

+ graphify's interactive graph.html showing the FastAPI codebase as a force-directed knowledge graph with a legend of detected communities +

+

+ The FastAPI codebase mapped by graphify. Every node is a concept, colors are detected communities, and the whole thing is clickable in graph.html. +

-Works in Claude Code, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, Amp, OpenClaw, Factory Droid, Trae, Hermes, Kimi Code, Kiro, Pi, Devin CLI, and Google Antigravity. +**Works in:** Claude Code, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, Amp, OpenClaw, Factory Droid, Trae, Hermes, Kimi Code, Kiro, Pi, Devin CLI, and Google Antigravity. ``` /graphify . ``` -That's it. You get three files: +That's it. You get **three files**: ``` graphify-out/ @@ -43,11 +54,32 @@ graphify export callflow-html --- +## What it does + +A knowledge graph is not a vector index: it is **deterministic**, **every edge is explained**, and it costs **zero tokens to build**. + +| Capability | What you get | +|---|---| +| **God nodes** | The most-connected concepts, so you see what everything flows through | +| **Communities** | The graph split into subsystems (Leiden), with LLM-free labels | +| **Cross-file links** | `calls` / `imports` / `inherits` / `mixes_in` resolved across ~40 languages via tree-sitter AST | +| **Query, path, explain** | Ask a question, trace the path between two things, or explain one concept, all against `graph.json` | +| **Rationale + doc refs** | `# NOTE:` / `# WHY:` comments and ADR/RFC citations become first-class nodes linked to the code | +| **Beyond code** | Docs, PDFs, images, and video/audio all map into the same graph | +| **Local-first** | AST plus a local embedder build the graph with zero LLM credits; your code never leaves your machine | + +--- + ## Benchmarks -On an open harness where every system uses the same model, the same budgets, and a judge blind-validated against a second judge (90.6% agreement, Cohen's kappa 0.81), graphify has the **best retrieval recall of any memory system tested on LOCOMO** (about 10x mem0), **ties for best on LongMemEval-S** (76%), and builds its graph with **zero LLM credits**. It also beats a grep+read baseline on real code-intelligence tasks (ERPNext, ~1M LOC) at a fraction of the token cost. +| Benchmark | Metric | graphify | Field | +|---|---|---|---| +| LOCOMO (n=300) | recall@10 | **0.497** | mem0 0.048, supermemory 0.149 | +| LOCOMO (n=300) | QA accuracy | **45.3%** | supermemory 49.7%, mem0 27.3% | +| LongMemEval-S (n=50) | QA accuracy | **76%** | tied best | +| Graph build | LLM credits | **0** | per-token for most systems | -Full methodology, per-system tables, judge validation, and reproduction commands: **[BENCHMARKS.md](./BENCHMARKS.md)**. +Every system ran on the same harness with the same model and budgets, scored by a judge blind-validated against a second judge (90.6% agreement, Cohen's kappa 0.81). Full per-system tables, the code-intelligence result, and reproduction commands: **[BENCHMARKS.md](./BENCHMARKS.md)**. --- @@ -126,7 +158,8 @@ for example `graphify claude install --project` or `graphify codex install --pro > **Git hooks and uv tool / pipx:** `graphify hook install` embeds the current interpreter path directly into the hook scripts at install time, so the post-commit hook fires correctly even in GUI git clients and CI runners where `~/.local/bin` is not on PATH. If you reinstall or upgrade graphify, re-run `graphify hook install` to refresh the embedded path. -### Pick your platform +
+Pick your platform (20+ assistants, click to expand) | Platform | Install command | |----------|----------------| @@ -154,15 +187,16 @@ for example `graphify claude install --project` or `graphify codex install --pro | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | -Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. CodeBuddy uses the same Agent tool and PreToolUse hook mechanism as Claude Code. Factory Droid uses the `Task` tool for parallel subagent dispatch. OpenClaw and Aider use sequential extraction (parallel agent support is still early on those platforms). Trae uses the Agent tool for parallel subagent dispatch and does **not** support PreToolUse hooks — AGENTS.md is the always-on mechanism. +Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. CodeBuddy uses the same Agent tool and PreToolUse hook mechanism as Claude Code. Factory Droid uses the `Task` tool for parallel subagent dispatch. OpenClaw and Aider use sequential extraction (parallel agent support is still early on those platforms). Trae uses the Agent tool for parallel subagent dispatch and does **not** support `PreToolUse` hooks, so AGENTS.md is the always-on mechanism. `--platform agents` (alias `--platform skills`) targets the generic cross-framework [Agent-Skills](https://github.com/anthropics/skills) locations: the spec's user-global `~/.agents/skills/` (read by `npx skills` and spec-compliant frameworks) for a global install, and `./.agents/skills/` for a project (`--project`) install. The bare `graphify install` stays single-platform (Claude Code) by design — use the named `agents` platform when you want the skill discoverable by any framework that reads `.agents/skills`. > Codex uses `$graphify` instead of `/graphify`. -### Optional extras +
-Install only what you need: +
+Optional extras (install only what you need) | Extra | What it adds | Install | |---|---|---| @@ -185,9 +219,12 @@ Install only what you need: | `postgres` | Live PostgreSQL introspection (`--postgres DSN`) | `uv tool install "graphifyy[postgres]"` | | `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]"` | | `chinese` | Chinese query segmentation (jieba) | `uv tool install "graphifyy[chinese]"` | | `all` | Everything above | `uv tool install "graphifyy[all]"` | +
+ --- ## Make your assistant always use the graph @@ -219,19 +256,24 @@ Run this once in your project after building a graph: | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | -This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions — preferring scoped queries like `graphify query ""` over reading the full report or grepping raw files. On platforms that support payload-bearing hooks (Claude Code, Gemini CLI), a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. On the others (Codex, OpenCode, Cursor, etc.), the persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. `GRAPH_REPORT.md` is still available for broad architecture review. +This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions, preferring scoped queries like `graphify query ""` over reading the full report or grepping raw files. -**CodeBuddy** does the same two things as Claude Code: writes a `CODEBUDDY.md` section telling CodeBuddy to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and installs **PreToolUse hooks** (`.codebuddy/settings.json`) that fire before Bash search commands and file reads, nudging toward `graphify query` instead. +- **Hook platforms** (Claude Code, Gemini CLI): a hook fires automatically before search-style tool calls (and, on Claude Code, before reading source files one by one via the Read/Glob tools) and nudges your assistant toward the graph path. +- **Instruction-file platforms** (Codex, OpenCode, Cursor, etc.): persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. -**Codex** writes to `AGENTS.md` and also installs a **PreToolUse hook** in `.codex/hooks.json` that fires before every Bash tool call — same always-on mechanism as Claude Code. +`GRAPH_REPORT.md` is still available for broad architecture review. -To remove graphify from all platforms at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). +**CodeBuddy** does the same two things as Claude Code: writes a `CODEBUDDY.md` section telling CodeBuddy to read `graphify-out/GRAPH_REPORT.md` before answering architecture questions, and installs `PreToolUse` hooks (`.codebuddy/settings.json`) that fire before Bash search commands and file reads, nudging toward `graphify query` instead. ---- +**Codex** writes to `AGENTS.md` and also installs a `PreToolUse` hook in `.codex/hooks.json` that fires before every Bash tool call, same always-on mechanism as Claude Code. -**Kilo Code** installs the Graphify skill to `~/.config/kilo/skills/graphify/SKILL.md` and a native `/graphify` command to `~/.config/kilo/command/graphify.md`. `graphify kilo install` also writes `AGENTS.md` plus a native **`tool.execute.before` plugin** (`.kilo/plugins/graphify.js` + `.kilo/kilo.json` or `.kilo/kilo.jsonc` registration) so Kilo gets the same always-on graph reminder behavior through native `.kilo` config. +**Kilo Code** installs the Graphify skill to `~/.config/kilo/skills/graphify/SKILL.md` and a native `/graphify` command to `~/.config/kilo/command/graphify.md`. `graphify kilo install` also writes `AGENTS.md` plus a native `tool.execute.before` plugin (`.kilo/plugins/graphify.js` + `.kilo/kilo.json` or `.kilo/kilo.jsonc` registration) so Kilo gets the same always-on graph reminder behavior through native `.kilo` config. -**Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true` — Cursor includes it in every conversation automatically, no hook needed. +**Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true`, so Cursor includes it in every conversation automatically, no hook needed. + +To remove graphify from all platforms at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). + +--- ## What's in the report @@ -260,7 +302,7 @@ To remove graphify from all platforms at once: `graphify uninstall` (add `--purg | Video / Audio | `.mp4 .mov .mp3 .wav` and more (requires `uv tool install graphifyy[video]`) | | YouTube / URLs | any video URL (requires `uv tool install graphifyy[video]`) | -Code is extracted locally with no API calls (AST via tree-sitter). Everything else goes through your AI assistant's model API. +Code is extracted **locally with no API calls** (AST via tree-sitter). Everything else goes through your AI assistant's model API. Google Drive for desktop `.gdoc`, `.gsheet`, and `.gslides` files are shortcut pointers, not document content. To include native Google Docs, Sheets, and Slides @@ -447,7 +489,7 @@ These are only needed for **headless / CI extraction** (`graphify extract`). Whe - **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine. - **Docs, PDFs, images** — sent to your AI assistant for semantic extraction (via the `/graphify` skill, using whatever model your IDE session runs). Headless `graphify extract` requires `GEMINI_API_KEY` / `GOOGLE_API_KEY` (Gemini), `MOONSHOT_API_KEY` (Kimi), `ANTHROPIC_API_KEY` (Claude), `OPENAI_API_KEY` (OpenAI), `DEEPSEEK_API_KEY` (DeepSeek), a running Ollama instance (`OLLAMA_BASE_URL`), AWS credentials via the standard provider chain (Bedrock - no API key needed, uses IAM), or the `claude` CLI binary (Claude Code - no API key needed, uses your Claude subscription). The `--dedup-llm` flag uses the same key. - **Data residency** — `graphify extract` auto-detects which provider to use based on which API key is set (priority: Gemini → Kimi → Claude → OpenAI → DeepSeek → Azure → Bedrock → Ollama). For code with data-residency requirements, use `--backend ollama` (fully local) or pass an explicit `--backend` flag. Kimi (`MOONSHOT_API_KEY`) routes to Moonshot AI servers in China. -- No telemetry, no usage tracking, no analytics. +- **No telemetry**, no usage tracking, no analytics. - **Query logging** — every `graphify query`, `graphify path`, `graphify explain`, and MCP `query_graph` call is logged to `~/.cache/graphify-queries.log` in JSON Lines format (timestamp, question, corpus, nodes returned, duration). Full subgraph responses are **not** stored by default. Set `GRAPHIFY_QUERY_LOG_DISABLE=1` to opt out, or `GRAPHIFY_QUERY_LOG=/dev/null` to silence without disabling the code path. --- @@ -515,6 +557,14 @@ uv tool upgrade graphifyy graphify install # overwrites the skill file ``` +**Claude Code prompt cache invalidated after every `graphify extract`** +Graphify writes output files (`graph.json`, `graphify-out/`) into the workspace. If those paths aren't ignored, every write invalidates Claude Code's prompt cache, forcing a full re-upload at cache-write rates on the next turn. Add them to `.claudeignore`: +```text +# .claudeignore +graph.json +graphify-out/ +``` + --- ## Full command reference @@ -681,7 +731,7 @@ graphify label ./my-project --backend=openai --model gpt-4o # force a specific --- -## Built on graphify — Penpax +## Built on graphify: Penpax [**Penpax**](https://graphifylabs.ai) is the always-on layer built on top of graphify — it applies the same graph approach to your entire working life: meetings, browser history, emails, files, and code, updating continuously in the background. diff --git a/docs/graph-hero.png b/docs/graph-hero.png new file mode 100644 index 000000000..f4968f6d1 Binary files /dev/null and b/docs/graph-hero.png differ diff --git a/graphify/extract.py b/graphify/extract.py index f14f4638d..be7e99448 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -868,6 +868,69 @@ def _java_type_parameters_in_scope(node, source: bytes) -> frozenset[str]: return frozenset(names) +# java.lang (auto-imported) plus the ubiquitous java.util / java.io / java.time / +# java.util.{stream,function,concurrent} / java.math / java.nio.file types that +# appear as field, parameter, return, and generic-argument annotations. They never +# resolve to a project node, so emitting `references` edges to them is pure noise +# (mirrors _GO_PREDECLARED_TYPES / _PYTHON_ANNOTATION_NOISE). Suppressed at the +# type-ref walker so they are never created as nodes or emitted as edges. The +# boxed-scalar/`void` primitives are already dropped by grammar node type above; +# these are the class/interface names the grammar reports as identifiers. +_JAVA_BUILTIN_TYPES = frozenset({ + # java.lang — core + "Object", "String", "CharSequence", "StringBuilder", "StringBuffer", + "Number", "Byte", "Short", "Integer", "Long", "Float", "Double", + "Boolean", "Character", "Void", "Class", "Enum", "Record", "Math", + "System", "Thread", "Runnable", "Comparable", "Iterable", "Cloneable", + "AutoCloseable", "Appendable", "Readable", "Process", "ProcessBuilder", + "Runtime", "Package", "ThreadLocal", "InheritableThreadLocal", + # java.lang — throwables + "Throwable", "Exception", "RuntimeException", "Error", + "IllegalArgumentException", "IllegalStateException", "NullPointerException", + "IndexOutOfBoundsException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "NumberFormatException", "ArithmeticException", + "UnsupportedOperationException", "InterruptedException", + "CloneNotSupportedException", "SecurityException", "StackOverflowError", + "OutOfMemoryError", "AssertionError", + # java.util — collections & core + "Collection", "List", "ArrayList", "LinkedList", "Vector", "Stack", + "Set", "HashSet", "LinkedHashSet", "TreeSet", "SortedSet", "NavigableSet", + "EnumSet", "Map", "HashMap", "LinkedHashMap", "TreeMap", "SortedMap", + "NavigableMap", "Hashtable", "EnumMap", "Properties", "Queue", "Deque", + "ArrayDeque", "PriorityQueue", "Iterator", "ListIterator", "Comparator", + "Optional", "OptionalInt", "OptionalLong", "OptionalDouble", "Collections", + "Arrays", "Objects", "Date", "Calendar", "Random", "UUID", "Scanner", + "StringJoiner", "StringTokenizer", "BitSet", "Spliterator", "Locale", + "NoSuchElementException", "ConcurrentModificationException", + # java.util.stream + "Stream", "IntStream", "LongStream", "DoubleStream", "Collector", + "Collectors", + # java.util.function + "Function", "BiFunction", "Consumer", "BiConsumer", "Supplier", + "Predicate", "BiPredicate", "UnaryOperator", "BinaryOperator", + "IntFunction", "ToIntFunction", "ToLongFunction", "ToDoubleFunction", + # java.util.concurrent + "Callable", "Future", "CompletableFuture", "CompletionStage", "Executor", + "ExecutorService", "Executors", "ScheduledExecutorService", "TimeUnit", + "ConcurrentHashMap", "ConcurrentMap", "CopyOnWriteArrayList", + "BlockingQueue", "CountDownLatch", "Semaphore", "CyclicBarrier", + "AtomicInteger", "AtomicLong", "AtomicBoolean", "AtomicReference", + # java.time + "Instant", "Duration", "Period", "LocalDate", "LocalTime", "LocalDateTime", + "ZonedDateTime", "OffsetDateTime", "ZoneId", "ZoneOffset", "DayOfWeek", + "Month", "Year", "Clock", "DateTimeFormatter", + # java.io / java.nio.file + "IOException", "UncheckedIOException", "FileNotFoundException", "File", + "InputStream", "OutputStream", "Reader", "Writer", "BufferedReader", + "BufferedWriter", "InputStreamReader", "OutputStreamWriter", "FileReader", + "FileWriter", "PrintStream", "PrintWriter", "ByteArrayInputStream", + "ByteArrayOutputStream", "Serializable", "Closeable", "Path", "Paths", + "Files", + # java.math + "BigDecimal", "BigInteger", +}) + + def _java_collect_type_refs( node, source: bytes, @@ -885,19 +948,23 @@ def _java_collect_type_refs( return if t == "type_identifier": name = _read_text(node, source) - if name and name not in skip: + if name and name not in skip and name not in _JAVA_BUILTIN_TYPES: out.append((name, "generic_arg" if generic else "type")) return if t == "scoped_type_identifier": text = _read_text(node, source).rsplit(".", 1)[-1] - if text: + if text and text not in _JAVA_BUILTIN_TYPES: out.append((text, "generic_arg" if generic else "type")) return if t == "generic_type": for c in node.children: if c.type in ("type_identifier", "scoped_type_identifier"): text = _read_text(c, source).rsplit(".", 1)[-1] - if text and (c.type == "scoped_type_identifier" or text not in skip): + if ( + text + and text not in _JAVA_BUILTIN_TYPES + and (c.type == "scoped_type_identifier" or text not in skip) + ): out.append((text, "generic_arg" if generic else "type")) break for c in node.children: @@ -5799,7 +5866,114 @@ def extract_js(path: Path) -> dict: config = _TS_CONFIG else: config = _JS_CONFIG - return _extract_generic(path, config) + result = _extract_generic(path, config) + if "error" not in result: + _extract_js_rationale(path, result) + return result + + +# ── JS/TS rationale + doc-reference extraction ──────────────────────────────── +# +# Parity with _extract_python_rationale: Python files get rationale nodes from +# docstrings and `# NOTE:`-style comments, but JS/TS comments were discarded +# entirely. That silently drops two high-value signals in mixed corpora: +# 1. rationale comments (`// NOTE:`, `// WHY:`, ...) — same as Python; +# 2. architecture-decision references (`ADR-0011`, `RFC 793`) that teams +# conventionally cite in file/function headers. These are the natural +# join points between code and design docs in the same graph — without +# them, code<->ADR edges never form even when the code cites the ADR. + +_JS_RATIONALE_PREFIXES = ( + "// NOTE:", "// IMPORTANT:", "// HACK:", "// WHY:", "// RATIONALE:", + "// TODO:", "// FIXME:", + "* NOTE:", "* IMPORTANT:", "* HACK:", "* WHY:", "* RATIONALE:", + "* TODO:", "* FIXME:", +) + +# Doc-reference tokens worth first-classing as graph nodes. Deliberately +# conservative: ADR-NNNN (Architecture Decision Records, any zero padding) +# and RFC NNNN / RFC-NNNN. +_JS_DOC_REF_RE = re.compile(r"\b(ADR[- ]?\d{1,5}|RFC[- ]?\d{1,5})\b", re.IGNORECASE) + +# Only look for doc references inside comments, not string literals or code. +_JS_COMMENT_LINE_RE = re.compile(r"^\s*(//|/\*|\*)") + + +def _extract_js_rationale(path: Path, result: dict) -> None: + """Post-pass: extract rationale comments and doc references from JS/TS source. + Mutates result in-place by appending to result['nodes'] and result['edges']. + """ + try: + source_text = path.read_text(encoding="utf-8", errors="replace") + except Exception: + return + + stem = _file_stem(path) + str_path = str(path) + nodes = result["nodes"] + edges = result["edges"] + seen_ids = {n["id"] for n in nodes} + file_nid = _make_id(str(path)) + seen_doc_refs: set[str] = set() + + def _add_rationale(text: str, line: int) -> None: + label = text[:80].replace("\r\n", " ").replace("\r", " ").replace("\n", " ").strip() + rid = _make_id(stem, "rationale", str(line)) + if rid not in seen_ids: + seen_ids.add(rid) + nodes.append({ + "id": rid, + "label": label, + "file_type": "rationale", + "source_file": str_path, + "source_location": f"L{line}", + }) + edges.append({ + "source": rid, + "target": file_nid, + "relation": "rationale_for", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{line}", + "weight": 1.0, + }) + + def _add_doc_ref(token: str, line: int) -> None: + # Normalize "adr 11" / "ADR-0011" spellings to a canonical "ADR-0011" + # style label so references to the same document collapse to one node. + kind, num = re.match(r"([A-Za-z]+)[- ]?(\d+)", token).groups() + kind = kind.upper() + label = f"{kind}-{num.zfill(4)}" if kind == "ADR" else f"{kind}-{num}" + if label in seen_doc_refs: + return + seen_doc_refs.add(label) + rid = _make_id("docref", label) + if rid not in seen_ids: + seen_ids.add(rid) + nodes.append({ + "id": rid, + "label": label, + "file_type": "doc_ref", + "source_file": str_path, + "source_location": f"L{line}", + }) + edges.append({ + "source": file_nid, + "target": rid, + "relation": "cites", + "confidence": "EXTRACTED", + "source_file": str_path, + "source_location": f"L{line}", + "weight": 1.0, + }) + + for lineno, line_text in enumerate(source_text.splitlines(), start=1): + stripped = line_text.strip() + if any(stripped.startswith(p) for p in _JS_RATIONALE_PREFIXES): + _add_rationale(stripped.lstrip("/* "), lineno) + if _JS_COMMENT_LINE_RE.match(line_text): + for m in _JS_DOC_REF_RE.finditer(stripped): + _add_doc_ref(m.group(1), lineno) def extract_svelte(path: Path) -> dict: @@ -15821,6 +15995,30 @@ def _body_of(block): } +# Extensionless executables (CLI entry points like `devctl` or `manage`) carry +# their language in the shebang, not the suffix. detect.classify_file already +# routes them to the CODE path via _shebang_interpreter; _get_extractor must +# honor the same signal or these files are classified as code and then silently +# dropped by extraction. Only interpreters with a real extractor are mapped — +# detect's wider set (perl, fish, tcsh, Rscript) stays unmapped and skipped. +_SHEBANG_DISPATCH: dict[str, Any] = { + "python": extract_python, + "python2": extract_python, + "python3": extract_python, + "bash": extract_bash, + "sh": extract_bash, + "dash": extract_bash, + "zsh": extract_bash, + "ksh": extract_bash, + "node": extract_js, + "nodejs": extract_js, + "ruby": extract_ruby, + "lua": extract_lua, + "php": extract_php, + "julia": extract_julia, +} + + # ObjC-only directives. They are illegal in C and C++, so finding one in a `.h` # file is a near-zero-false-positive signal that the header is Objective-C (and so # belongs to extract_objc, not extract_c). `@property` is deliberately excluded: it @@ -15908,6 +16106,14 @@ def _get_extractor(path: Path) -> Any | None: # grammar has no class_specifier). Reroute to extract_cpp (#1547). if _is_cpp_header(path): return extract_cpp + # Extensionless files: resolve by shebang, mirroring detect.classify_file. + # Without this, detect labels e.g. `#!/usr/bin/env bash` CLIs as code but + # extraction returns no extractor and the file silently contributes nothing. + if not suffix: + from graphify.detect import _shebang_interpreter + interp = _shebang_interpreter(path) + if interp is not None: + return _SHEBANG_DISPATCH.get(interp) return _DISPATCH.get(suffix) diff --git a/pyproject.toml b/pyproject.toml index 30d2a6da5..9f3f48881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.6" +version = "0.9.7" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } @@ -70,12 +70,17 @@ gemini = ["openai", "tiktoken"] openai = ["openai", "tiktoken"] chinese = ["jieba"] sql = ["tree-sitter-sql"] +# extract_pascal() uses tree-sitter-pascal for AST-quality extraction (more +# accurate calls/inherits edges) and falls back to a regex extractor when it is +# absent (#781), so this stays optional. Unlike tree-sitter-dm below, it ships +# prebuilt wheels for every platform (win/macOS/Linux), so no C toolchain needed. +pascal = ["tree-sitter-pascal"] # tree-sitter-dm (BYOND DreamMaker) ships only a Windows wheel, so on Linux/Mac it # must compile from source (needs a C toolchain + python3-dev). Keeping it optional # avoids breaking the default `uv tool install graphifyy` for everyone (#1104). dm = ["tree-sitter-dm"] terraform = ["tree-sitter-hcl"] -all = ["mcp", "starlette>=1.3.1", "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"] +all = ["mcp", "starlette>=1.3.1", "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"] [project.scripts] graphify = "graphify.__main__:main" diff --git a/tests/test_extract.py b/tests/test_extract.py index ae88e364b..4e76ad120 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -1514,6 +1514,56 @@ def test_extract_json_via_dispatch(): assert _get_extractor(Path("foo.json")) is extract_json +def test_extensionless_shebang_via_dispatch(tmp_path): + """Extensionless CLIs resolve their extractor from the shebang, mirroring + detect.classify_file — otherwise detect labels them code and extraction + silently drops them.""" + from graphify.extract import _get_extractor + + cli = tmp_path / "devctl" + cli.write_text("#!/usr/bin/env bash\necho hi\n") + assert _get_extractor(cli) is extract_bash + + pytool = tmp_path / "manage" + pytool.write_text("#!/usr/bin/env python3\nprint('hi')\n") + assert _get_extractor(pytool) is extract_python + + # env -S split-args form is handled by the shared shebang parser + split = tmp_path / "runner" + split.write_text("#!/usr/bin/env -S bash -eu\necho hi\n") + assert _get_extractor(split) is extract_bash + + +def test_extensionless_without_usable_shebang_stays_unsupported(tmp_path): + from graphify.extract import _get_extractor + + plain = tmp_path / "LICENSE-COPY" + plain.write_text("plain text, no shebang\n") + assert _get_extractor(plain) is None + + # Interpreter known to detect but with no AST extractor: stays skipped + # rather than being mis-parsed by a wrong grammar. + perl = tmp_path / "legacy" + perl.write_text("#!/usr/bin/env perl\nprint 1;\n") + assert _get_extractor(perl) is None + + +def test_extract_extensionless_bash_cli_end_to_end(tmp_path): + """A shebang-only bash CLI must contribute nodes with the same ID scheme + as a .sh file (path stem + entity), so doc-created stub IDs merge.""" + cli = tmp_path / "devctl" + cli.write_text( + "#!/usr/bin/env bash\n" + "helper() { echo hi; }\n" + "main() { helper; }\n" + 'main "$@"\n' + ) + result = extract([cli], cache_root=tmp_path) + ids = {n["id"] for n in result["nodes"]} + assert "devctl_helper" in ids + assert "devctl_main" in ids + + def test_extract_bash_node_metadata_is_sanitized(): """Bash extractor must route node metadata through sanitize_metadata so HTML-sensitive characters cannot reach downstream graph viewers raw.""" diff --git a/tests/test_java_type_resolution.py b/tests/test_java_type_resolution.py index ccd97d9bb..35229776e 100644 --- a/tests/test_java_type_resolution.py +++ b/tests/test_java_type_resolution.py @@ -150,6 +150,57 @@ def test_java_type_parameters_do_not_resolve_to_real_class(tmp_path: Path): assert ("Generic", "references", "T") not in references +def test_java_builtin_library_types_not_emitted_as_references(tmp_path: Path): + # Built-in / standard-library types (java.lang, java.util, …) used as field, + # parameter, or return types carry no useful graph meaning: they never resolve + # to a project node, so emitting `references` edges to them is pure noise. + svc = _write( + tmp_path / "Svc.java", + "package com.app;\n" + "import java.util.List;\n" + "import java.util.Map;\n" + "public class Svc {\n" + " private String name;\n" + " private List ids;\n" + " public Map lookup(Long id) { return null; }\n" + " public java.util.Optional flag() { return null; }\n" + "}\n", + ) + result = extract([svc], cache_root=tmp_path) + + ref_targets = { + by_label + for (src, rel, by_label) in _label_edges(result, {"references"}) + } + for builtin in ( + "String", "Integer", "Map", "Object", "Long", + "List", "Optional", "Boolean", + ): + assert builtin not in ref_targets, ( + f"builtin/library type {builtin!r} should not be a references target" + ) + + +def test_java_user_types_still_emit_references(tmp_path: Path): + # Guard against over-skipping: a user-defined type sharing the field/return + # shape must still resolve to a real `references` edge. + dto = _write(tmp_path / "OrderDto.java", + "package com.app;\npublic class OrderDto {}\n") + svc = _write( + tmp_path / "OrderSvc.java", + "package com.app;\n" + "public class OrderSvc {\n" + " private java.util.List orders;\n" + " public OrderDto first() { return null; }\n" + "}\n", + ) + result = extract([dto, svc], cache_root=tmp_path) + ref_targets = { + by_label for (_, _, by_label) in _label_edges(result, {"references"}) + } + assert "OrderDto" in ref_targets, "user type OrderDto must still emit references" + + def test_java_cross_file_constructor_call_resolves(tmp_path: Path): # #1373: `new Foo(...)` in a method body must produce a cross-file edge to the # Foo definition. Foo is NOT used as a return type here, so the edge can only diff --git a/tests/test_languages.py b/tests/test_languages.py index 38eb7e169..9aa64b526 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -472,7 +472,9 @@ def test_java_record_component_type_references(tmp_path): result = extract_java(source) assert ("Order", "Payload") in _edge_labels(result, "references", "field") - assert ("Order", "List") in _edge_labels(result, "references", "field") + # `List` is a java.util library type: skipped as noise, so only its user-type + # generic argument (`Item`) survives, not the container itself. + assert ("Order", "List") not in _edge_labels(result, "references") assert ("Order", "Item") in _edge_labels(result, "references", "generic_arg") assert ("Order", "Attachment") in _edge_labels(result, "references", "field") diff --git a/tests/test_rationale.py b/tests/test_rationale.py index b52aa3909..4c915664d 100644 --- a/tests/test_rationale.py +++ b/tests/test_rationale.py @@ -261,3 +261,72 @@ def normal(self) -> int: f"rationale node {r_id} for ``.{decorated_name}()`` is orphaned " f"(degree 0) after build_from_json" ) + + +# ── JS/TS rationale + doc-reference extraction ──────────────────────────────── + + +def _write_ts(tmp_path: Path, code: str) -> Path: + p = tmp_path / "sample.ts" + p.write_text(textwrap.dedent(code)) + return p + + +def test_js_rationale_comment_extracted(tmp_path): + from graphify.extract import extract_js + path = _write_ts(tmp_path, ''' + // NOTE: must run before compile() or the linker will fail + export function build(): void {} + ''') + result = extract_js(path) + rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"] + assert any("NOTE" in n["label"] for n in rationale) + + +def test_js_block_comment_rationale_extracted(tmp_path): + from graphify.extract import extract_js + path = _write_ts(tmp_path, ''' + /** + * WHY: retries are capped because the upstream rate-limits at 10 rps. + */ + export function fetchData(): void {} + ''') + result = extract_js(path) + rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"] + assert any("rate-limits" in n["label"] for n in rationale) + + +def test_js_adr_reference_extracted(tmp_path): + from graphify.extract import extract_js + path = _write_ts(tmp_path, ''' + // Gateway pattern per ADR-0002; provider selection per ADR-0015. + export function route(): void {} + ''') + result = extract_js(path) + refs = [n for n in result["nodes"] if n.get("file_type") == "doc_ref"] + labels = {n["label"] for n in refs} + assert "ADR-0002" in labels and "ADR-0015" in labels + cites = [e for e in result["edges"] if e.get("relation") == "cites"] + assert len(cites) == 2 + + +def test_js_adr_reference_normalized_and_deduped(tmp_path): + from graphify.extract import extract_js + path = _write_ts(tmp_path, ''' + // See ADR-11 for the trust boundary. + // ADR 0011 also governs the injection containment below. + export function guard(): void {} + ''') + result = extract_js(path) + refs = [n for n in result["nodes"] if n.get("file_type") == "doc_ref"] + assert [n["label"] for n in refs] == ["ADR-0011"] + + +def test_js_adr_in_string_literal_not_extracted(tmp_path): + from graphify.extract import extract_js + path = _write_ts(tmp_path, ''' + export const banner = "compliant with ADR-0099"; + ''') + result = extract_js(path) + refs = [n for n in result["nodes"] if n.get("file_type") == "doc_ref"] + assert refs == [] diff --git a/uv.lock b/uv.lock index fcd87008b..088ebbbdc 100644 --- a/uv.lock +++ b/uv.lock @@ -1090,7 +1090,7 @@ wheels = [ [[package]] name = "graphifyy" -version = "0.9.3" +version = "0.9.6" 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-pascal" }, { name = "tree-sitter-sql" }, { name = "watchdog" }, { name = "yt-dlp" }, @@ -1198,6 +1199,9 @@ openai = [ { name = "openai" }, { name = "tiktoken" }, ] +pascal = [ + { name = "tree-sitter-pascal" }, +] pdf = [ { name = "markdownify" }, { name = "pypdf" }, @@ -1308,6 +1312,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-pascal", marker = "extra == 'all'" }, + { name = "tree-sitter-pascal", marker = "extra == 'pascal'" }, { name = "tree-sitter-php", specifier = ">=0.23,<0.25" }, { name = "tree-sitter-powershell", specifier = ">=0.26,<0.28" }, { name = "tree-sitter-python", specifier = ">=0.23,<0.26" }, @@ -1325,7 +1331,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", "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", "all"] [package.metadata.requires-dev] dev = [ @@ -4728,6 +4734,26 @@ 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-pascal" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/89/cb9ce7f7e9a119a3d518020c387fb994f8f6cac07a3f796fbb3409b506fb/tree_sitter_pascal-0.11.0.tar.gz", hash = "sha256:7d4da59b8c0b2ff9a32181562eed25c8ba13a6c8bbca2cb895e10f5344708dba", size = 293593, upload-time = "2026-07-01T03:53:16.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/74/621a4d1690e224de4ae00fb90304469bd96c1ae25bd7a8eabb95f9809b90/tree_sitter_pascal-0.11.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:7a8c2bb6ab958cea7390e18ed28f4535ca24ba110eae0449d5a77c294844d45a", size = 238019, upload-time = "2026-07-01T03:53:01.704Z" }, + { url = "https://files.pythonhosted.org/packages/1e/36/8660f400b1c42714f345fc0afae295be2fafec51531d199b7c6e2387881e/tree_sitter_pascal-0.11.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bccb2d757c17820cdcfaa760bac1df4caa6278e1e2334d6956e0f9c31c6e8f4a", size = 125475, upload-time = "2026-07-01T03:53:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1e/06f87ce971119b6a6c1682a0647cad9774463ae9c68faa8c016d8c10d30f/tree_sitter_pascal-0.11.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ed50bb16a8469a54ae365f51dac046a3247e2efbab203269a8d737fa816b11a6", size = 132345, upload-time = "2026-07-01T03:53:04.643Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/e37df67cc704f9c19531c858835396366d39790e71114fa181929b0dba00/tree_sitter_pascal-0.11.0-cp38-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ec2ca83d1e01a1eaf7711d6392947058c64302562f987be5c0bcf7e185d0dbd", size = 157887, upload-time = "2026-07-01T03:53:05.849Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/9e21b0199e86078ca4763ab4bc68fa6e1a3e625e168dee9fdcf27455d09f/tree_sitter_pascal-0.11.0-cp38-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ab7557a05400f2841b18d000997453dadff09c2830bd48313cf4264bab018bbe", size = 153629, upload-time = "2026-07-01T03:53:06.99Z" }, + { url = "https://files.pythonhosted.org/packages/20/34/947ae7c72eac8363e3de08f2a1e2f992beeb6afb27daca939443438e4614/tree_sitter_pascal-0.11.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:543717086ae596aa1895c6151230e45f78ed20a69b9c1ab83a5d5950db068381", size = 153629, upload-time = "2026-07-01T03:53:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/14/05/6b103890fdb63c57fb89650ece51333852e0ee8a4a3ef0966168acebfad4/tree_sitter_pascal-0.11.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e317158e32c6e19ab75fe8664b48c48d71e757f6e2f8de0996eba63583d4ea55", size = 152709, upload-time = "2026-07-01T03:53:09.386Z" }, + { url = "https://files.pythonhosted.org/packages/46/6e/ef2c893744b4d53f53113f7ca5bf3016afed6ced3db82ae7d3a42a2faf72/tree_sitter_pascal-0.11.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:ab2e43d9a4d30ee482322a41f7f12ca2f40b4f0ee1771646329fe57a6a6a29ba", size = 157179, upload-time = "2026-07-01T03:53:10.501Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bf/5b64986483183667c086c6612185a03f3de2408f81b6bac49b60fa45c187/tree_sitter_pascal-0.11.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b545e3b7a20fc7f7ce346e0fc598120a90b980adf050357946b7b00d2a81f875", size = 152117, upload-time = "2026-07-01T03:53:11.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/3553a1c89441a8f78daedd6182580db4df234b6b4ddd0e9d18ac9a2f7385/tree_sitter_pascal-0.11.0-cp38-abi3-win32.whl", hash = "sha256:c898d8c2771c5da47dbab20aea1d99e3aca8878a90e24db0a5558619ac8ad5c5", size = 124205, upload-time = "2026-07-01T03:53:12.969Z" }, + { url = "https://files.pythonhosted.org/packages/19/96/695db7513510ea4fedb5e4e9b47011bf309253715c5eb9892c71486312e6/tree_sitter_pascal-0.11.0-cp38-abi3-win_amd64.whl", hash = "sha256:99346d54c928d42c820c0aa262f6acbf4fb2856d69016f1df11f834f681b45e0", size = 124349, upload-time = "2026-07-01T03:53:14.082Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/e5381365ed5b247cad2e047682b081ccbb79f84bb5c1b48642ef3385f1d3/tree_sitter_pascal-0.11.0-cp38-abi3-win_arm64.whl", hash = "sha256:9d22b974a47765f1c98629338d82d1df89c35c270a73f08dda8fbd49538e32d4", size = 124914, upload-time = "2026-07-01T03:53:15.191Z" }, +] + [[package]] name = "tree-sitter-php" version = "0.24.1"