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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Events v2 — call-form registrations** (`kb.extract.deterministic.events`): the extractor now
also extracts module-level SQLAlchemy `event.listen(Target, "event", fn)` statements — the v1
KNOWN GAP — as a new registration family `sqlalchemy_listen`. `fn` must resolve to a first-party
top-level function of the same module or an imported one (reusing the calls.py import-table
machinery, now shared at module level); the registration attaches to the HANDLER's artifact,
which may live in another module than the `listen()` call, and the artifact gains a grounding
edge on the registering file's module span (new role `registration_site`) — a call-form
registration wired in a third module grounds ONE artifact across THREE files (handler + target
class + registration site). Only module-level calls are extracted (they run deterministically at
import time); `listen(...)` inside a function/class body, lambda/attribute listeners, and the
bare `from sqlalchemy.event import listen` form are documented gaps, never a wrong guess.
Registrations gained `form`/`registration_module`/`registration_line` payload fields. The Tier-1
events gate gains three tests (call-form extracted; three-file provenance; the two new gaps
asserted); headline HARD gates stay **fifteen**.

### Changed

- **`events` extractor version 1 → 2**: all `event_handler` artifact ids are recomputed on the
next index (idempotent re-index rewrites snapshots; superseded rows are orphaned — GC remains
deferred, harmless); `kb describe` regenerates event-handler descriptions on its next key-gated
run. Purely-decorator handlers keep semantically identical payloads (only the new registration
fields are added).

## [0.7.0] - 2026-07-08

### Added
Expand Down
20 changes: 13 additions & 7 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,15 @@ test.)
user code); scored against **griffe** as a dev-only independent static oracle (never on `index`).
- **(d)** Event-handler extractor *(shipped)*: one `event_handler` per handler carrying decorator
registrations — pydantic `@field_validator`/`@model_validator`, FastAPI `@app.on_event`,
SQLAlchemy `@event.listens_for` (stacked decorators aggregate in `payload.registrations`; one
artifact per handler because `artifact_id` is content-addressed over spans + extractor, §6).
Grounded on the handler span + cross-file on the listened-to class; hand-labeled Tier-1 gate.
Known gaps (asserted): call-form `event.listen(...)`, lifespan, pydantic-v1, dynamic names.
SQLAlchemy `@event.listens_for` — AND module-level call-form `event.listen(Target, "e", fn)`
registrations (family `sqlalchemy_listen`; `fn` resolved same-module or via the calls.py import
tables — the handler may live in another module than the listen; grounded additionally on the
registering file's module span, role `registration_site`). Stacked decorators and call sites
aggregate in `payload.registrations`; one artifact per handler. Grounded on the handler span +
cross-file on the listened-to class; hand-labeled Tier-1 gate incl. a three-file provenance case
(handler + target class + registration site). Known gaps (asserted): `listen(...)` inside a
function/class body (conditional), lambda/attribute `fn`, the bare
`from sqlalchemy.event import listen` form, lifespan, pydantic-v1, dynamic names.
- **(e)** Call-graph edge extractor *(shipped)*: one `call_edge` per RESOLVED caller→callee pair,
three deterministic tiers (same-module; imported — **cross-file**; `self.` method of the same
class); precision-first — only first-party-resolved edges are emitted, recall bounded by
Expand Down Expand Up @@ -404,7 +409,7 @@ freshness(current|stale@sha)`, with a deterministic tie-break for reproducible e
| Module | Responsibility | Key tech |
|--------|----------------|----------|
| `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) |
| `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); process paths (`paths.PathEngine` — the first shipped increment of the PathEngine seam — BFS to sink-registry matches, multi-file grounded, registry digest identity-bearing). | grimp, tree-sitter queries; griffe (dev-only oracle) |
| `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` + module-level call-form `listen`, static, cross-file target + registration-site grounding, hand-labeled gate); call-graph edges (per-edge artifacts, three resolution tiers, caller+callee span grounding, hand-labeled gate); process paths (`paths.PathEngine` — the first shipped increment of the PathEngine seam — BFS to sink-registry matches, multi-file grounded, registry digest identity-bearing). | grimp, tree-sitter queries; griffe (dev-only oracle) |
| `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 |
| `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 |
| `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 |
Expand Down Expand Up @@ -490,8 +495,9 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas

1. Second deterministic family: **entities (pydantic/dataclass/SQLAlchemy) — shipped** (static
tree-sitter, hand-labeled Tier-1 gate); **events — shipped** (decorator registrations for
pydantic/FastAPI/SQLAlchemy, static tree-sitter, hand-labeled Tier-1 gate; call-form
`event.listen` deferred).
pydantic/FastAPI/SQLAlchemy plus module-level call-form `event.listen` — v2, static
tree-sitter, hand-labeled Tier-1 gate; function-body/lambda/bare-import forms and lifespan
stay documented gaps).
2. The **one** grounded business-process extractor (named real path + labeler + validator +
deterministic sub-property gate) — **shipped**: the `call_edge` extractor + Tier-1 calls gate;
the `process_path` builder (PathEngine BFS + sink registry + `.kb/sinks.yaml` override,
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ flowchart LR
**v0.7 — spine (artifact-identity rule v2), six deterministic extractors (cross-file grounding, incl. the library public-API surface, event handlers, and call-graph edges) plus the grounded process-path builder over them, LLM-grounded descriptions up to whole-repo architecture overviews and business-process-path labels, ADR-candidate mining from git history (`kb mine`), live incremental re-index (`kb watch`), MCP serving, the fifteen knowledge gates, and published Docker images.** Everything here grounds what it claims, and nothing it cannot:

- **Provenance spine** — content-addressed `span_id` (LOCKED); tree-sitter spans with a normalized S-expression fingerprint and per-SHA location; a single-Postgres, Alembic-managed store with content-addressed idempotent writes; the ≥ 1 `derived_from` anti-hallucination invariant enforced in-app *and* by a deferred DB trigger; pygit2 git ingest (no checkout) with a diff-based invalidation seed.
- **Deterministic extractors** — the **import / dependency graph** (grimp resolves the edge, tree-sitter grounds it on the exact import statement, with an honest `approximate` fallback for re-exports / relative / unmappable imports — never a silent loss); the **FastAPI API-contract** extractor, which grounds a single route **across files** (handler in `routes.py` + `response_model` class in `schemas.py`); the **domain-entity** extractor (pydantic / dataclass / SQLAlchemy classes and their fields, grounded on the class definition **and cross-file on the entities they reference** — purely static, with documented detection limits); the **library public-API-surface** extractor (what a package exposes from its `__init__.py` — `__all__`-authoritative, with `__init__` re-exports resolved **cross-file** to the defining function/class — validated against an independent **griffe** static oracle); the **event-handler** extractor (pydantic `@field_validator`/`@model_validator`, FastAPI `@app.on_event`, SQLAlchemy `@event.listens_for` — one artifact per handler, grounded on the handler span and **cross-file** on the class it listens to; the call-form `event.listen(...)` and dynamic names are documented gaps); and the **call-graph edge** extractor (deterministic caller→callee edges — same-module, imported **cross-file**, and `self.` method calls — one `call_edge` per resolved pair with call-site lines aggregated; `obj.method(...)`, `getattr`, inherited self-calls and decorator/default-arg expressions are documented gaps; the deterministic foundation under the future business-process extractor); and the **process-path builder** (second-order: BFS over the call edges from extracted entrypoints — route/event handlers — to sink-registry matches, one `process_path` grounded on EVERY span of the chain, multi-file provenance; built-in sink registry + per-repo `.kb/sinks.yaml` override).
- **Deterministic extractors** — the **import / dependency graph** (grimp resolves the edge, tree-sitter grounds it on the exact import statement, with an honest `approximate` fallback for re-exports / relative / unmappable imports — never a silent loss); the **FastAPI API-contract** extractor, which grounds a single route **across files** (handler in `routes.py` + `response_model` class in `schemas.py`); the **domain-entity** extractor (pydantic / dataclass / SQLAlchemy classes and their fields, grounded on the class definition **and cross-file on the entities they reference** — purely static, with documented detection limits); the **library public-API-surface** extractor (what a package exposes from its `__init__.py` — `__all__`-authoritative, with `__init__` re-exports resolved **cross-file** to the defining function/class — validated against an independent **griffe** static oracle); the **event-handler** extractor (pydantic `@field_validator`/`@model_validator`, FastAPI `@app.on_event`, SQLAlchemy `@event.listens_for` **and the module-level call-form `event.listen(Target, "e", fn)`** — one artifact per handler, grounded on the handler span, **cross-file** on the class it listens to, and on the registering file for call-form wiring; function-body/lambda `listen` and dynamic names are documented gaps); and the **call-graph edge** extractor (deterministic caller→callee edges — same-module, imported **cross-file**, and `self.` method calls — one `call_edge` per resolved pair with call-site lines aggregated; `obj.method(...)`, `getattr`, inherited self-calls and decorator/default-arg expressions are documented gaps; the deterministic foundation under the future business-process extractor); and the **process-path builder** (second-order: BFS over the call edges from extracted entrypoints — route/event handlers — to sink-registry matches, one `process_path` grounded on EVERY span of the chain, multi-file provenance; built-in sink registry + per-repo `.kb/sinks.yaml` override).
- **`kb introspect`** — a sandboxed, network-blocked `app.openapi()` oracle, eval-only and never on the index path, that the API gate scores the static contract against.
- **Read-only MCP server** — `find_provenance`, `get_knowledge`, and `search_knowledge`, each returning provenance-carrying units (method + confidence + freshness).
- **pgvector embeddings + semantic search** — a replaceable embedding provider (sentence-transformers by default, OpenAI optional) populated by a separate `kb embed` pass; torch stays out of the index path.
Expand Down Expand Up @@ -244,7 +244,7 @@ A Python package `kb` (uv, src-layout). Modules and their responsibilities:
| `kb.extract.deterministic.library_surface` | Static library public-API-surface extractor — one `public_symbol` per name a package exposes from its `__init__.py` (`__all__`-authoritative), `__init__` re-exports resolved **cross-file** to the defining function/class; validated by an independent griffe static oracle. Never imports user code. |
| `kb.extract.deterministic.calls` | Static call-graph edge extractor — one `call_edge` per resolved caller→callee pair (same-module / imported **cross-file** / `self.` method), call-site lines aggregated; only resolved first-party edges are emitted; dynamic/attribute/inherited calls are documented gaps. Never imports user code. |
| `kb.extract.deterministic.paths` | Second-order process-path builder — `process_path` per (entrypoint, sink, terminal): shortest chain of resolved call edges from an extracted route/event handler to a sink-registry match, grounded on every span along the path (multi-file); built-in registry + `.kb/sinks.yaml` override (registry digest is identity-bearing); caps flagged, never silent. |
| `kb.extract.deterministic.events` | Static event-handler extractor — one `event_handler` per handler carrying pydantic validator / FastAPI `on_event` / SQLAlchemy `listens_for` decorator registrations (stacked decorators in `payload.registrations`), grounded on the handler span **and, across files, on the class it listens to**; call-form `event.listen(...)` and dynamic names are documented gaps. |
| `kb.extract.deterministic.events` | Static event-handler extractor — one `event_handler` per handler carrying pydantic validator / FastAPI `on_event` / SQLAlchemy `listens_for` decorator registrations **and module-level call-form `event.listen(...)` registrations** (all aggregated in `payload.registrations`; the handler may live in another module than the `listen()`), grounded on the handler span, **across files on the class it listens to**, and on the registering file's module span (role `registration_site`); function-body/lambda `listen` and dynamic names are documented gaps. |
| `kb.introspect` | Sandboxed, network-blocked `app.openapi()` oracle — eval-only ground truth for the API gate, never on the index path. |
| `kb.mcp` | Read-only MCP server and its provenance-carrying records: `find_provenance`, `get_knowledge`, `search_knowledge`. |
| `kb.embed` | Replaceable embedding adapters (sentence-transformers default, OpenAI optional) + snapshot population. Torch isolated behind the `embed` extra and a lazy import. |
Expand Down Expand Up @@ -272,7 +272,7 @@ CI (GitHub Actions, workflow **"CI"**, `.github/workflows/ci.yml`) runs ruff, `m
4. **Tier-1 API oracle** — the statically-extracted FastAPI contract equals the app's own `openapi()` (from the sandboxed introspect oracle), and the route's cross-file grounding (handler + `response_model`) is asserted.
5. **Tier-1 entities oracle** — extracted pydantic / dataclass / SQLAlchemy entities + their fields match a hand-labeled oracle, each grounded on its class span; a bare declarative `Base` is correctly *not* an entity and a `create_model(...)` model is asserted as a *known* gap.
6. **Tier-1 library-surface oracle** — the statically-extracted public API surface of a package (`__all__`-authoritative, `__init__` re-exports resolved **cross-file**) equals an independent **griffe** static oracle; underscore-private names are excluded, and a third-party / dynamic-`__all__` re-export is grounded-but-flagged, asserted as a *known* gap.
7. **Tier-1 events oracle** — extracted event handlers (pydantic validators, FastAPI `on_event`, SQLAlchemy `listens_for`) match a hand-labeled oracle, each grounded on its handler span and **cross-file** on the class it listens to; the call-form `event.listen(...)` and a dynamic `@app.on_event(EVENT)` name are asserted as *known* gaps.
7. **Tier-1 events oracle** — extracted event handlers (pydantic validators, FastAPI `on_event`, SQLAlchemy `listens_for` and the module-level call-form `event.listen`) match a hand-labeled oracle, each grounded on its handler span and **cross-file** on the class it listens to; a call-form registration wired in a third module grounds ONE artifact across **three files** (handler + target class + registration site); `listen(...)` inside a function body, a lambda listener, and a dynamic `@app.on_event(EVENT)` name are asserted as *known* gaps.
8. **Tier-1 call-graph oracle** — extracted call edges match a hand-labeled oracle (same-module, cross-file imported — incl. aliases and module-attribute forms — `self.` method calls, and class instantiation), each grounded on the caller **and** callee def spans (cross-file); a mutually recursive pair yields two distinct artifacts (identity-v2 regression); `obj.method(...)`, `getattr`, inherited self-calls and decorator-expression calls are asserted as *known* gaps.
9. **Tier-3 knowledge-vs-RAG recall** — knowbase cross-file recall@k == 1.0 for every cross-file question (API contracts **and** domain entities: in each case one artifact already spans both files, so the floor is *structural*, independent of embedding quality); the RAG arm is reported but **never asserted**, so a model bump can't redden CI.
10. **Tier-4 one-hop invalidation** — a content diff invalidates *exactly* the artifacts whose grounding span changed (set-equality: no over-invalidation, no stale survivors); a version bump invalidates everything.
Expand Down
Loading
Loading