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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **LLM labeling of process paths** (`kb describe`, 4th slice): `DESCRIBE_KINDS` gained
`process_path`, so the key-gated describe pass now writes a grounded NL name/summary for each
materialized business-process path, with a larger facts budget (the path payload —
steps/edges/sink — is rich context). Claims are span-validated against the path's own spans
(fabricated claims dropped; a path with no surviving claim stores nothing) and the stored
description carries `confidence = kept/(kept+dropped)` < 1.0 — the honest pricing of the call
graph's unknown-unknowns promised in DESIGN §9. Route/entity/module/package prompts stay
byte-identical (no `PROMPT_VERSION` bump). The `semantic_grounding_test` gate gains a
process-path fixture (multi-file path via the fixture's own `.kb/sinks.yaml`; real claim kept /
fabricated claim dropped); headline HARD gates stay **fourteen**. Closes DESIGN §14 item 2.
- **Deterministic business-process path extractor** (`kb.extract.deterministic.paths`): the first
SECOND-ORDER extractor — `ExtractContext` gained `prior_artifacts` (the pipeline feeds each
extractor's outputs to later ones; registration order is load-bearing) and the pipeline now
Expand All @@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
silent), grounded on EVERY span along the path (roles `entrypoint`/`step`/`terminal` —
multi-file provenance). Found paths are exact (`confidence` 1.0); incompleteness is an explicit
payload fact. `pyyaml` moved to runtime deps; `summarize`/`embed_text` gained `process_path`
branches. The LLM labeler is the next slice.
branches. The LLM labeler ships in the entry above.
- **Tier-1 process-paths HARD gate** (`kb.eval.tier1_processes_test`): hand-labeled oracle (2-hop
cross-file chain, 0-hop direct sink, event-handler entrypoint), the flagship artifact grounded
across **three files**, cycle no-hang, `.kb/sinks.yaml` override proven end-to-end, no-sink route
Expand Down
19 changes: 13 additions & 6 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,14 @@ rejected. **Verbalized LLM confidence is never used as the score.**
> construction** — every sink claim IS a registry match on the materialized path and every endpoint
> IS an extracted entrypoint. Their `confidence` is 1.0: a found path is machine-checked exact (the
> `call_edge` precedent); the unknown-unknowns of the bounded-recall call graph are priced into the
> FUTURE LLM-labeled process artifact (confidence < 1.0 there), with incompleteness carried as
> LLM-labeled process artifact (confidence < 1.0 there), with incompleteness carried as
> explicit payload facts here, never a fudged scalar.
>
> *Implemented (LLM labeling):* `kb describe` now labels each `process_path` as a fourth describe
> slice — the label's claims validate against the path's own spans (fabricated claims dropped; a
> path with no surviving claim stores nothing), and the stored description ships with
> `confidence = kept / (kept + dropped)` < 1.0, as promised above. Gated in
> `semantic_grounding_test` on the stub LLM (no API key), adversarial fabricated claim included.

---

Expand Down Expand Up @@ -379,7 +385,7 @@ freshness(current|stale@sha)`, with a deterministic tie-break for reproducible e
| `kb.eval` | Tiered eval; deterministic tiers gate CI. | pytest over SHA-pinned golden repos |
| `kb.mcp` | Read-only MCP server; provenance-carrying records; budget-aware assembly. | FastMCP (pinned), Pydantic models |
| `kb.daemon` | Orchestration + CLI: index a repo @ SHA (full or incremental), run extractors in order, write snapshot, host MCP; `kb watch` polls a local branch ref and indexes new first-parent commits incrementally (`branch_ref` cursor, per-commit crash-safe advance). | typer |
| `kb.extract.semantic` | **Shipped:** `kb describe` — LLM-grounded NL descriptions of routes/entities/modules **and per-package architecture overviews** (a package grounded on its own + direct-child modules' spans; the overview synthesizes the import graph + public surface + member-module summaries as context, claims code-grounded), each claim validated against the target's spans by a deterministic sub-property gate (`grounding.validate_claims`); separate key-gated pass, never on `index`. *Deferred:* whole-repo overview; the grounded business-process extractor (entrypoints → call-graph slice → sinks → LLM labeler → span-binding validator). | thin LLM adapter (`kb.llm`); later: `PathEngine` (call-graph), YAML sink registry |
| `kb.extract.semantic` | **Shipped:** `kb describe` — LLM-grounded NL descriptions of routes/entities/modules, **per-package architecture overviews** (a package grounded on its own + direct-child modules' spans; the overview synthesizes the import graph + public surface + member-module summaries as context, claims code-grounded), **and process-path labels** (one per materialized `process_path`, grounded on every span along the path, confidence < 1.0), each claim validated against the target's spans by a deterministic sub-property gate (`grounding.validate_claims`); separate key-gated pass, never on `index`. *Deferred:* whole-repo overview. | thin LLM adapter (`kb.llm`); `PathEngine` + YAML sink registry live in `kb.extract.deterministic.paths` |

---

Expand Down Expand Up @@ -459,10 +465,11 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas
pydantic/FastAPI/SQLAlchemy, static tree-sitter, hand-labeled Tier-1 gate; call-form
`event.listen` deferred).
2. The **one** grounded business-process extractor (named real path + labeler + validator +
deterministic sub-property gate). *Deterministic foundation shipped:* the `call_edge` extractor
+ Tier-1 calls gate, and now the `process_path` builder (PathEngine BFS + sink registry +
`.kb/sinks.yaml` override, multi-file grounded paths, Tier-1 processes gate). The LLM labeler
and the span-binding validator are next.
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,
multi-file grounded paths, Tier-1 processes gate); and the LLM labeler (`kb describe`'s
`process_path` slice — span validation against the path's own spans IS the binding validator,
gated in `semantic_grounding_test`).
3. Recursive invalidation (`artifact_depends_on`), multi-branch dedup, freshness precompute.
4. Embeddings + `search_knowledge` *(shipped v0.2)*; then `pg_search`/BM25 + RRF if vector ranking is insufficient.
5. ADR mining from git/PR history.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ flowchart LR
- **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.
- **A frozen RAG-over-source baseline** and the **Tier-3 knowledge-vs-RAG recall gate** — the honest A/B that backs the "knowledge > RAG" thesis.
- **LLM-grounded descriptions** — an optional, key-gated `kb describe` pass has an LLM write NL summaries for routes, entities, modules (per file), and **packages** (a per-package architecture overview that synthesizes the import graph + public surface + member-module summaries, grounded on the package's own and its direct submodules' spans); every claim is validated against the target's own spans by a deterministic sub-property gate, so ungrounded claims are *dropped* (the anti-hallucination invariant, with a model in the loop). Stored as `extraction_method = "llm_grounded"`, grounded on code spans.
- **LLM-grounded descriptions** — an optional, key-gated `kb describe` pass has an LLM write NL summaries for routes, entities, modules (per file), **packages** (a per-package architecture overview that synthesizes the import graph + public surface + member-module summaries, grounded on the package's own and its direct submodules' spans), and **business-process paths** (each materialized `process_path` chain gets an LLM-written label grounded on every span along the path, with a confidence that honestly stays < 1.0); every claim is validated against the target's own spans by a deterministic sub-property gate, so ungrounded claims are *dropped* (the anti-hallucination invariant, with a model in the loop). Stored as `extraction_method = "llm_grounded"`, grounded on code spans.
- **Incremental re-index** — `kb index --parent <sha>` reuses unchanged files' spans from the parent snapshot and parses only the diff; extraction stays full, so the result is identical to a full re-index (a HARD gate proves it). Auto-detects the parent; falls back to full when none is indexed.
- **Fourteen HARD CI eval gates** (see [Development](#development)).

- **A nightly LLM-judged A/B** (optional, key-gated, **non-gating**) — an answerer LLM answers each question from knowbase's grounded context vs a RAG-over-source context, and a judge LLM scores **answer accuracy** (against hand-written gold) + **hallucination**. Tracked metrics on top of recall; it never blocks CI.

**Not done yet** (and deliberately not faked): ADR mining from git history, the LLM labeling of business-process paths, and languages beyond Python. See the [Roadmap](#roadmap).
**Not done yet** (and deliberately not faked): ADR mining from git history and languages beyond Python. See the [Roadmap](#roadmap).

## Quickstart

Expand Down Expand Up @@ -167,7 +167,7 @@ uv run kb embed --db-url <postgres-url> # separate pass: populate artifact emb
uv run kb describe --db-url <postgres-url> # separate, key-gated pass (ANTHROPIC_API_KEY / OPENAI_API_KEY)
```

`kb describe` has an LLM (via `kb.llm`, `KB_LLM_PROVIDER` in {`anthropic`,`openai`}) write a short NL summary + structured claims for each route, entity, module (per file), and **package** (a per-package architecture overview, grounded on the package's own and its direct submodules' spans, synthesizing the import graph + public surface + member-module summaries as context) in the latest snapshot. **Every claim is validated against that target's own grounding spans** — claims citing a symbol not in the code are dropped, and a `description` artifact is stored only if something survives, grounded on those code spans (`extraction_method = "llm_grounded"`). It needs an API key, never runs on `kb index`, and the deterministic grounding gate is exercised in CI without a key (stub LLM).
`kb describe` has an LLM (via `kb.llm`, `KB_LLM_PROVIDER` in {`anthropic`,`openai`}) write a short NL summary + structured claims for each route, entity, module (per file), **package** (a per-package architecture overview, grounded on the package's own and its direct submodules' spans, synthesizing the import graph + public surface + member-module summaries as context), and **business-process path** (a grounded NL label for the deterministic `process_path` chain, grounded on every span along the path) in the latest snapshot. **Every claim is validated against that target's own grounding spans** — claims citing a symbol not in the code are dropped, and a `description` artifact is stored only if something survives, grounded on those code spans (`extraction_method = "llm_grounded"`). It needs an API key, never runs on `kb index`, and the deterministic grounding gate is exercised in CI without a key (stub LLM).

### Serve to an AI agent (MCP)

Expand Down Expand Up @@ -240,7 +240,7 @@ A Python package `kb` (uv, src-layout). Modules and their responsibilities:
| `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. |
| `kb.rag` | The frozen pgvector RAG-over-source baseline — the "other arm" of the knowledge-vs-RAG A/B (no provenance, no grounding). |
| `kb.extract.semantic` | LLM-grounded extraction (`kb describe`): NL descriptions of routes/entities/modules **and per-package architecture overviews** (rich synthesis from the import graph + public surface + member summaries, grounded on code spans) with a deterministic sub-property gate (`grounding.validate_claims`) that drops any claim not backed by the target's spans. Separate key-gated pass; never on `index`. |
| `kb.extract.semantic` | LLM-grounded extraction (`kb describe`): NL descriptions of routes/entities/modules, **per-package architecture overviews** (rich synthesis from the import graph + public surface + member summaries, grounded on code spans), **and business-process-path labels** (one per `process_path`, grounded on every span along the path) with a deterministic sub-property gate (`grounding.validate_claims`) that drops any claim not backed by the target's spans. Separate key-gated pass; never on `index`. |
| `kb.daemon.cli` | The `kb` CLI: `index` (full or `--incremental`/`--parent`), `watch` (poll a local branch ref, per-commit incremental catch-up, `branch_ref` cursor), `migrate`, `embed`, `describe`, `serve` (MCP), and `introspect` — all functional. |
| `kb.eval` | Fourteen HARD CI gates (identity reproducibility, adversarial grounding, Tier-1 import oracle, Tier-1 API oracle, Tier-1 entities oracle, Tier-1 library-surface oracle, Tier-1 events oracle, Tier-1 call-graph oracle, Tier-1 process-paths oracle, Tier-3 knowledge-vs-RAG recall, Tier-4 one-hop invalidation, invariants, semantic grounding floor, incremental re-index equivalence) plus the supporting MCP / embed / store suite. |

Expand Down Expand Up @@ -300,7 +300,7 @@ Next milestones:
- [x] **LLM-grounded semantic layer** — model-backed artifacts that still carry ≥ 1 span (`extraction_method = "llm_grounded"`): `kb describe` writes span-validated descriptions for routes, entities, modules, and per-package architecture overviews. *(shipped)*
- [x] **Incremental re-index on git push** — `kb index --incremental`/`--parent` reuses unchanged files' spans from the parent snapshot (extraction stays full; equivalence is gated), and `kb watch` polls a local branch ref, indexing each new first-parent commit incrementally with a `branch_ref` cursor (`--max-catchup` guard, `--once` for cron/CI). *(shipped)*
- [ ] **ADR mining** from git / PR history.
- [~] **Grounded business-process extraction** — *deterministic path extraction shipped*: `process_path` artifacts (entrypoint → call chain → sink-registry match, multi-file grounding). LLM labeling + span-binding validator are the remaining piece.
- [x] **Grounded business-process extraction** — `process_path` artifacts (entrypoint → call chain → sink-registry match, multi-file grounding) plus LLM labeling via `kb describe` (a span-validated NL label per path, grounded on the path's own spans, confidence honestly < 1.0). *(shipped)*
- [ ] **More languages** beyond Python.

## Contributing
Expand Down
54 changes: 54 additions & 0 deletions src/kb/eval/semantic_grounding_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
from kb.daemon.pipeline import index_commit
from kb.eval._fixtures import make_git_repo
from kb.eval.tier1_api_test import FILES
from kb.extract.deterministic.calls import CallGraphExtractor
from kb.extract.deterministic.fastapi_contract import FastAPIExtractor
from kb.extract.deterministic.paths import ProcessPathExtractor
from kb.extract.semantic.describe import describe_snapshot
from kb.extract.semantic.grounding import validate_claims
from kb.store import models as m
Expand All @@ -27,6 +29,32 @@
REAL = "OrderOut" # appears in the fixture (schemas.py + the routes' response_model)
FAKE = "nonexistent_symbol_xyz" # appears nowhere -> must be dropped as a hallucination

# A process-path fixture (tier1_processes_test style, unique "lp" module names so the shared
# session DB can't collide): a route handler in ONE file calls a helper in ANOTHER file whose body
# hits a sink from the fixture's own .kb/sinks.yaml. The handler references OrderOut, so REAL is a
# token of the path's entrypoint span and must survive validation on the process path too.
LP_FILES = {
".kb/sinks.yaml": (
"version: 1\nextend: true\nsinks:\n"
' - name: label_write\n patterns: ["*.record_label"]\n'
),
"src/lp/__init__.py": "",
"src/lp/schemas.py": "class OrderOut:\n order_id: int\n",
"src/lp/api.py": (
"from fastapi import APIRouter\n"
"from lp.schemas import OrderOut\n"
"from lp.billing import charge\n\n"
"router = APIRouter()\n\n\n"
"@router.post('/labels')\n"
"def create_label():\n charge()\n return OrderOut()\n"
),
"src/lp/billing.py": (
"class Books:\n def record_label(self, kind):\n return kind\n\n\n"
"books = Books()\n\n\n"
"def charge():\n return books.record_label('label')\n"
),
}


class _StubProvider:
"""Deterministic stand-in for an LLMProvider: always returns one real + one fabricated claim."""
Expand Down Expand Up @@ -148,3 +176,29 @@ def test_package_descriptions_are_grounded(engine: Engine, tmp_path: Path) -> No
with engine.connect() as conn:
prov_files = {p.file_path for p in provenance_for_artifact(conn, sha, row.logical_key)}
assert prov_files # grounded on the package's code spans (>= 1 file)


def test_process_path_descriptions_are_grounded(engine: Engine, tmp_path: Path) -> None:
"""The same span-validation gate covers LLM-labeled process paths (DESIGN.md §9, §14 item 2)."""
sha = make_git_repo(tmp_path, [LP_FILES])[0]
index_commit(
engine,
str(tmp_path),
sha,
# ORDER is load-bearing: ProcessPathExtractor is second-order and must run last.
extractors=[FastAPIExtractor(), CallGraphExtractor(), ProcessPathExtractor()],
first_party_root="src",
)
describe_snapshot(engine, sha, _StubProvider())

rows = [r for r in _description_rows(engine, sha) if r.payload["target_kind"] == "process_path"]
assert len(rows) == 1 # exactly the one materialized path gets a label
row = rows[0]
assert row.logical_key == "desc:process:lp.api.create_label->label_write@lp.billing.charge"
symbols = [c["symbol"] for c in row.payload["claims"]]
assert REAL in symbols # the grounded claim survives on the process path
assert FAKE not in symbols # adversarial: the fabricated claim is dropped here too
assert row.is_deterministic is False # surfaced as llm_grounded
with engine.connect() as conn:
prov_files = {p.file_path for p in provenance_for_artifact(conn, sha, row.logical_key)}
assert len(prov_files) >= 2 # grounded across the path's files (multi-file provenance)
Loading
Loading