Skip to content

Commit d15da2a

Browse files
authored
feat(extract): call-graph edge extractor (caller->callee) + Tier-1 calls gate (#26)
Add the sixth deterministic extractor — the deterministic foundation under the future business-process extractor (DESIGN §14 item 2). One `call_edge` artifact per RESOLVED caller->callee pair (call:{caller_fq}->{callee_fq}; call-site lines aggregated in the payload, the import-edge precedent). Three deterministic resolution tiers, precision-first (only first-party-resolved edges are emitted, never a wrong guess): same-module calls; imported-name calls — CROSS-FILE, `from x import f [as g]` and `import x[.y] [as z]; x.y.f()` module-attribute forms via per-module import tables (incl. relative imports, with the package-vs-module base handled for non-__init__ modules); and `self.method()` to a method of the same class. Documented gaps, surfaced by the gate: obj.method(...), getattr/ dynamic, super(), inherited self-calls, star imports, re-export indirection, decorator/default-arg expressions, class-body calls. Grounding: the caller def span (role `caller`; the module span for module-level calls) + the callee def span (role `callee`, cross-file). Direct recursion is a single-span artifact (one edge row — the (artifact_id, span_id) PK); mutual recursion yields two distinct artifacts (identity rule v2 — the collision class fixed in #24, now regression-tested at the extractor level). Body-only scanning excludes a def's own decorators/parameter defaults by construction; nested defs attribute calls to the innermost function (no-descend traversal). framework_versions deliberately empty (the import-graph rationale). New Tier-1 HARD gate `tier1_calls_test` (hand-labeled oracle, 11 edges, 10 tests): set equality; mutual-recursion identity regression; cross-file provenance; module-caller + merged lines; instantiation edges (callee_kind=class); six known gaps asserted. Headline HARD gates: twelve -> thirteen. Registered in default_extractors(); summarize/embed_text gained call_edge branches. 94 eval tests pass.
1 parent aa17843 commit d15da2a

8 files changed

Lines changed: 666 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Call-graph edge extractor** (`kb.extract.deterministic.calls`): the sixth deterministic
13+
extractor — one `call_edge` artifact per RESOLVED caller→callee pair (`call:{caller}->{callee}`,
14+
call-site lines aggregated in the payload, the import-edge precedent). Three deterministic
15+
resolution tiers: same-module calls, imported-name calls (**cross-file** — `from x import f
16+
[as g]` and `import x[.y] [as z]; x.y.f()` module-attribute forms, resolved via per-module import
17+
tables incl. relative imports), and `self.method()` to a method of the same class. Precision-first:
18+
only first-party-resolved edges are emitted; `obj.method(...)`, `getattr`/dynamic, `super()`,
19+
inherited self-calls, star imports, decorator/default-arg expressions and class-body calls are
20+
documented gaps. Grounded on the caller def span (role `caller`; module span for module-level
21+
calls) + the callee def span (role `callee`); direct recursion is a single-span artifact; mutual
22+
recursion yields two distinct artifacts (identity rule v2). `framework_versions` empty. Registered
23+
in `default_extractors()`; `summarize` / `embed_text` gained a `call_edge` branch. The
24+
deterministic foundation under the future business-process extractor (DESIGN §14 item 2).
25+
- **Tier-1 call-graph HARD gate** (`kb.eval.tier1_calls_test`): extracted edges match a hand-labeled
26+
oracle (11 edges across 5 modules); a mutually recursive pair stays two distinct artifacts over an
27+
identical evidence span set (extractor-level identity-v2 regression); cross-file provenance (one
28+
artifact spans the caller's and callee's files); six blind spots asserted as *known* gaps; nested
29+
defs attribute to the innermost function. Headline HARD gates: twelve → **thirteen**.
30+
1031
## [0.5.0] - 2026-07-07
1132

1233
### Fixed

DESIGN.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ test.)
254254
artifact per handler because `artifact_id` is content-addressed over spans + extractor, §6).
255255
Grounded on the handler span + cross-file on the listened-to class; hand-labeled Tier-1 gate.
256256
Known gaps (asserted): call-form `event.listen(...)`, lifespan, pydantic-v1, dynamic names.
257+
- **(e)** Call-graph edge extractor *(shipped)*: one `call_edge` per RESOLVED caller→callee pair,
258+
three deterministic tiers (same-module; imported — **cross-file**; `self.` method of the same
259+
class); precision-first — only first-party-resolved edges are emitted, recall bounded by
260+
documented gaps (dynamic/attribute calls, inheritance); grounded on caller + callee def spans;
261+
hand-labeled Tier-1 gate incl. a mutual-recursion identity-v2 regression. The deterministic
262+
foundation for §14 item 2.
257263
- Single Postgres with the `≥1 derived_from` invariant enforced at write.
258264
- **Adversarial fixture:** an ungrounded artifact that the write-time check **must reject**
259265
so the anti-hallucination discipline is tested in the MVP, not deferred with the LLM.
@@ -357,7 +363,7 @@ freshness(current|stale@sha)`, with a deterministic tie-break for reproducible e
357363
| Module | Responsibility | Key tech |
358364
|--------|----------------|----------|
359365
| `kb.structural` | Parse Python without executing it; enumerate symbols/imports/call-sites with per-SHA byte/line ranges; compute content-addressed span identity; incremental reparse. Hidden behind a `StructuralIndex`/`PathEngine` interface so a SCIP backend can replace tree-sitter later. | tree-sitter + tree-sitter-python (canonical bindings) |
360-
| `kb.extract.deterministic` | No-LLM extractors → exact artifacts (confidence=1.0): import graph; FastAPI API contract (static, cross-file grounded); domain entities (pydantic/dataclass/SQLAlchemy, static, cross-file links to referenced entities, hand-labeled gate); library public-API surface (static tree-sitter, cross-file `__init__` re-export resolution, independent griffe-oracle gate); event handlers (pydantic validators / FastAPI `on_event` / SQLAlchemy `listens_for`, static, cross-file target grounding, hand-labeled gate). | grimp, tree-sitter queries; griffe (dev-only oracle) |
366+
| `kb.extract.deterministic` | No-LLM extractors → exact artifacts (confidence=1.0): import graph; FastAPI API contract (static, cross-file grounded); domain entities (pydantic/dataclass/SQLAlchemy, static, cross-file links to referenced entities, hand-labeled gate); library public-API surface (static tree-sitter, cross-file `__init__` re-export resolution, independent griffe-oracle gate); event handlers (pydantic validators / FastAPI `on_event` / SQLAlchemy `listens_for`, static, cross-file target grounding, hand-labeled gate); call-graph edges (per-edge artifacts, three resolution tiers, caller+callee span grounding, hand-labeled gate). | grimp, tree-sitter queries; griffe (dev-only oracle) |
361367
| `kb.introspect` | Eval-only runtime oracle: runs a FastAPI app in a network-blocked sandbox and emits `app.openapi()` for the Tier-1 API gate. Never on the index path. | subprocess sandbox, fastapi |
362368
| `kb.embed` | Replaceable embedding adapters + snapshot population for `search_knowledge`. Torch isolated behind the `embed` extra and a lazy import. | sentence-transformers (default), OpenAI (optional), pgvector |
363369
| `kb.rag` | Frozen pgvector RAG-over-source baseline — the "other arm" of the knowledge-vs-RAG A/B (no provenance/grounding). | deterministic line-window chunker, pgvector |
@@ -404,6 +410,10 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas
404410
velocity, small team, no tagged GitHub releases (v0.6.6 is the npm version), needs Node + an
405411
activated venv, and the Python consumer will likely compile the `.proto` itself. This
406412
*strengthens* keeping it deferred behind an interface.
413+
- The shipped deterministic `call_edge` extractor takes the tree-sitter-call-sites road: it emits
414+
**only resolved** edges (precision-first) with recall bounded by documented gaps (dynamic
415+
dispatch, attribute calls, inheritance) — the "always quote recall, not just precision" rule;
416+
PyCG-class resolvers stay deferred behind `PathEngine`.
407417
- **PyCG (call graph, deferred semantic layer)****archived/unmaintained**; ~99% precision is
408418
paired with only **~70% recall** (≈30% of real edges missed → "incomplete path" is the common
409419
case). Treat as a known-temporary default strictly behind `PathEngine`; evaluate maintained
@@ -442,7 +452,9 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas
442452
pydantic/FastAPI/SQLAlchemy, static tree-sitter, hand-labeled Tier-1 gate; call-form
443453
`event.listen` deferred).
444454
2. The **one** grounded business-process extractor (named real path + labeler + validator +
445-
deterministic sub-property gate).
455+
deterministic sub-property gate). *Deterministic foundation shipped:* the `call_edge` extractor
456+
+ Tier-1 calls gate; `PathEngine` slicing, the sink registry, the labeler and the span-binding
457+
validator are next.
446458
3. Recursive invalidation (`artifact_depends_on`), multi-branch dedup, freshness precompute.
447459
4. Embeddings + `search_knowledge` *(shipped v0.2)*; then `pg_search`/BM25 + RRF if vector ranking is insufficient.
448460
5. ADR mining from git/PR history.

0 commit comments

Comments
 (0)