Thanks for your interest in knowbase. This document explains how to set up a dev environment, run the checks, and get a change merged. Please read it before opening a pull request — the project has a few hard rules (the provenance spine, the LOCKED span identity, the eval gates) that exist to keep the knowledge layer trustworthy, and a PR that ignores them cannot be merged.
If anything here is unclear or out of date, open an issue. The architecture and the locked design decisions live in DESIGN.md; this file is about the mechanics of contributing.
- Everything is grounded in code. Nothing is stored unless it is bound to at least one exact
code span (
file:line@sha). This>= 1 derived_frominvariant is enforced in the application (GroundingError) and by a deferred database trigger. Do not add code paths that write knowledge artifacts without grounding, and do not weaken either layer. - Determinism first. knowbase is not an AI code generator and not RAG-over-code. The MVP is a deterministic provenance spine. LLMs and embeddings are replaceable adapters that come later; contributions to the core must be deterministic and testable against ground truth.
- Span identity is LOCKED. See "The LOCKED span-identity rule"
below before touching
kb.idsor the structural fingerprint.
knowbase is a Python 3.12+ package (kb, src-layout) managed with uv.
uv sync --extra dev # create the venv + install runtime and dev dependenciesThat installs the runtime deps (tree-sitter, grimp, pygit2, SQLAlchemy, psycopg 3, Alembic, Typer,
sqlparse, …) plus the dev tooling (pytest, ruff, mypy, pyyaml). After syncing, run anything inside
the project venv with uv run:
uv run kb --help # the CLI (entry point kb = kb.daemon.cli:app)You do not need Docker for development.
The eval gates exercise a real PostgreSQL (the content-addressed store, the deferred grounding trigger, invalidation queries), so the test suite needs a Postgres to talk to. There are three ways it gets one, in priority order:
-
Existing database (preferred in CI and convenient locally). Set
KB_TEST_DB_URLto a SQLAlchemy/psycopg URL pointing at an existing, empty database. The suite uses it directly and never spins a cluster. Example:export KB_TEST_DB_URL="postgresql+psycopg://postgres:postgres@127.0.0.1:5432/postgres"
This is exactly what CI does against its PostgreSQL 17 service.
-
Ephemeral local cluster (the default when
KB_TEST_DB_URLis unset). The suite spins a throwaway PostgreSQL cluster withinitdb/pg_ctl, runs against it, and tears it down — no Docker, no persistent state. You need the PostgreSQL binaries available (from Postgres.app, Homebrew, or a system package). The suite looks forinitdb/pg_ctlonPATHand in common install locations. -
Pointing at out-of-path binaries. If your
initdb/pg_ctlare not onPATH, setKB_PG_BINDIRto the directory that holds them:export KB_PG_BINDIR="/Applications/Postgres.app/Contents/Versions/17/bin"
If no usable Postgres is found, the harness fails with a clear message telling you to set
KB_PG_BINDIR or KB_TEST_DB_URL, or to install PostgreSQL. The migrations under migrations/ are
applied to whichever database is in play; the target is PostgreSQL 17.
Three checks gate every change. All three must pass locally before you open a PR, and all three run in CI:
uv run ruff check src/kb # lint
uv run mypy # strict type-check
uv run pytest src/kb/eval -q # the eval gates (provisions a test database as described above)ruff checkmust report no findings.mypyruns in--strictmode (configured inpyproject.toml; packages =kb). It must be clean. Do not add blanket# type: ignore; if a third-party stub is missing, prefer a narrow, justified ignore or a typed wrapper. Thekb.evalpackage is intentionally excluded from strict typing — it is validated by running, not by types.pytest src/kb/eval -qruns the gates. They are the source of truth for correctness; treat a red gate as a blocking defect, never as flakiness to retry around.
The eval suite (src/kb/eval/) is five HARD gates. They are not ordinary unit tests — they pin the
guarantees the whole project rests on, and CI fails if any of them fails:
- Identity reproducibility (
identity_test.py) — span identity is reproducible and location- independent. Formatting, comment, docstring, byte-offset, file-path, and commit changes must NOT change aspan_id; a rename MUST. No database is involved; this is the pure identity core. - Adversarial grounding (
adversarial_test.py) — the anti-hallucination invariant holds end to end. An ungrounded artifact is rejected by the application (GroundingError, before any write) and by the deferred database trigger at COMMIT; a genuinely grounded artifact commits cleanly. - Tier-1 import oracle (
tier1_imports_test.py) — deterministic import/dependency edges match a hand-labeled oracle exactly, grounded on the actual import-statement span, with known static-analysis blind spots asserted as explicit gaps rather than silent losses. - Tier-4 one-hop invalidation (
tier4_invalidation_test.py) — a content diff invalidates exactly the artifacts whose grounding span changed (no over-invalidation, no stale survivors), and a version bump invalidates everything, kept distinct from content-diff invalidation. - Invariants (
invariants_test.py) — cross-cutting invariants over a real index: zero orphans (every stored artifact is grounded by>= 1derived_from), and re-indexing the same SHA yields the identical set of artifact ids.
If you add an extractor (e.g. an API-contract or events extractor), it must ship with a deterministic gate backed by ground truth — a hand-labeled oracle for a small fixture repo, asserted for exact, grounded output, in the same spirit as the Tier-1 import gate. The bar:
- Build the fixture (use the helpers in
src/kb/eval/_fixtures.pyto construct a git repo) and hand-label the expected artifacts and their grounding spans. - Assert the extractor produces exactly those artifacts, each bound to the correct span.
- Assert any known limitation explicitly as a gap, never let it pass as a silent loss.
- Every emitted artifact must carry
>= 1derived_fromedge (seekb.extract.base).
An extractor without a deterministic ground-truth gate will not be merged. "It seemed to work on my repo" is not ground truth.
- Formatting & lint: ruff, configured in
pyproject.toml(rule setsE, F, W, I, N, UP, B, C4, RUF, targetpy312). Runuv run ruff check src/kb. Useuv run ruff check --fixfor mechanical fixes anduv run ruff formatif you want consistent formatting. - Line length: 100.
- Typing: mypy
--strict. New code inkbmust be fully typed. - Tests: the eval tests live under
src/kb/eval/and are named*_test.py(pytest is configured withpython_files = ["*_test.py", "test_*.py"]andtestpaths = ["src/kb/eval"]). Name new gate files<thing>_test.py. Shared, non-test helpers in that package are prefixed with an underscore (e.g._fixtures.py,_pg.py) so pytest does not collect them. - Keep modules focused and docstring the why, matching the existing house style (every gate file opens with a docstring stating what guarantee it pins and the relevant DESIGN.md section).
Content-addressed identity is the spine of the system, and it is LOCKED. The rules that define a
span_id and an artifact id are pinned because changing them silently would let new digests collide
with — or diverge from — digests already stored in real databases.
The identity rules live in:
kb.ids— the pure hashing functions;NORMALIZATION_VERSIONis defined here.span_iddeliberately excludes the file path and byte offsets.kb.structural.fingerprint— the normalized S-expression fingerprint (named nodes only; comments and docstrings dropped; identifiers and literals kept).- the structural symbol-path computation that feeds
fq_symbol_path.
If you change any span-identity rule — the fingerprint normalization, what is dropped or kept,
the symbol-path derivation, or the hashing in kb.ids — you MUST bump
kb.ids.NORMALIZATION_VERSION. This invalidates existing digests safely instead of letting them
collide. (For artifact-level extractor changes, bump the relevant extractor_version the same way.)
The identity gate exists precisely to catch unbumped changes: it will fail if the LOCKED behavior
shifts without an explicit version bump. A PR that alters identity rules without bumping the version
will be rejected.
Use Conventional Commits for commit messages:
<type>(<optional scope>): <imperative summary>
Common types: feat, fix, docs, refactor, test, perf, chore, ci. Scope the project
areas where it helps (e.g. feat(extract): add FastAPI route extractor, fix(store): …,
test(eval): …). Keep the summary in the imperative mood and under ~72 characters; put rationale
and context in the body. If a change bumps NORMALIZATION_VERSION or is otherwise breaking, mark it
with ! (e.g. feat(ids)!: …) and/or a BREAKING CHANGE: footer.
- Branch from
master. Create a focused topic branch (e.g.feat/api-contract-extractor). - Keep PRs small and focused. One logical change per PR. Large, mixed PRs are hard to review and slow to merge; split unrelated changes.
- Make the checks pass locally before pushing:
uv run ruff check src/kb,uv run mypy, anduv run pytest src/kb/eval -qmust all be green. - Add or update gates for behavior you change. New extractors require a deterministic ground-truth gate (see above).
- Open the PR against
master. Describe what changed and why, and call out anything that touches the provenance spine, the LOCKED identity rules, or the migrations. - Green CI is required to merge. The GitHub Actions workflow named CI
(
.github/workflows/ci.yml) runs ruff, mypy--strict, and the eval gates against a PostgreSQL 17 service. A PR cannot be merged until CI is green.
Review is about correctness and the invariants first; please be patient and responsive to feedback.
- Bugs: open an issue using the bug-report template:
.github/ISSUE_TEMPLATE/bug_report.yml. Include the exact command, the repo/SHA you indexed, the database URL scheme (not credentials), and the full error and traceback. For anything involving identity or invalidation, the smaller the reproducing fixture, the better. - Features: open an issue using the feature-request template:
.github/ISSUE_TEMPLATE/feature_request.yml. Describe the durable knowledge you want extracted and how it would be grounded in code spans. Proposals that fit the roadmap (API-contract extraction, read-only MCP serving, then semantic search) and respect the provenance/determinism principles are easiest to land.
For security-sensitive reports, please do not open a public issue; contact the maintainers privately.
knowbase is licensed under AGPL-3.0-or-later (see LICENSE). By contributing, you agree that your contributions are licensed under the same terms. Do not add code or assets that are incompatible with AGPL-3.0-or-later.