Skip to content

Commit d0472ca

Browse files
authored
- examples/multi-repo.yaml: stop pinning five extractors. The file promised (#172)
in its own header that it "enables all extractors so it works regardless of which languages each repository uses", then named five of twelve — so a cluster containing PHP, Rust, Python, Java or C++ indexed those repos as though the language were unsupported. A cluster config is the one place you cannot know which languages will turn up. The per-language examples keep their single extractor: pin a plugin list only when the list is the reason the file exists. - CONTRIBUTING: enable the pre-push hook, and run the agent-hook end-to-end test if you touch the installer. CI cannot run it — runners do not run agent sessions — so a developer's machine is the only place it executes. - docs/BENCHMARKS.md: 38 repositories, 4,211,113 facts, 239,349 files, zero parse errors, 38/38 byte-identical across 114 runs. Ratchet widened to 12 repositories and 8 languages against 1,192 pre-existing findings. Swift is excluded from that section only, with the reason stated.
1 parent f70ad64 commit d0472ca

16 files changed

Lines changed: 2113 additions & 7 deletions

File tree

CONTRIBUTING.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ Thank you for your interest in contributing to enola. Every contribution — cod
1919
git checkout -b my-feature
2020
```
2121

22+
5. **Enable the pre-push hook** — once per clone:
23+
24+
```bash
25+
git config core.hooksPath .githooks
26+
```
27+
28+
It runs the guards that are cheap locally and awkward in CI: the `cacheVersion`
29+
coverage check, and the golden + determinism suite. Skip in an emergency with
30+
`git push --no-verify`; CI enforces both anyway.
31+
32+
It also runs one check **CI cannot run at all**. See below.
33+
2234
## What to work on
2335

2436
- **Bug reports and fixes** — if something doesn't work, open an issue or submit a fix.
@@ -41,6 +53,30 @@ If you're considering a larger change, please open an issue first so we can disc
4153

4254
5. Open a pull request against `main`. Describe what the change does and why.
4355

56+
### If you touch the agent hooks
57+
58+
`pkg/install/`, `cmd/enola/hook.go`, `cmd/enola/doctor.go` and `internal/hookstate/`
59+
are covered by one test CI will never run for you:
60+
61+
```bash
62+
ENOLA_E2E=1 go test -run TestStopHook_FiresInARealSession ./pkg/install/
63+
```
64+
65+
It installs the hooks the way a user does, ends a **real agent session** with a known
66+
regression present, and asserts the verdict came out. CI runners do not run agent
67+
sessions by design, so this only ever executes on a developer's machine — the pre-push
68+
hook runs it automatically when your push touches one of those paths, and skips with a
69+
loud message if `claude` is not on your `PATH`.
70+
71+
Please do not treat it as optional. The failure mode it guards against is a hook
72+
configuration that parses, reports success, and does nothing: every cheaper check
73+
passed while the `Stop` hook was silently never firing, because a unit test can only
74+
compare the output against the same belief that produced it. The full account is in
75+
[`DEFECTS_FOUND.md`](DEFECTS_FOUND.md).
76+
77+
If you cannot run it, say so in the PR so a reviewer can. `enola doctor` is the
78+
same question asked after the fact, on a real repository.
79+
4480
## Code style
4581

4682
- Follow standard Go conventions (`gofmt`, `go vet`).

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ claude mcp add enola enola
7676

7777
`enola install` writes a short instruction into the files your agents already read — Claude Code, Cursor, Copilot, Codex, Pi — and `--hooks` adds the two hooks that run the loop for you. It previews every change and asks before writing, never creates files you didn't have, and `enola uninstall` puts everything back byte-for-byte.
7878

79+
After your next session, `enola doctor` reports whether those hooks actually fired. Worth running once: a hook configuration is a contract with your agent, and one it quietly ignores looks exactly like one it honours.
80+
7981
Prefer to drive it yourself? Skip step 2 and run the loop by hand:
8082

8183
```bash

docs/BENCHMARKS.md

Lines changed: 404 additions & 0 deletions
Large diffs are not rendered by default.

docs/extraction/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# What enola extracts, per language
2+
3+
These pages answer one question: **given this code, what ends up in the graph?**
4+
5+
Every example is a file that ships in this repository, and every fact shown is copied
6+
from the golden file the test suite asserts against. So none of it is a description of
7+
intended behaviour — it is the behaviour, and if an extractor changes without these
8+
pages changing, the golden tests fail first.
9+
10+
| | |
11+
|---|---|
12+
| Fixture sources | [`internal/engine/testdata/repos/`](../../internal/engine/testdata/repos/) |
13+
| Expected facts | [`internal/engine/testdata/golden/`](../../internal/engine/testdata/golden/) |
14+
| Measured on real repositories | [BENCHMARKS.md](../BENCHMARKS.md) |
15+
16+
## The pages
17+
18+
| Language | Routes and clients it understands | |
19+
|---|---|---|
20+
| [Go](go.md) | gorilla/mux, chi, Gin, Echo, `net/http` clients, gRPC, Kafka | prefix composition across function boundaries |
21+
| [TypeScript / JavaScript](typescript.md) | Express, NestJS, Next.js, `fetch`, axios, Prisma, TypeORM, Drizzle | Vue, Svelte, and file-based routing |
22+
| [Python](python.md) | FastAPI, Flask, Django, SQLAlchemy, gRPC | `include_router` prefixes folded repo-wide |
23+
| [Ruby](ruby.md) | Rails `routes.rb`, ActiveRecord, Packwerk | nested `resource`/`resources` path shapes |
24+
| [Java](java.md) | Spring MVC, RestTemplate, Feign, JPA, Dubbo SPI | |
25+
| [Kotlin](kotlin.md) | Retrofit, Room, Compose, Hilt | |
26+
| [Swift](swift.md) | URLSession, SwiftUI, UIKit | endpoint enums, protocol-extension prefixes |
27+
| [PHP](php.md) | Laravel, Symfony, WordPress, Guzzle | `apiResource` expansion, YAML route config |
28+
| [Rust](rust.md) | Axum route DSL | `.nest()` mounts composed crate-wide |
29+
| [C / C++](cpp.md) || header/source method merging, namespaces, templates |
30+
| [gRPC and OpenAPI](grpc-openapi.md) | `.proto` services, OpenAPI specs | the contract as the server side of an edge |
31+
32+
## How to read a page
33+
34+
Each one is organized the same way:
35+
36+
1. **At a glance** — a table from source construct to fact kind.
37+
2. **What each construct produces** — the code, then the facts, then the query it unlocks.
38+
3. **What is deliberately not extracted** — the limits, stated next to the capability.
39+
40+
That last section is not an apology. A missing edge shows up in `enola coverage` as an
41+
unresolved count you can go and look at; a *wrong* edge is invisible and gets acted on.
42+
Every extractor here reports the gap rather than inventing the edge, and the section
43+
says where those gaps are.
44+
45+
## The fact model in one paragraph
46+
47+
Everything below is one of six kinds — `module`, `symbol`, `route`, `storage`,
48+
`dependency`, `service` — plus two reference-only kinds (`file_ref`, `test_ref`) that
49+
carry edges without being architecture themselves. Facts are name-keyed, carry a
50+
`file:line`, and hold typed relations (`imports`, `calls`, `declares`, `handled_by`,
51+
`depends_on`, …). [ARCHITECTURE.md](../../ARCHITECTURE.md#the-fact-model) has the full
52+
model; these pages assume it only loosely.

docs/extraction/cpp.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# C / C++ — what enola extracts
2+
3+
Parsed with tree-sitter-c and tree-sitter-cpp. Detected by a C source (`.c`), a C++
4+
source (`.cpp`/`.cc`/`.cxx`/`.hpp`/…), or a build file (`CMakeLists.txt`, `Makefile`,
5+
`meson.build`, `*.vcxproj`) plus any header. Language is recorded **per fact**, so a mixed
6+
tree reports `c` and `cpp` separately rather than collapsing to one.
7+
8+
Fixture: [`cpp_sample`](../../internal/engine/testdata/repos/cpp_sample/)
9+
10+
## At a glance
11+
12+
| You write | enola stores | Kind |
13+
|---|---|---|
14+
| a source directory | one module per directory | `module` |
15+
| `int foo(void) { … }` | a symbol with `has_body=true` | `symbol` |
16+
| `static int foo(void)` | the same, with `static=true`, `exported=false` | `symbol` |
17+
| a declaration in a header + definition in a `.c` | **one** merged symbol, not two | `symbol` |
18+
| `#define X`, `const int` | a `constant` symbol | `symbol` |
19+
| `gc->set = xlp_gpio_set;` | a call edge to the assigned function | relation |
20+
| `.lock = fn` in a compound literal | a call edge to `fn` | relation |
21+
| a function name inside a `#define` body | a call edge recovered by a macro pre-pass | relation |
22+
| a token-pasted callback (`_pfx##_name##_show`) | a call edge to the pasted name | relation |
23+
| a call inside a `for`/`while` | `calls_in_loop` / `calls_in_scaling_loop` | props |
24+
25+
## Symbols, and why `has_body` matters
26+
27+
```c
28+
static int omap_reserve(void) { … }
29+
```
30+
31+
```
32+
symbol drivers.omap_reserve drivers/board.c:12
33+
props: symbol_kind=function, language=c, static=true, exported=false,
34+
has_body=true, cyclomatic=1
35+
```
36+
37+
A header declaration and its definition are the same entity. Merging them means the
38+
symbol has one location — the definition — and one set of callers, instead of a phantom
39+
zero-caller declaration sitting next to the real thing.
40+
41+
## The three ways a C callback gets its only caller
42+
43+
C code wires behaviour through function pointers, and none of it looks like a call. All
44+
three forms are recovered, because otherwise most of a driver reads as dead code.
45+
46+
**1. Function-pointer field assignment**
47+
48+
```c
49+
static int probe(struct gpio_chip *gc)
50+
{
51+
gc->set = xlp_gpio_set; /* plain */
52+
gc->get = &xlp_gpio_get; /* address-of */
53+
ct->chip.irq_mask = mvebu_mask; /* nested field */
54+
gc->ngpio = 32; /* plain data — must NOT create an edge */
55+
}
56+
```
57+
58+
The first three create a `calls` edge from `probe` to the assigned function. The fourth
59+
assigns an integer and creates nothing — the discriminator is whether the right-hand side
60+
names a known function, not whether the statement is an assignment.
61+
62+
**2. Compound-literal designated initializers**
63+
64+
```c
65+
cfg = (struct regmap_config) {
66+
.reg_bits = 8,
67+
.lock = dio48e_regmap_lock,
68+
.unlock = dio48e_regmap_unlock,
69+
};
70+
```
71+
72+
`dio48e_regmap_lock` and `dio48e_regmap_unlock` get inbound edges from the enclosing
73+
function. `.reg_bits = 8` does not.
74+
75+
**3. References that exist only inside macro bodies**
76+
77+
A function named only in a `#define` replacement list is invisible to the AST — the
78+
preprocessor would have to run first. A project-wide macro pre-pass recovers both the call
79+
position and the value position:
80+
81+
```c
82+
#define ATTR_PERM(_pfx, _name, _perm) \
83+
static struct configfs_attribute _pfx##attr_##_name = { \
84+
.show = _pfx##_name##_show, \
85+
.store = _pfx##_name##_store, \
86+
}
87+
```
88+
89+
The pre-pass is **project-wide, not include-scoped**, so the pasted `cfg_label_show` /
90+
`cfg_label_store` callbacks are recovered even though the invoking file does not literally
91+
`#include` the header that defines the macro. Following `#include` graphs exactly would
92+
lose these, and the kernel's sysfs and configfs attribute surfaces are built almost
93+
entirely this way.
94+
95+
## Loops
96+
97+
```
98+
symbol drivers.constant_loop drivers/loops.cpp:12
99+
props: loop_count=1, loop_depth=1, scaling_loop_depth=0,
100+
calls_in_loop=[drivers.step], calls_in_scaling_loop=[]
101+
```
102+
103+
Same model as [Go](go.md#loops-for-n1-hunting) and [Ruby](ruby.md): a constant-bounded
104+
loop records the call but keeps the scaling set empty rather than absent.
105+
106+
## C++ specifics
107+
108+
Namespaces, templates and class methods are extracted, with header and source methods
109+
merged into one symbol as above.
110+
111+
> **A note on `override`.** The C++ corpus used to validate this extractor
112+
> (getdp, gmsh, the Linux kernel) is pre-C++11 and never uses the `override`
113+
> specifier, so the code path that consumes it has zero exercise on real code.
114+
> `virtual` is the specifier that is actually in play there.
115+
116+
## What is deliberately not extracted
117+
118+
- **Preprocessor evaluation.** `#if`/`#ifdef` branches are all parsed; enola does not pick
119+
a configuration, so facts from mutually exclusive branches can coexist.
120+
- **Template instantiation.** A template is one symbol, not one per instantiation.
121+
- **Virtual dispatch.** A call through a base-class pointer resolves to the declared
122+
method, not to every override.
123+
- **Linker-level symbol resolution.** Two static functions with the same name in different
124+
translation units are distinguished by module, not by object file.
125+
126+
---
127+
128+
Measured on real C/C++ repositories — including the Linux kernel: [BENCHMARKS.md](../BENCHMARKS.md).

0 commit comments

Comments
 (0)