Skip to content

Commit ce2ebb9

Browse files
authored
feat(mine): enrich decisions with GitHub PR text (kb mine --prs, ADR slice 2) (#36)
New kb.git.forge module - the ONLY networked step in the toolchain, strictly opt-in: PRProvider protocol + GitHubPRProvider (stdlib urllib, GITHUB_TOKEN/GH_TOKEN optional), origin_slug and pr_number_from_subject (squash '(#NN)' anchor). PR text feeds the prompt (own system prompt) and the payload (capped) - context and fact, never grounding. PR text is MUTABLE after merge, so a digest of the capped text is folded into framework_versions (identity-bearing, the sink-registry precedent): an edited PR yields a NEW artifact id, never a silent payload swap. The enriched prompt pins prompt_version '2+pr1'; plain prompts and plain ids stay byte- identical. A failed fetch degrades to the byte-identical plain artifact, counted per run. Merge commits stay skipped. ADR-mining gate grows six tests; headline HARD gates stay fifteen. Verified live against real GitHub + LLM before merge.
1 parent d8d497a commit ce2ebb9

7 files changed

Lines changed: 467 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **PR-description mining — ADR slice 2** (`kb mine --prs`, `kb.git.forge`): decisions mined
13+
from squash-merged commits (subject ending in `(#NN)`) are enriched with the pull request's
14+
title and body, fetched from api.github.com via a new `PRProvider` protocol +
15+
`GitHubPRProvider` (stdlib urllib; `GITHUB_TOKEN`/`GH_TOKEN` optional — anonymous works for
16+
public repos; `--repo-slug` overrides the origin-derived `owner/name`). **Strictly opt-in and
17+
the only networked step in the toolchain**: without `--prs`, behavior is byte-identical to
18+
slice 1. PR text is prompt context and a payload fact (`pr_number`/`pr_title`/`pr_body`,
19+
capped) — never grounding; and because PR text is MUTABLE after merge, a digest of the capped
20+
text is folded into `framework_versions` (identity-bearing, the sink-registry precedent), so
21+
an edited PR yields a NEW artifact id instead of a silent payload swap. The enriched prompt
22+
runs under its own system prompt and carries its own `prompt_version` (`"2+pr1"`); the plain
23+
prompt stays byte-identical. A failed fetch (404 / network / rate limit) degrades softly to
24+
the byte-identical plain artifact and is counted per run (`pr_enriched` / `pr_fetch_failed`
25+
in the CLI summary) — exactly two identities exist per (sha, model): plain and
26+
enriched(digest). The ADR-mining HARD gate grows six tests (enrichment identity, floor under
27+
enrichment, post-merge-edit → new id, fetch-failure degradation, no-PR byte-compat, slug and
28+
provider parsing incl. fail-soft HTTP paths); headline HARD gates stay **fifteen**.
29+
830
## [0.8.0] - 2026-07-09
931

1032
### Fixed

DESIGN.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ Two reframings carried from review:
8989
- **ADRs are not extractable from code.** Code shows "we use Kafka now", not "we switched
9090
because RabbitMQ couldn't handle load". ADR candidates are mined from git history + commit
9191
messages + PR descriptions. Separate pipeline — **local commit history shipped (slice 1,
92-
`kb mine`); PR-description mining deferred** (needs a network adapter; `kb.git` stays local).
92+
`kb mine`); PR-description mining shipped (slice 2, `kb mine --prs` — strictly opt-in: the
93+
only networked step in the toolchain, off the index/serve paths and OFF by default;
94+
`kb.git.forge` fetches a squash-merged commit's PR title/body from api.github.com). PR text is
95+
context for the LLM and a payload fact — never grounding; and because PR text is MUTABLE after
96+
merge, a digest of the capped text is folded into `framework_versions` (identity-bearing, the
97+
sink-registry precedent) so an edited PR yields a NEW artifact id instead of a silent payload
98+
swap. A failed fetch degrades to the byte-identical plain artifact, counted per run.**
9399
The D5 grounding bridge: a `decision` artifact is grounded on the spans its source commit
94100
**changed** (present at the commit, absent at its first parent — the commit's diff is the
95101
decision's footprint in code), so even prose-born knowledge points at exact code spans. The
@@ -290,7 +296,7 @@ test.)
290296
EMBED stage; tsvector/`pg_search`/BM25/RRF; snapshot Merkle-root; the runtime `app.openapi()`
291297
sandbox oracle (eval-only, later milestone — it executes user code); the grounded
292298
business-process/LLM layer (call-graph slicing, sink registry, labeler, validator); ADR mining
293-
(slice 1 since shipped — `kb mine`; PR mining still deferred);
299+
(both slices since shipped — `kb mine` / `kb mine --prs`);
294300
multi-branch dedup, mutable branch pointers exercise; eval Tiers 2/3 (build when the artifacts
295301
they score exist; stub the Tier-3 question schema now); GC; SCIP/scip-python upgrade.
296302

@@ -418,12 +424,12 @@ freshness(current|stale@sha)`, with a deterministic tie-break for reproducible e
418424
| `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 |
419425
| `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 |
420426
| `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 |
421-
| `kb.git` | Ingest commits/branches; diff SHAs → changed byte ranges → changed span_ids. (PR mining deferred.) | pygit2 |
427+
| `kb.git` | Ingest commits/branches; diff SHAs → changed byte ranges → changed span_ids. `kb.git.forge`: GitHub PR-text adapter (stdlib urllib; feeds opt-in `kb mine --prs` — the only networked module; fail-soft). | pygit2 |
422428
| `kb.store` | Single source of truth: content-addressed spans/artifacts, provenance edges, snapshot manifests. Enforces the ≥1-derived_from invariant at write. | PostgreSQL 17, psycopg 3, SQLAlchemy Core, alembic |
423429
| `kb.eval` | Tiered eval; deterministic tiers gate CI. | pytest over SHA-pinned golden repos |
424430
| `kb.mcp` | Read-only MCP server; provenance-carrying records; budget-aware assembly. | FastMCP (pinned), Pydantic models |
425431
| `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 |
426-
| `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), **process-path labels** (one per materialized `process_path`, grounded on every span along the path, confidence < 1.0), **event-handler descriptions**, **and the whole-repo overview** (one `desc:repo` per snapshot, grounded on the bounded top-level surface via `queries.repo_target`, synthesizing package summaries + cross-package imports + artifact counts as context), each claim validated against the target's spans by a deterministic sub-property gate (`grounding.validate_claims`); separate key-gated pass, never on `index`. **`kb mine`** — ADR-candidate mining over local first-parent history: one `decision` per mined commit, grounded on the commit's changed spans (role `changed`), message verbatim in the payload, same claim floor. *Deferred:* PR-description mining. | thin LLM adapter (`kb.llm`); `PathEngine` + YAML sink registry live in `kb.extract.deterministic.paths` |
432+
| `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), **process-path labels** (one per materialized `process_path`, grounded on every span along the path, confidence < 1.0), **event-handler descriptions**, **and the whole-repo overview** (one `desc:repo` per snapshot, grounded on the bounded top-level surface via `queries.repo_target`, synthesizing package summaries + cross-package imports + artifact counts as context), each claim validated against the target's spans by a deterministic sub-property gate (`grounding.validate_claims`); separate key-gated pass, never on `index`. **`kb mine`** — ADR-candidate mining over local first-parent history: one `decision` per mined commit, grounded on the commit's changed spans (role `changed`), message verbatim in the payload, same claim floor; `--prs` enriches squash-merged commits with PR title/body (digest identity-bearing in `framework_versions`; enriched prompt pinned by its own `prompt_version`). | thin LLM adapter (`kb.llm`); `PathEngine` + YAML sink registry live in `kb.extract.deterministic.paths` |
427433

428434
---
429435

@@ -511,9 +517,10 @@ Review fact-checked these against current (2026) sources. Caveats are first-clas
511517
gated in `semantic_grounding_test`).
512518
3. Recursive invalidation (`artifact_depends_on`), multi-branch dedup, freshness precompute.
513519
4. Embeddings + `search_knowledge` *(shipped v0.2)*; then `pg_search`/BM25 + RRF if vector ranking is insufficient.
514-
5. ADR mining from git/PR history — **slice 1 shipped** (`kb mine`: local commit history, decisions
515-
grounded on the source commit's changed spans, `adr_mining_test` gate); PR-description mining
516-
deferred (network adapter).
520+
5. ADR mining from git/PR history — **both slices shipped**: `kb mine` (local commit history,
521+
decisions grounded on the source commit's changed spans, `adr_mining_test` gate) and
522+
`kb mine --prs` (opt-in GitHub PR-text enrichment via `kb.git.forge`; PR-text digest is
523+
identity-bearing).
517524
6. SCIP/scip-python precise-reference backend behind `PathEngine`.
518525
7. Scale: GC/retention, read replicas, monorepo boundaries, runtime-oracle sandbox hardening.
519526

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ flowchart LR
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.
9595
- **LLM-grounded descriptions** — an optional, key-gated `kb describe` pass has an LLM write NL summaries for routes, entities, **event handlers**, 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), **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), and **the repo as a whole** (one whole-repo overview per snapshot, grounded on the bounded top-level surface and synthesizing the package overviews beneath it); 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.
97-
- **ADR-candidate mining** — an optional, key-gated `kb mine` pass walks local first-parent git history and has an LLM extract the *decision* a commit records (the "why" that code alone cannot show). The provenance bridge: each `decision` artifact is grounded on the spans its source commit **changed** — prose-born knowledge still points at exact code spans; the commit message is stored verbatim as a fact (immutable, pinned by the sha), and every claim passes the same deterministic span-validation floor (fabricated claims dropped; a commit with no surviving claim stores nothing). Local commits only; PR-description mining is a future slice.
97+
- **ADR-candidate mining** — an optional, key-gated `kb mine` pass walks local first-parent git history and has an LLM extract the *decision* a commit records (the "why" that code alone cannot show). The provenance bridge: each `decision` artifact is grounded on the spans its source commit **changed** — prose-born knowledge still points at exact code spans; the commit message is stored verbatim as a fact (immutable, pinned by the sha), and every claim passes the same deterministic span-validation floor (fabricated claims dropped; a commit with no surviving claim stores nothing). With **`--prs`** (strictly opt-in — the only networked step in the toolchain), a squash-merged commit's GitHub PR title/body enrich the prompt and the payload; PR text stays context, never grounding, and a digest of it is folded into the artifact's identity, so a PR edited after merge yields a *new* artifact instead of a silent payload swap.
9898
- **Fifteen HARD CI eval gates** (see [Development](#development)).
9999

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

102-
**Not done yet** (and deliberately not faked): PR-description mining (the network slice of ADR mining) and languages beyond Python. See the [Roadmap](#roadmap).
102+
**Not done yet** (and deliberately not faked): languages beyond Python. See the [Roadmap](#roadmap).
103103

104104
## Quickstart
105105

@@ -176,7 +176,9 @@ uv run kb describe --db-url <postgres-url> # separate, key-gated pass (ANTHROP
176176
uv run kb mine <path-to-repo> --db-url <postgres-url> # separate, key-gated pass (ANTHROPIC_API_KEY / OPENAI_API_KEY)
177177
```
178178

179-
`kb mine` walks the local first-parent history from the latest indexed commit (`--sha` to start elsewhere, `--max-commits` to bound the LLM spend) and has an LLM extract the decision each commit records from its message plus its changed code. Each stored `decision` artifact is **grounded on the spans the commit changed** — the same ≥ 1-span invariant as everything else — with the commit message kept verbatim in the payload as a fact. Claims are span-validated (fabricated claims dropped; nothing survives → nothing stored); merge commits are skipped (PR mining is a future slice), docs-only commits never pay an LLM call, and re-running skips already-mined commits (`--force` to re-mine). Requires the commits to be indexed first (`kb index` / `kb watch`).
179+
`kb mine` walks the local first-parent history from the latest indexed commit (`--sha` to start elsewhere, `--max-commits` to bound the LLM spend) and has an LLM extract the decision each commit records from its message plus its changed code. Each stored `decision` artifact is **grounded on the spans the commit changed** — the same ≥ 1-span invariant as everything else — with the commit message kept verbatim in the payload as a fact. Claims are span-validated (fabricated claims dropped; nothing survives → nothing stored); merge commits are skipped (squash-merge flow is fully covered; grounding a classic merge on its first-parent diff is an open question), docs-only commits never pay an LLM call, and re-running skips already-mined commits (`--force` to re-mine). Requires the commits to be indexed first (`kb index` / `kb watch`).
180+
181+
Add `--prs` to enrich decisions with GitHub PR titles/bodies (`GITHUB_TOKEN=... kb mine <repo> --prs`) — strictly opt-in, the only networked step in the toolchain (anonymous works for public repos at 60 req/h; `--repo-slug owner/name` overrides the origin-derived slug). A digest of the fetched PR text is identity-bearing, and a failed fetch degrades to plain mining with a per-run counter.
180182

181183
### Serve to an AI agent (MCP)
182184

@@ -280,7 +282,7 @@ CI (GitHub Actions, workflow **"CI"**, `.github/workflows/ci.yml`) runs ruff, `m
280282
12. **Semantic grounding floor** — the LLM-grounded describer's claims are validated against the artifact's own spans by a deterministic sub-property gate; an adversarial fabricated claim is *dropped*, never stored, on every describe path (artifacts incl. event handlers, modules, packages, process paths, and the whole-repo overview — whose grounding is provably bounded: a grandchild module never grounds it). Run on a stub LLM, so it gates without an API key.
281283
13. **Incremental re-index equivalence** — an incremental re-index (reuse unchanged files' spans from the parent, parse only the diff) yields the *identical* `{logical_key: artifact_id}` snapshot as a full re-index of the same tree, and the parse is provably skipped for unchanged files (counter assertions); a missing/unindexed parent falls back to full.
282284
14. **Tier-1 process-paths oracle** — materialized business-process paths match a hand-labeled oracle (2-hop cross-file chain, 0-hop direct-sink handler, event-handler entrypoint); the flagship artifact is grounded across **three files**; a reachable call cycle cannot hang the BFS; the fixture's own `.kb/sinks.yaml` proves the override chain; a no-sink route yields no artifact; depth caps are honored.
283-
15. **ADR-mining floor** — mined `decision` artifacts are grounded on the spans their source commit changed (role `changed`, provenance limited to the touched files); an adversarial fabricated claim is *dropped*; a commit whose changed code backs no claim stores nothing; a docs-only commit never calls the LLM; merges are skipped; re-mining is idempotent and never re-bills stored decisions (run on a stub LLM, so it gates without an API key).
285+
15. **ADR-mining floor** — mined `decision` artifacts are grounded on the spans their source commit changed (role `changed`, provenance limited to the touched files); an adversarial fabricated claim is *dropped*; a commit whose changed code backs no claim stores nothing; a docs-only commit never calls the LLM; merges are skipped; re-mining is idempotent and never re-bills stored decisions. PR enrichment is covered too: the PR-text digest is identity-bearing (an edited PR yields a new id, never a silent swap), a failed fetch degrades to the byte-identical plain artifact, and commits without a PR reference stay byte-compatible (run on stub LLM/PR providers, so it gates without keys or network).
284286

285287
The identity rules in `kb.ids` (and `kb.structural`) are **LOCKED**: changing one is a breaking change, gated behind a `NORMALIZATION_VERSION` / `extractor_version` bump so existing digests are invalidated rather than silently colliding.
286288

@@ -310,7 +312,7 @@ Next milestones:
310312
- [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, event handlers, modules, per-package architecture overviews, and a whole-repo overview. *(shipped)*
311313
- [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)*
312314
- [x] **ADR mining — slice 1 (local commit history)**`kb mine` walks first-parent history and stores span-grounded `decision` candidates: each grounded on the spans its commit changed, message verbatim as a fact, claims under the deterministic span-validation floor. *(shipped)*
313-
- [ ] **PR-description mining** — the network slice of ADR mining (PR titles/bodies mapped onto merge commits).
315+
- [x] **PR-description mining**`kb mine --prs` (strictly opt-in, the only networked step) enriches decisions with GitHub PR titles/bodies for squash-merged commits; the PR-text digest is identity-bearing, so post-merge PR edits can never silently swap a payload. *(shipped)*
314316
- [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)*
315317
- [ ] **More languages** beyond Python.
316318

0 commit comments

Comments
 (0)