Skip to content
Merged
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
58 changes: 42 additions & 16 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,26 +107,49 @@ A snapshot is produced by a fixed, deterministic pipeline ([`internal/engine/eng
Repository
File Walker ──▶ Extractors ──▶ Fact Store ──▶ Cross-Repo Linker ──▶ Graph Index
(apply (Go, Java, (indexed by (only with 2+ (bidirectional)
ignore Kotlin, Python, kind / file / repos loaded) │
globs) TS, Swift, name / repo) ▼
Ruby, C++, OpenAPI) Explainers
(cycles, layers,
crossrepo)
Renderer
(llm_context.md)
Artifacts
(.enola/)
┌──────────────────┐ apply ignore globs (a few extractors also scan
│ File Walker │ config-format files directly — see Supported languages)
└──────────────────┘
┌──────────────────┐ Go · Java · Kotlin · JS/TS · Vue · Svelte · Python ·
│ Extractors │ Swift · Ruby · C++ · PHP · OpenAPI (source → facts)
└──────────────────┘
┌──────────────────┐
│ Fact Store │ indexed by kind / file / name / repo
└──────────────────┘
┌──────────────────┐
│ Cross-Repo Linker│ only with 2+ repos loaded
└──────────────────┘
┌──────────────────┐
│ Graph Index │ bidirectional (+ synthetic edges)
└──────────────────┘
┌──────────────────┐ cycles · layers · crossrepo · coverage · unused-routes ·
│ Explainers │ god-class · hotspots · dependency-depth ·
└──────────────────┘ exported-surface · complexity-outliers (facts → insights)
┌──────────────────┐
│ Renderer │ llm_context.md (snapshot → artifacts)
└──────────────────┘
┌──────────────────┐
│ Artifacts │ .enola/
└──────────────────┘
```

Stage by stage:

1. **File walker** — enumerates files under the repo, applying the `ignore` globs from config so build output, vendored code, tests, and generated files never reach the parsers.
1. **File walker** — enumerates files under the repo, applying the `ignore` globs from config so build output, vendored code, tests, and generated files never reach the parsers. A few extractors (OpenAPI, and PHP's Symfony route config) deliberately scan specific config-format files (YAML/JSON) directly from disk, bypassing these globs — because the globs exist to suppress config/data noise, not to hide those architecturally meaningful files (see [Supported languages](#supported-languages)).
2. **Extractors** — each enabled language extractor first *detects* whether it applies (e.g. Go runs when there's a `go.mod`), then *parses* the matching files and emits facts. This is pure parsing; see [Supported languages](#supported-languages) for what each one understands.
3. **Fact store** — facts land in an in-memory store ([`internal/facts/store.go`](internal/facts/store.go)) indexed by kind, file, name, and repo for fast queries. In append mode, facts are tagged with a repo label and file paths are repo-prefixed.
4. **Cross-repo linker** — only when two or more repos are loaded. It connects the per-repo graphs by matching HTTP client/server routes and shared-library imports, emitting `service` nodes and cross-repo dependency edges. The link set is recomputed from scratch on every append, so it always reflects exactly the repos currently loaded.
Expand Down Expand Up @@ -448,6 +471,7 @@ Each extractor is detected by characteristic project files and then parses what
| Swift | tree-sitter | `Package.swift`, `.xcodeproj`, or `.xcworkspace` present |
| Ruby | tree-sitter | `Gemfile` present |
| C++ | tree-sitter | a C++ source (`.cpp`/`.cc`/`.cxx`/`.hpp`/...) present, or a build file (`CMakeLists.txt`/`Makefile`/`meson.build`/`*.vcxproj`) plus any header |
| PHP | tree-sitter | `composer.json`, a WordPress bootstrap file (`wp-load.php`/`wp-settings.php`/`wp-config.php`), or any `.php` source within 3 directory levels |
| OpenAPI | YAML/JSON scanner| any file containing `openapi:` or `swagger:` |

**Go** uses the standard-library parser directly, so symbols, methods, interfaces, imports, and call edges are exact.
Expand All @@ -474,6 +498,8 @@ Each extractor is detected by characteristic project files and then parses what

**C++** is parsed with tree-sitter and handles the header/source split that defines the language. Classes, structs, unions, enums (incl. `enum class`), namespaces, free functions and methods, data members, and `typedef`/`using` aliases become symbol facts named `<dir>.<ns1::ns2::Class::member>` — enola's `<dir>.` module convention on the outside, native C++ `::` scope inside. Because an out-of-line definition `Class::method` (parsed from a `qualified_identifier`) yields the same canonical name as its in-class declaration, a dedup pass **merges a header's method prototype with its `.cpp` definition** into a single symbol (the definition wins for file/line and carries the call-graph edges). Base classes become `implements` edges; method bodies are walked for `calls`/`instantiates` edges; quoted `#include "x.h"` becomes a `dependency` resolved to the declaring module, while system `<...>` includes are skipped. Templates are unwrapped to their inner declaration and flagged `templated`, and the walker descends through `#if`/`#ifdef` preprocessor guards (so code wrapped in `#if defined(HAVE_*)` and headers behind include guards are still extracted). Like the other AST extractors, it walks each function/method body for complexity metrics — `cyclomatic`, `loop_depth`, `loop_count`, `calls_in_loop`, and `recursive_self` (counting `for`/`while`/`do-while`/range-`for` and STL-algorithm lambdas like `for_each`/`transform` as loops) — which the enterprise `analyze_performance` tool consumes. *Limitation:* header/source merging relies on the `.h` and `.cpp` living in the same directory (the common layout); split `include/` + `src/` trees are not merged.

**PHP** is parsed with tree-sitter (the `LanguagePHP` grammar, which tolerates the HTML/`<?php` interleaving of WordPress templates). Classes, interfaces, traits, enums (and their cases), top-level functions, methods, class constants, and properties become symbol facts. Type names are fully qualified with the file's `namespace` (`App\Models\Order`), members use PHP's native `Class::method` / `Class::$prop` notation, and global symbols keep their bare name (the common WordPress case). `extends`/`implements` and in-class `use SomeTrait;` become `implements` edges; method/function bodies are walked for `calls` (global-function, static `Class::method`, and bare instance-method targets), `instantiates` (`new X`), and the standard complexity metrics (`cyclomatic`, `loop_depth`, `loop_count`, `calls_in_loop`, `recursive_self`). `use Foo\Bar;` imports become `dependency` facts; a resolve pass builds a project-wide FQN→module index and turns namespaced references (inheritance, trait use, static calls, instantiations) and resolvable imports into internal module-coupling edges, classifying the rest as external — so PHP participates in `traverse`, `find_path`, and `impact_analysis`. **Outbound HTTP-client detection** emits a client-role `route` fact for each call made through Guzzle (`$client->get('/x')`, `$client->request('POST', '/x')`), the Laravel `Http` facade (`Http::get(...)`, including `Http::withToken(...)->get(...)` chains), Symfony's `HttpClient` (`$httpClient->request(...)`), raw cURL (`curl_setopt($ch, CURLOPT_URL, …)`, `curl_init`), and `file_get_contents` — relative paths are kept (with a `target_hint` inferred from any nearby base-URL env var), while absolute `http(s)://` URLs and interpolated/concatenated paths are skipped. **Framework route DSLs** add server-role `route` facts. **WordPress awareness** (enabled when a `wp-load.php`/`wp-settings.php`/`wp-config.php` marker is present) emits a `route` fact per hook call: `add_action`/`add_filter` registrations (carrying the `callback` target), the `do_action`/`apply_filters` hook points, and `register_rest_route` endpoints — dynamic (interpolated/variable) hook names are skipped. **Laravel** (detected via `laravel/framework` in `composer.json`, an `artisan` file, or a `routes/web.php`|`api.php`) parses the `Route::` DSL in `routes/*.php`: verb registrations (`Route::get/post/…`), `Route::match`/`any`, `Route::resource`/`apiResource` (expanded into their REST actions), nested group prefixes (both `Route::group(['prefix' => …], …)` and the fluent `Route::prefix('x')->…->group(…)`), and the `->name(…)` modifier — handlers are normalized to `Controller::method`. **Symfony** (detected via `symfony/framework-bundle`, or `bin/console` + `config/`) reads routes from PHP 8 `#[Route('/path', methods: ['GET'], name: '…')]` attributes (a class-level attribute prefixes its methods; `methods` accepts string literals or `Request::METHOD_*` constants) and legacy `@Route(…)` docblock annotations, plus YAML (`config/routes.yaml`, `config/routes/*.yaml`) and XML route configuration — these config files are scanned directly from disk, independently of the main walker (the same approach the OpenAPI extractor uses), so they are found even when `*.yaml` is globally ignored, as the bundled configs do. Once emitted, these client/server routes flow through the cross-repo linker and the `unused-routes` explainer like every other language. *Scope:* Symfony route imports/`resource` includes and bundle-local route configs (`**/Resources/config/routes.*`) are not expanded/discovered.

---

## Output artifacts
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Cursor (add to `mcp.json`):

Done. Your agent now has a precise structural map of your code. For configuration options, multi-repo setup, and what to ask next, see [Quick start](#quick-start) below.

**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · C++ · Vue · Svelte · OpenAPI — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, SwiftUI, Jetpack Compose, …)
**Supported languages:** Go · JavaScript · TypeScript · Python · Java · Kotlin · Swift · Ruby · C++ · PHP · Vue · Svelte · OpenAPI — with framework awareness (Next.js, Nuxt, SvelteKit, FastAPI, Django, Spring, Rails, Laravel, Symfony, SwiftUI, Jetpack Compose, WordPress, …)

---

Expand Down Expand Up @@ -222,11 +222,12 @@ Working across several repos? Generate the first, then add the rest with append
| Swift | `Package.swift`, `.xcodeproj`, `.xcworkspace` (SwiftUI / UIKit aware) |
| Ruby | `Gemfile` (Rails / ActiveRecord / Packwerk aware) |
| C++ | `.cpp`/`.hpp`/… source or `CMakeLists.txt`/`Makefile` + header (header/source method merging, namespaces, templates) |
| PHP | `composer.json`, WordPress markers, or any `.php` source (WordPress / Laravel / Symfony route + outbound HTTP-client aware) |
| OpenAPI | any spec with an `openapi:` / `swagger:` key |

Framework- and platform-specific detection for each language is described in **[ARCHITECTURE.md → Supported languages](ARCHITECTURE.md#supported-languages)**.

> Python and Ruby are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.
> Python, Ruby, and PHP are parsed with tree-sitter and contribute call and dependency edges to the graph, so `traverse`, `find_path`, and `impact_analysis` reach into them — not just modules and routes.

---

Expand Down
2 changes: 2 additions & 0 deletions examples/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# - swift (detection: Package.swift, .xcodeproj, or .xcworkspace)
# - ruby (detection: Gemfile)
# - cpp (detection: .cpp/.hpp/... or CMakeLists.txt/Makefile + header)
# - php (detection: composer.json, WordPress markers, or any .php source)

repo: "."
ignore:
Expand Down Expand Up @@ -92,6 +93,7 @@ extractors:
- typescript
- swift
- ruby
- php
explainers:
- cycles
- layers
Expand Down
58 changes: 58 additions & 0 deletions examples/php.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# enola configuration for a PHP project (plain PHP, Composer, or WordPress).
#
# Detection: The PHP extractor activates when a composer.json, a WordPress
# bootstrap file (wp-load.php / wp-settings.php / wp-config.php), or any
# .php source (within 3 directory levels) is present.
# Features: Classes, interfaces, traits, enums (and cases), functions, methods,
# class constants and properties (namespace-qualified "Ns\Class::member"
# naming; global symbols keep their bare name). Inheritance and trait-use
# (implements edges), a call graph (calls / instantiations), `use` import
# dependencies resolved into internal module-coupling edges, and per-
# function complexity.
# Outbound HTTP-client calls become client routes: Guzzle, the Laravel
# Http facade, Symfony HttpClient, cURL (CURLOPT_URL), and
# file_get_contents (relative paths only; absolute URLs skipped).
# Framework route DSLs become server routes:
# - WordPress: add_action / add_filter (with their callback),
# do_action / apply_filters, register_rest_route.
# - Laravel: Route::get/post/… , match / any, resource / apiResource
# (expanded), nested group prefixes, and ->name(...) — in routes/*.php.
# - Symfony: #[Route(...)] attributes / @Route annotations on
# controllers, plus YAML/XML config (config/routes.yaml, config/routes/).

repo: "."
ignore:
# VCS and tooling
- ".git/**"
- ".enola/**"
# Composer / package dependencies
- "vendor/**"
- "**/vendor/**"
- "node_modules/**"
- "**/node_modules/**"
# WordPress uploads and caches (data, not code)
- "wp-content/uploads/**"
- "wp-content/cache/**"
- "**/cache/**"
# Build / minified assets
- "**/*.min.js"
- "**/*.min.css"
- "build/**"
- "dist/**"
# Tests and fixtures (uncomment to exclude)
# - "tests/**"
# - "**/*Test.php"
# Documentation / config / data
- "**/*.md"
extractors:
- php
explainers:
- cycles
- layers
- crossrepo
- coverage
renderers:
- llm_context
output:
dir: ".enola"
max_context_tokens: 16000
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/tree-sitter/go-tree-sitter v0.24.0
github.com/tree-sitter/tree-sitter-cpp v0.22.4-0.20240818224355-b1a4e2b25148
github.com/tree-sitter/tree-sitter-java v0.21.1-0.20240824015150-576d8097e495
github.com/tree-sitter/tree-sitter-php v0.22.9-0.20240819002312-a552625b56c1
github.com/tree-sitter/tree-sitter-python v0.23.6
github.com/tree-sitter/tree-sitter-ruby v0.21.1-0.20240818211811-7dbc1e2d0e2d
github.com/tree-sitter/tree-sitter-typescript v0.23.2
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func Default() *Config {
"**/Pods/**",
"**/.gradle/**",
},
Extractors: []string{"cpp", "go", "java", "kotlin", "openapi", "python", "typescript", "swift", "ruby"},
Extractors: []string{"cpp", "go", "java", "kotlin", "openapi", "php", "python", "typescript", "swift", "ruby"},
Explainers: []string{"cycles", "layers", "crossrepo", "coverage", "unused-routes", "god-class", "hotspots", "dependency-depth", "exported-surface", "complexity-outliers"},
Renderers: []string{"llm_context"},
Output: OutputConfig{
Expand Down
3 changes: 2 additions & 1 deletion internal/engine/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
// v2: Swift URLSession extractor precision (file-URL exclusion, interpolation fix).
// v3: Python route facts use method/role/bare-path Name (was http_method, verb-in-name).
// v4: Java HTTP client detection (RestTemplate call sites + @FeignClient interfaces).
const cacheVersion = "v4"
// v5: PHP HTTP client detection + Laravel/Symfony route DSLs (attributes, YAML/XML config).
const cacheVersion = "v5"

// extractorCache holds per-extractor facts keyed by a content hash of the files
// the extractor depends on. It is loaded from disk at the start of a snapshot and
Expand Down
Loading
Loading