Skip to content

Commit a40d44b

Browse files
authored
feat(describe): LLM labeling of process paths (kb describe, 4th slice) (#28)
DESCRIBE_KINDS gains process_path: kb describe now writes a grounded NL name/summary for each materialized business-process path, with a larger facts budget (facts_cap=4000) since the path payload (steps/edges/sink) is rich context. Claims validate against the path's own spans; the stored label ships with confidence = kept/(kept+dropped) < 1.0 — the DESIGN §9 unknown-unknowns pricing. Route/entity/module/package prompts stay byte-identical (no PROMPT_VERSION bump). The semantic-grounding gate gains a process-path fixture (multi-file path via the fixture's own .kb/sinks.yaml; real claim kept, fabricated claim dropped, provenance across 2 files). Headline HARD gates stay fourteen. Closes DESIGN item 2 of §14.
1 parent b903ddc commit a40d44b

5 files changed

Lines changed: 106 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **LLM labeling of process paths** (`kb describe`, 4th slice): `DESCRIBE_KINDS` gained
13+
`process_path`, so the key-gated describe pass now writes a grounded NL name/summary for each
14+
materialized business-process path, with a larger facts budget (the path payload —
15+
steps/edges/sink — is rich context). Claims are span-validated against the path's own spans
16+
(fabricated claims dropped; a path with no surviving claim stores nothing) and the stored
17+
description carries `confidence = kept/(kept+dropped)` < 1.0 — the honest pricing of the call
18+
graph's unknown-unknowns promised in DESIGN §9. Route/entity/module/package prompts stay
19+
byte-identical (no `PROMPT_VERSION` bump). The `semantic_grounding_test` gate gains a
20+
process-path fixture (multi-file path via the fixture's own `.kb/sinks.yaml`; real claim kept /
21+
fabricated claim dropped); headline HARD gates stay **fourteen**. Closes DESIGN §14 item 2.
1222
- **Deterministic business-process path extractor** (`kb.extract.deterministic.paths`): the first
1323
SECOND-ORDER extractor — `ExtractContext` gained `prior_artifacts` (the pipeline feeds each
1424
extractor's outputs to later ones; registration order is load-bearing) and the pipeline now
@@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2232
silent), grounded on EVERY span along the path (roles `entrypoint`/`step`/`terminal`
2333
multi-file provenance). Found paths are exact (`confidence` 1.0); incompleteness is an explicit
2434
payload fact. `pyyaml` moved to runtime deps; `summarize`/`embed_text` gained `process_path`
25-
branches. The LLM labeler is the next slice.
35+
branches. The LLM labeler ships in the entry above.
2636
- **Tier-1 process-paths HARD gate** (`kb.eval.tier1_processes_test`): hand-labeled oracle (2-hop
2737
cross-file chain, 0-hop direct sink, event-handler entrypoint), the flagship artifact grounded
2838
across **three files**, cycle no-hang, `.kb/sinks.yaml` override proven end-to-end, no-sink route

DESIGN.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,14 @@ rejected. **Verbalized LLM confidence is never used as the score.**
343343
> construction** — every sink claim IS a registry match on the materialized path and every endpoint
344344
> IS an extracted entrypoint. Their `confidence` is 1.0: a found path is machine-checked exact (the
345345
> `call_edge` precedent); the unknown-unknowns of the bounded-recall call graph are priced into the
346-
> FUTURE LLM-labeled process artifact (confidence < 1.0 there), with incompleteness carried as
346+
> LLM-labeled process artifact (confidence < 1.0 there), with incompleteness carried as
347347
> explicit payload facts here, never a fudged scalar.
348+
>
349+
> *Implemented (LLM labeling):* `kb describe` now labels each `process_path` as a fourth describe
350+
> slice — the label's claims validate against the path's own spans (fabricated claims dropped; a
351+
> path with no surviving claim stores nothing), and the stored description ships with
352+
> `confidence = kept / (kept + dropped)` < 1.0, as promised above. Gated in
353+
> `semantic_grounding_test` on the stub LLM (no API key), adversarial fabricated claim included.
348354
349355
---
350356

@@ -379,7 +385,7 @@ freshness(current|stale@sha)`, with a deterministic tie-break for reproducible e
379385
| `kb.eval` | Tiered eval; deterministic tiers gate CI. | pytest over SHA-pinned golden repos |
380386
| `kb.mcp` | Read-only MCP server; provenance-carrying records; budget-aware assembly. | FastMCP (pinned), Pydantic models |
381387
| `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 |
382-
| `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 |
388+
| `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` |
383389

384390
---
385391

@@ -459,10 +465,11 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas
459465
pydantic/FastAPI/SQLAlchemy, static tree-sitter, hand-labeled Tier-1 gate; call-form
460466
`event.listen` deferred).
461467
2. The **one** grounded business-process extractor (named real path + labeler + validator +
462-
deterministic sub-property gate). *Deterministic foundation shipped:* the `call_edge` extractor
463-
+ Tier-1 calls gate, and now the `process_path` builder (PathEngine BFS + sink registry +
464-
`.kb/sinks.yaml` override, multi-file grounded paths, Tier-1 processes gate). The LLM labeler
465-
and the span-binding validator are next.
468+
deterministic sub-property gate) — **shipped**: the `call_edge` extractor + Tier-1 calls gate;
469+
the `process_path` builder (PathEngine BFS + sink registry + `.kb/sinks.yaml` override,
470+
multi-file grounded paths, Tier-1 processes gate); and the LLM labeler (`kb describe`'s
471+
`process_path` slice — span validation against the path's own spans IS the binding validator,
472+
gated in `semantic_grounding_test`).
466473
3. Recursive invalidation (`artifact_depends_on`), multi-branch dedup, freshness precompute.
467474
4. Embeddings + `search_knowledge` *(shipped v0.2)*; then `pg_search`/BM25 + RRF if vector ranking is insufficient.
468475
5. ADR mining from git/PR history.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ flowchart LR
9292
- **Read-only MCP server**`find_provenance`, `get_knowledge`, and `search_knowledge`, each returning provenance-carrying units (method + confidence + freshness).
9393
- **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.
9494
- **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.
95-
- **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.
95+
- **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.
9696
- **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.
9797
- **Fourteen HARD CI eval gates** (see [Development](#development)).
9898

9999
- **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.
100100

101-
**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).
101+
**Not done yet** (and deliberately not faked): ADR mining from git history and languages beyond Python. See the [Roadmap](#roadmap).
102102

103103
## Quickstart
104104

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

170-
`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).
170+
`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).
171171

172172
### Serve to an AI agent (MCP)
173173

@@ -240,7 +240,7 @@ A Python package `kb` (uv, src-layout). Modules and their responsibilities:
240240
| `kb.mcp` | Read-only MCP server and its provenance-carrying records: `find_provenance`, `get_knowledge`, `search_knowledge`. |
241241
| `kb.embed` | Replaceable embedding adapters (sentence-transformers default, OpenAI optional) + snapshot population. Torch isolated behind the `embed` extra and a lazy import. |
242242
| `kb.rag` | The frozen pgvector RAG-over-source baseline — the "other arm" of the knowledge-vs-RAG A/B (no provenance, no grounding). |
243-
| `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`. |
243+
| `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`. |
244244
| `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. |
245245
| `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. |
246246

@@ -300,7 +300,7 @@ Next milestones:
300300
- [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)*
301301
- [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)*
302302
- [ ] **ADR mining** from git / PR history.
303-
- [~] **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.
303+
- [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)*
304304
- [ ] **More languages** beyond Python.
305305

306306
## Contributing

src/kb/eval/semantic_grounding_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from kb.daemon.pipeline import index_commit
1919
from kb.eval._fixtures import make_git_repo
2020
from kb.eval.tier1_api_test import FILES
21+
from kb.extract.deterministic.calls import CallGraphExtractor
2122
from kb.extract.deterministic.fastapi_contract import FastAPIExtractor
23+
from kb.extract.deterministic.paths import ProcessPathExtractor
2224
from kb.extract.semantic.describe import describe_snapshot
2325
from kb.extract.semantic.grounding import validate_claims
2426
from kb.store import models as m
@@ -27,6 +29,32 @@
2729
REAL = "OrderOut" # appears in the fixture (schemas.py + the routes' response_model)
2830
FAKE = "nonexistent_symbol_xyz" # appears nowhere -> must be dropped as a hallucination
2931

32+
# A process-path fixture (tier1_processes_test style, unique "lp" module names so the shared
33+
# session DB can't collide): a route handler in ONE file calls a helper in ANOTHER file whose body
34+
# hits a sink from the fixture's own .kb/sinks.yaml. The handler references OrderOut, so REAL is a
35+
# token of the path's entrypoint span and must survive validation on the process path too.
36+
LP_FILES = {
37+
".kb/sinks.yaml": (
38+
"version: 1\nextend: true\nsinks:\n"
39+
' - name: label_write\n patterns: ["*.record_label"]\n'
40+
),
41+
"src/lp/__init__.py": "",
42+
"src/lp/schemas.py": "class OrderOut:\n order_id: int\n",
43+
"src/lp/api.py": (
44+
"from fastapi import APIRouter\n"
45+
"from lp.schemas import OrderOut\n"
46+
"from lp.billing import charge\n\n"
47+
"router = APIRouter()\n\n\n"
48+
"@router.post('/labels')\n"
49+
"def create_label():\n charge()\n return OrderOut()\n"
50+
),
51+
"src/lp/billing.py": (
52+
"class Books:\n def record_label(self, kind):\n return kind\n\n\n"
53+
"books = Books()\n\n\n"
54+
"def charge():\n return books.record_label('label')\n"
55+
),
56+
}
57+
3058

3159
class _StubProvider:
3260
"""Deterministic stand-in for an LLMProvider: always returns one real + one fabricated claim."""
@@ -148,3 +176,29 @@ def test_package_descriptions_are_grounded(engine: Engine, tmp_path: Path) -> No
148176
with engine.connect() as conn:
149177
prov_files = {p.file_path for p in provenance_for_artifact(conn, sha, row.logical_key)}
150178
assert prov_files # grounded on the package's code spans (>= 1 file)
179+
180+
181+
def test_process_path_descriptions_are_grounded(engine: Engine, tmp_path: Path) -> None:
182+
"""The same span-validation gate covers LLM-labeled process paths (DESIGN.md §9, §14 item 2)."""
183+
sha = make_git_repo(tmp_path, [LP_FILES])[0]
184+
index_commit(
185+
engine,
186+
str(tmp_path),
187+
sha,
188+
# ORDER is load-bearing: ProcessPathExtractor is second-order and must run last.
189+
extractors=[FastAPIExtractor(), CallGraphExtractor(), ProcessPathExtractor()],
190+
first_party_root="src",
191+
)
192+
describe_snapshot(engine, sha, _StubProvider())
193+
194+
rows = [r for r in _description_rows(engine, sha) if r.payload["target_kind"] == "process_path"]
195+
assert len(rows) == 1 # exactly the one materialized path gets a label
196+
row = rows[0]
197+
assert row.logical_key == "desc:process:lp.api.create_label->label_write@lp.billing.charge"
198+
symbols = [c["symbol"] for c in row.payload["claims"]]
199+
assert REAL in symbols # the grounded claim survives on the process path
200+
assert FAKE not in symbols # adversarial: the fabricated claim is dropped here too
201+
assert row.is_deterministic is False # surfaced as llm_grounded
202+
with engine.connect() as conn:
203+
prov_files = {p.file_path for p in provenance_for_artifact(conn, sha, row.logical_key)}
204+
assert len(prov_files) >= 2 # grounded across the path's files (multi-file provenance)

0 commit comments

Comments
 (0)