Applies to kgmd 0.2.x
For contributors changing kgmd's code or its documentation. Working through this page from a fresh clone gets you an installed development environment, a clean local check run that mirrors the automated gate, and the conventions that decide whether a change is finished.
git clone https://github.com/johncarpenter/kgmd.git
cd kgmd
python -m venv .venv && source .venv/bin/activate
make installmake install runs pip install -e ".[dev]", which installs kgmd in editable mode plus the dev
extras declared in pyproject.toml: pytest>=7.0, pytest-mock>=3.10, and ruff>=0.4.
Supported interpreters are Python 3.10, 3.11, 3.12, and 3.13 (requires-python = ">=3.10").
Your interpreter must be built with loadable SQLite extension support. kgmd/db.py calls
conn.enable_load_extension(True) and then sqlite_vec.load(conn) on every connection, so an
interpreter compiled without that capability cannot open a corpus database at all — the failure
appears the first time any command touches .kgmd/graph.db, not at install time. The symptom and
the remedy are on the installation page.
No provider credential is needed to develop or to run the test suite. The suite mocks the LLM and never computes an embedding.
Run these three in order before pushing. Each target is one line in the Makefile; the exact
commands are:
| Target | Command it runs |
|---|---|
make format |
ruff format kgmd/ tests/ then ruff check --fix kgmd/ tests/ |
make lint |
ruff check kgmd/ tests/ |
make test |
python -m pytest tests/ -v |
Two more targets exist for release mechanics rather than day-to-day work:
| Target | Command it runs |
|---|---|
make build |
python -m build |
make clean |
rm -rf dist/ build/ *.egg-info plus removal of every __pycache__ directory |
Ruff is configured in pyproject.toml: line-length = 100, target-version = "py310", and lint
rule sets E, F, I, W. I means import sorting is enforced, so a hand-ordered import block
that ruff disagrees with fails make lint. Pytest is configured with testpaths = ["tests"], which
is why a bare pytest from the repository root collects the same files as make test.
.github/workflows/ci.yml runs on every push to main and every pull request targeting main. It
installs pip install -e ".[dev]" on ubuntu-latest across a four-way matrix — Python 3.10, 3.11,
3.12, and 3.13 — and then runs exactly two steps:
ruff check .
pytest -vTwo differences from the local sequence matter:
- CI lints
.(the whole repository), whilemake lintlints onlykgmd/andtests/. A lint error in a root-level or tooling Python file passes locally and fails in CI. - CI runs
ruff checkbut neverruff format --check. Formatting drift is therefore not caught by the gate at all — it is caught only by you runningmake formatlocally. Run it; otherwise reformatting noise accumulates in unrelated diffs.
There is no separate documentation job. The documentation checks are ordinary tests in
tests/test_docs.py and run inside pytest -v on all four interpreters.
Tests are plain pytest functions — def test_something(): at module level. There are no test
classes anywhere in tests/, and new tests should not introduce one.
tests/conftest.py provides a four-stage fixture chain; take the cheapest fixture that gives you
what you need:
| Fixture | Builds on | What you get |
|---|---|---|
tmp_corpus |
tmp_path |
A corpus/ directory with every tests/fixtures/*.md note copied in. |
initialized_corpus |
tmp_corpus |
Adds .kgmd/ with logs/, prompts/, a default config.yaml, and a graph.db created by init_db. |
db_conn |
initialized_corpus |
An open connection to that empty database, closed on teardown. |
seeded_db |
initialized_corpus |
An open connection to a database populated with one document, two chunks, one extraction run, five entities, four mentions, and four relations. |
Rules the existing suite follows:
- The LLM is mocked at the
litellm.completionseam. Tests patch it where it is looked up, for examplepatch("kgmd.llm.litellm.completion", return_value=mock_response)for extraction and resolution, andpatch("kgmd.induce.litellm.completion", ...)for schema induction. Never mock kgmd's own functions to stand in for the provider. - Embeddings are never computed. No test loads a fastembed model. Vectors are hand-packed
float32 blobs —
struct.pack(f"{dim}f", *vec)— and inserted directly, which is the same wire formatkgmd/embed.pywrites. Similarity behaviour is tested by choosing the vectors, not by embedding text. - No network. Nothing in the suite opens a socket, and no test requires a provider credential.
- No sleeps. There are no timing-dependent waits; if a test needs a state transition, it drives the transition directly.
- No absolute paths. Every filesystem test roots itself in
tmp_pathvia the fixture chain.
Test modules are named after the unit under test: test_chunk.py, test_db.py, test_export.py,
test_extract.py, test_induce.py, test_mcp.py, test_query.py, test_resolve.py, and
test_docs.py.
tests/test_docs.py enforces coverage in both directions by introspecting the real surface — Click
command objects for the CLI, DEFAULT_CONFIG for configuration, an AST scan of kgmd/mcp_server.py
for tool names, and the export format choices — and comparing it against the headings, table rows,
and code spans in docs/.
The consequence is symmetrical:
- Adding a command, a parameter, a configuration key, an MCP tool, or an export format without documenting it fails the suite.
- Documenting a command, parameter, key, tool, or format that does not exist also fails the
suite. A stale flag left in a table after the code dropped it is a build failure, as is a
`kgmd ...`invocation naming a subcommand that was never added.
So a capability change and its documentation ship in the same commit. There is no follow-up window in which the docs are allowed to be wrong.
The version stamp on line 2 of every page (> Applies to kgmd 0.2.x) is checked against
kgmd/__init__.py. Bumping __version__ across a minor boundary turns every page red until the
stamps are updated — see the release process.
The suite also rejects, on every page: the usual unwritten-placeholder markers and the "soon"
phrasing (the test greps for them literally, so they cannot be spelled out on a page — read
test_no_unwritten_placeholders in tests/test_docs.py for the exact list), developer absolute
paths, credential-shaped strings, internal links that do not resolve on disk, and any page more than
two links from the documentation index. Pages must keep the fixed prologue — H1,
stamp, blank line, audience paragraph — and exactly one H1.
| Change | Code goes in | Documentation that must be updated |
|---|---|---|
| New pipeline stage | A new kgmd/<stage>.py exposing a run_<stage>(conn, ...) function, wired into kgmd/cli.py as a subcommand and into the build orchestration |
../reference/cli.md command entry; ../concepts.md if it introduces vocabulary |
| New read capability | kgmd/query.py, then surfaced from kgmd/cli.py and, if it should be agent-visible, kgmd/mcp_server.py |
../reference/cli.md, plus ../guides/mcp.md when a tool is registered |
| New prompt | A text asset in kgmd/prompts/, loaded through the same override lookup the other stages use |
./architecture.md prompt asset list |
| New configuration key | DEFAULT_CONFIG in kgmd/config.py, and the stage that reads it — a key with no read site is inert and must be documented as such |
../reference/configuration.md |
| New export format | kgmd/export.py plus the format choice in the export command in kgmd/cli.py |
../reference/export.md |
| New table or column | SCHEMA_SQL in kgmd/schema.py only; nothing else issues DDL |
./architecture.md if it changes the module contract |
Tests for any of the above go in the matching tests/test_<module>.py, using the fixture chain
above.
For the layering rules those destinations follow, read the architecture page.
.omp/commands/speckit.*.md are vendored Spec Kit prompt definitions, installed by specify and
pinned by checksum in .specify/integrations/omp.manifest.json. Upstream, the hook sections require
an agent to invoke any mandatory hook automatically, taking the command id from
.specify/extensions.yml. That file is repository-controlled, so upstream's behaviour lets an
untrusted checkout direct an agent to run an arbitrary command with the agent session's authority.
The committed copies are patched to close that path: a hook command is never invoked without explicit user approval, and only an allowlist stored outside the checkout may pre-approve one. The manifest checksums were regenerated to match the patched files, so the tree is self-consistent.
Re-running specify init in this repository will restore the upstream text and drop the patch. If
you upgrade the scaffold, re-apply it and regenerate the checksums, or the repository ships an
automatic-execution path again. This project defines no hooks: there is no .specify/extensions.yml
and none is expected.