Development
Prerequisites
uv (it installs Python 3.12 for the project)
A stable Rust toolchain, 1.95 or newer, for the Rust core; uv fetches maturin itself
Docker, for the Postgres and Iceberg integration tests
Node.js 24 (see
.nvmrc) and pnpm, only to work on the graph viewervhs, only to re-record the demo GIFs
Installation
Clone the repository
git clone git@github.com:kodexlabsio/konstruct.git cd konstruct
Install dependencies and the git hooks
uv sync uv run pre-commit install
The hooks run Ruff and mypy before each commit (and the viewer checks when
viewer/changed), check the commit message, and run the tests before each push.uv syncalso compiles the Rust core, in release mode, intokonstruct/_core.abi3.so(_core.pydon Windows) in the source tree. uv rebuilds it whenever a file undersrc/changes, the next timeuv syncoruv runruns.
Tests
uv run playwright install chromium # once, for the browser tests
uv run pytest # Postgres and Iceberg REST tests skip without services
eval "$(.kodex/scripts/services.sh up)" # start them in Docker and export their addresses
uv run pytest --cov # everything, with branch coverage (fails under 90%)
.kodex/scripts/services.sh down
Kind |
Where |
What it checks |
|---|---|---|
Unit |
|
Each module, including lineage through CTEs, joins, aliases, |
Writing graph.json |
|
A graph written against orjson’s text of its plain data ( |
Native graph classes |
|
|
Reading graph.json |
|
|
Property-based |
|
Invariants on generated inputs with Hypothesis: impact and trace are inverse, lineage matches generated CTE chains in seven dialects, Airflow dependencies match generated DAGs, no text crashes the SQL analysis |
Integration |
|
DuckDB, Postgres and Iceberg (SQL and REST catalogs) for real; Snowflake against fakesnow and BigQuery against its emulator; Databricks against a stub; builds of projects with and without dbt; inputs changed while a build runs, after it read them, or before it watched them (konstruct.toml, dbt_project.yml), deleted and renamed, edited to the same size with the time put back, saved atomically or only chmodded; new files in a directory it read, and files beside a file it read with its extension, each kept as a digest of what says a file changed, that appear or become inputs by the rule discovery found that file by (a SQL file or DAG beside another, a SQL file at the top that |
End to end |
|
The example built with real dbt, every CLI command, the Python API, and the MCP server in process and over stdio |
Golden files |
|
The exact text of the CLI output and REPORT.md; regenerate with |
Terminal output |
|
Colour and its off switches ( |
Interfaces |
|
What users build on, as a snapshot: the command line (commands, options with what each takes, positional arguments by position, choices) and its exit statuses, the shape of every |
Compatibility |
|
The graphs each release built of the example and of an orchestration project (the example with the Dagster and Airflow fixtures), with the |
Browser |
|
graph.html in Chromium: pages, column rows and edges, formulas, SQL, routes and old deep links, selectors, full screen, keyboard navigation of the graph, search, export, themes, languages and right-to-left layout, WCAG 2.2 AA with axe-core in both themes and at 200% zoom, full screen and the top bar at narrow widths, Markdown descriptions, deep quotes and cut descriptions, translated selector errors, the Content-Security-Policy, a served page reloading on server-sent events, hostile text, a 1,200-model graph |
World map |
|
The world page on jaffle_shop: an entity per node with its trust and metrics, cardinalities at both ends of each relationship, the keyboard, proposed metrics never shown as approved, WCAG 2.2 AA in both themes; a failed check marked by shape, a page written before trust was recorded with its own edge style, metrics of one name and entity from two relations and from two Looker views, metrics with no entity, and a 400-entity ontology under a heading that counts it, whose names appear only once readable; in Vitest, trust, the key above the map, a unique key for each metric, the metrics of an older page, formulas and sources from the columns, the layout of 2,000 entities without overlaps, the block of entities in no relationship, and the arrow keys |
Map |
|
The map page in Chromium with WebGPU on SwiftShader: drawn pixels, the same layout and pixels on every load, search, the keyboard and the tab order, the list as one tab stop, filters, opening pages and coming back, WCAG 2.2 AA in both themes, the CSP, the message without WebGPU, buffers larger than the device allows, and characters the glyph atlas has no room for; in Vitest, the scene in the lineage page’s layout, placement, buffer packing, the camera, picking against a full scan, colours, the distance field, text setting, and the renderer against a fake device’s limits and error scopes |
Usage |
|
The SQL sent to Snowflake, BigQuery and Databricks, query text parsing for BigQuery, usage on columns, in the report and describe, the daily cache, off by default, and missing permissions as a warning, against stub drivers in each vendor’s row shapes |
Search by meaning |
|
A renamed revenue column found through abbreviations, lineage and formulas, synonyms, formulas by their shape, exact names first, |
Conflicts |
|
Two revenue definitions that differ by one filter (the pair, the filter and its file), different sources, ranking, grouping by business terms, copies left out, no false conflicts on the example; formulas written differently counted as one when their columns have |
Formula identity |
|
Rewrites that are the same, the same only without NULLs, and different, with the kind of witness; random formulas checked against DuckDB on random tables with NULLs and empty tables, and each witness run in DuckDB, a value it gives a column compared with a constant as a value of the column; a sum and the same sum reordered, past the limit on terms too, the same under every fresh key; constants that wrapped around a fixed modulus; presences kept whole that collided; columns with |
Ontology |
|
Entity names and keys from unique tests, primary keys, identifier fields and grains; terms that name entities; measures and their formulas; relationships, cardinality and |
Ossie |
|
Both Ossie shapes, the build input and its warnings, composite keys and relationships, data types, and an export validated against the Ossie schema, including a metric read from Ossie and written back |
Ossie portability |
|
Ossie’s own M0 to M3 portability ladder ( |
Looker |
|
LookML views: tables (or the view’s own name), the columns each field reads (through other fields, dimension group timeframes and the default ones, own-view references, location and duration SQL, measure filters, cycles), refinements, |
Power BI |
|
TMDL tables, descriptions, columns, measures (one line, several lines, with properties and annotations after them), calculated columns, tabs or spaces, CRLF and a byte order mark; warehouse tables from |
Definitions |
|
The definitions.yml format and its errors, terms in the build, |
Formulas |
|
|
Export |
|
OpenLineage events validated against the OpenLineage JSON schemas; Mermaid, DOT and CSV |
Scale |
|
A generated 300-model project: full build and |
Real world |
|
dbt Labs’ jaffle-shop at a pinned commit (needs network; |
Lineage against data |
|
|
Gate |
|
|
Repositories |
|
Several repositories in one graph: lineage, terms and options across them, a relation one builds and another reads as one node whatever the order of |
Lineage lockfile |
|
|
Data laws |
|
Laws proposed only where the SQL keeps a total and confirmed per day on DuckDB and Postgres: a planted fan-out join in the data and in the SQL, each law kind both ways, |
Change review |
|
|
PySpark |
|
The SQL a job passes to |
Dagster |
|
The Dagster parser, assets and op jobs joining dbt and warehouse lineage, sensors ordering work, and parity with Dagster’s own asset and job graphs (needs Dagster; see the CI job) |
Databricks |
|
The connector against a Databricks workspace with Unity Catalog; runs when workspace credentials are set (see the file) |
Plugins |
|
The example plugin installed from its own entry points: its input read into the graph with column lineage from its SQL, its check in the gate, its pack bound and its MCP tool served, in process and in a fresh CLI process; plugins off until enabled, |
Ontology packs |
|
Every pack loads with a title, kinds and definitions and no status; every match block picks out its own pack and requires two columns that are not generic; a block of generic table names with fewer than three such columns needing the schema’s name; matching by tables, source-only columns and a schema name or loader, singular or plural; a schema named for its source binding two entity tables in any naming ( |
Loaders |
|
Which tool landed a table, for every mark, with its documentation; application tables with lookalike columns ( |
Loader configuration |
|
dlt pipelines (resources, sources, |
Vocabularies |
|
SKOS concepts and OWL classes as terms (labels in other languages, synonyms, definitions, broader concepts, classes as entities); merging into |
Trino |
|
The connector against a stub of the client (comments from |
Agent toolkit |
|
Every skill, command, rule and instruction text in the house style, with frontmatter each agent accepts and only real commands and options; what each skill says Konstruct prints, checked against the output of a real build of the example (impact, trace, explain, define, ontology, conflicts, diff, gate on a breaking change and on a changed approved term, verify, the Ossie export); the plugin in |
Agent benchmark |
|
The harness on a miniature Spider 2.0-DBT tree with a scripted agent: both arms, the same prompt, the graph and skills only in the Konstruct arms, the general-skill arm, |
Answer benchmark |
|
The question set’s fingerprint against its recorded version (questions, definitions and project pins), refusing bad questions, every reference answer and every trap on the built example’s DuckDB warehouse (and on jaffle-shop in the real-world job), the rubric on sample answers (tolerances by unit, cents for dollars, sure and unsure answers, a doubt that is no doubt, abstained and unanswered conversations), intervals and templates, a read-only warehouse with a memory limit that stops slow queries, what each arm sees (the project arm’s files included), only Konstruct’s tools that answer from the graph in the konstruct arm and no model key in its server, a run that stops when a graph changes, a prepared project built again when the question set or the harness changed, conversations with stub agents, repeats, resuming, the plan, the dry run, an output inside a git repository refused, a run from the command line with a stub SDK, and the paper’s numbers only when its flag is set |
Survey |
|
The public-project survey on local git repositories: a dbt project built through a generated DuckDB profile with file and network access off, one that is not dbt and one that does not clone, a project that cannot write outside its database or read the surveyor’s environment, pinning by commit, the timeout, |
Rust core |
|
Walks, neighbours, paths and relation names against brute-force scans on generated graphs; search ranking, limits and kinds; topic routing with Java replacements, refusing backreferences, lookaround and possessive repeats, in linear time on expressions that backtrack; FNV-1a 64 against its published vectors and precomputed hashes; word boundaries and substring search against byte-by-byte references on every short string, every byte value, every length to 300 at every alignment, megabyte inputs and strings that end at an unreadable page; the JSON scan against a byte-by-byte reference in the same ways, with strings, escaped quotes, backslash runs of every length across block boundaries, multi-byte UTF-8 and structural characters inside strings, and documents longer than a chunk; JSON string escaping and integer digits against byte-by-byte references over every byte value, every length and strings that end at an unreadable page; the JSON grammar’s values and errors; the graph’s shape and index read from a document; the int8 inner product against an exact reference over every tail length and alignment; each routine on every supported architecture; the scan for the words of Airflow and Dagster files against a plain reference in the same ways; HNSW recall against brute force, and indexes that do not depend on threads, arrival order or updates |
Discovery |
|
The walk against |
Formula identity in the Rust core |
|
Montgomery multiplication against 128-bit integer arithmetic and primality on every supported architecture; BLAKE2b against the 256 keyed known-answer vectors of its authors ( |
Paper |
|
The paper names every command, MCP tool and export format, types no number outside a sentence that cites its source (every measurement is a generated macro), uses only macros |
Server |
|
|
Parity |
|
The static Airflow parser against Airflow’s own DagBag (needs Airflow; see the file for the command) |
Fuzzing |
|
Atheris (coverage-guided) harnesses for the readers of untrusted input: |
After a release is tagged, the next batch adds it to the compatibility corpus; prepare_release.py
refuses to prepare the next release, and its --check refuses a pull request that prepares one,
until it is there (the release workflow’s own release from above main, for changes that reached
main without a batch, is not held to it):
uv run python .kodex/scripts/compat_snapshot.py --ref v0.12.5
The script builds the corpus projects with that release, from a git worktree with its own
environment, and records it in tests/compat/releases.json. Its outputs go to
tests/compat/v0.12.5/, unless they are the same as the newest release’s apart from the version,
in which case the release shares that folder. When test_compat then finds an older release’s
answer changed, the change is either a regression to fix or a deliberate one to list in
tests/compat/allowed.yml with its reason.
The dev dependencies include dbt-core and dbt-duckdb, so the dbt and end-to-end tests build a copy of the example project for real. In CI, KONSTRUCT_REQUIRE_SERVICES=1 turns a missing service or browser into a failure rather than a skip, and HYPOTHESIS_PROFILE=ci runs more generated examples. KONSTRUCT_BROWSER_CHANNEL=chrome runs the browser and map tests in an installed Google Chrome, and KONSTRUCT_BROWSER_EXECUTABLE in the Chromium binary at that path instead of the one Playwright downloads for its version.
The Rust Core
Konstruct is one package of Python and Rust, built by maturin with PyO3. The crate at the repository root (Cargo.toml, src/) is the engine: the graph model and its files, the walks and searches over it, and whatever else is measurably faster compiled or must run in bounded time on any input (the routing expressions of Kafka Connect configuration). The Python package is the API’s surface and the build’s inputs; it reaches the crate through the extension module konstruct._core, which konstruct/_core.pyi describes to mypy (and which tests/test_interfaces.py reads for the types of Graph, Node and Edge). Each part exists once: what moved to Rust was removed from Python.
File |
Contents |
|---|---|
|
The graph’s edges in compressed sparse row form: impact and trace walks, the neighbours |
|
The walk of a repository’s files on every CPU, in the order |
|
Relations by every name they answer to, for name resolution ( |
|
Ranked word search ( |
|
The JSON reader |
|
The shape of a |
|
The graph model: |
|
|
|
Where the words of names, descriptions and queries start and end, for search, hand-written in assembly for each supported architecture |
|
Whether a search word occurs in a label, and whether a key holds its escape character, hand-written in assembly for each supported architecture |
|
Whether a Python file mentions Airflow or Dagster, the words that begin the test for a DAG file and for a Dagster file ( |
|
Python source read as a syntax tree and never run, for the two readers below: Ruff’s parser (pinned, see AGENTS.md), the nodes in the order |
|
The Airflow reader ( |
|
The Dagster reader ( |
|
FNV-1a 64, the hasher of the indexes’ maps, hand-written in assembly for each supported architecture: x86_64, aarch64, riscv64, powerpc64le, s390x and loongarch64 |
|
Whether two formulas compute the same thing ( |
|
Search by meaning: identifiers split into words and concepts ( |
|
The server: hyper on tokio, fixed routes, ETags and encodings, server-sent events, reloads, tokens and limits |
|
The HTTP API’s route table, from which routing and the OpenAPI document are both made |
|
|
|
The PyO3 bindings, built with the |
|
Property tests against brute-force walks, paths and name scans on generated graphs |
|
Property tests of the formula identity test against symbolic expansion |
The graph builds each index the first time a query needs it and keeps it until the graph changes through add_node, add_edge or remove_edge; code that edits a node or an edge in place calls Graph.changed(). Text is folded to lower case in Python before it reaches Rust, so matching folds case exactly as str.lower does.
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo test # unit and property tests, without Python
cargo bench --bench core # the indexes on a generated project of 2,000 models
The other architectures’ tests run under qemu-user in CI, each in a Debian container with that architecture’s cross compiler. To run one locally, for example riscv64, install qemu-user, gcc-riscv64-linux-gnu and libc6-dev-riscv64-cross, then:
rustup target add riscv64gc-unknown-linux-gnu
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc \
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUNNER="qemu-riscv64 -L /usr/riscv64-linux-gnu" \
cargo test --target riscv64gc-unknown-linux-gnu
To add a function, write it in the crate with its tests, export it in src/python.rs, describe it in konstruct/_core.pyi, delete the Python it replaces, and measure with .kodex/scripts/benchmark.py and cargo bench --bench core before and after. uv build makes the sdist and a wheel for the machine it runs on.
The Agent Toolkit
The skills, slash commands, subagent and instructions are Markdown files in konstruct/interfaces/toolkit/, package data that ships in the wheel. A reference to another skill is written [[name]] and rendered for where it goes: konstruct-name when installed, /konstruct:name in the plugin. konstruct install renders them for each agent, and the Claude Code plugin in claude-code-plugin/ (listed by .claude-plugin/marketplace.json) is built from the same files:
uv run .kodex/scripts/build_plugin.py # after changing a toolkit file; commit claude-code-plugin/ with it
uv run .kodex/scripts/build_plugin.py --check
claude plugin validate . # the marketplace and the plugin, with Claude Code installed
test_toolkit fails while the plugin is out of date, when a text names a command or option that does not exist, and when a skill quotes output that a build of the example does not print.
The HTTP API’s operations are listed once, in src/serve/api.rs, which routes requests and writes the OpenAPI document. After changing an operation, regenerate docs/reference/openapi.json with KONSTRUCT_UPDATE_GOLDEN=1 uv run pytest tests/test_serve.py -k openapi and review the diff. uv run .kodex/scripts/benchmark_serve.py measures the server: start-up, latency and throughput under load (with the load client in .kodex/scripts/load.rs, built with rustc), a Python-backed answer cold and cached, and memory.
The Graph Viewer
graph.html is a Svelte 5 and TypeScript app in viewer/, built with Vite into one self-contained file, konstruct/outputs/viewer.html, with its script, styles and fonts inlined (the font licences are licenses/manrope.txt and licenses/jetbrains-mono.txt). That file is committed, so installing Konstruct needs no Node.js; Konstruct fills its two data placeholders when it writes graph.html: the graph, and the formulas, SQL, ontology and conflicts the pages show beside it. The pure logic (selectors, routes, search, layout, message formatting) is in viewer/src/lib/ with Vitest tests in viewer/tests/; the message catalogues are in viewer/src/locales/. Its package.json, lock file and toolchain configs (tsconfig.json, vite.config.ts, svelte.config.js, .prettierrc) are at the repository root, beside Cargo.toml and pyproject.toml, and every pnpm command runs there.
pnpm install
mkdir -p viewer/dev && cp examples/jaffle_shop/konstruct-out/graph.json viewer/dev/ # data for the dev server
pnpm dev # http://localhost:5173/viewer.html, reloading as you edit
pnpm check # svelte-check (strict TypeScript)
pnpm test # Vitest: selectors, routes, search, layout, catalogues and the graph model
pnpm build # writes konstruct/outputs/viewer.html; commit it with the source
Pre-commit hooks check formatting and types and rebuild the page when viewer/ changes, and a CI job fails if the committed build does not match the source. The browser tests (tests/test_browser.py) drive the built page in Chromium and fail on any network request.
Other Commands
The Makefile at the root runs the commands on this page by name; make alone lists its targets. The targets that run several tools run each of them and fail if any failed, so one run reports every problem.
Target |
Runs |
|---|---|
|
|
|
|
|
Ruff, rustfmt and Prettier, writing or only checking |
|
Ruff, mypy, Clippy with |
|
|
|
|
|
|
|
|
|
|
uv run ruff check,uv run ruff format,uv run mypy: lint, format and type check (strict)uv run .kodex/scripts/prepare_example.py: build the example warehouseuv run python paper/generate.py, thenlatexmk -pdf konstruct.texinpaper/: measure every result of Konstruct the paper quotes and build the PDF (TeX Live withlatexmk,texlive-latex-extra,texlive-fonts-recommendedandtexlive-science);--onlyreruns some sections and--sizessets the benchmark’s project sizesvhs .kodex/scripts/demo-cli.tape: re-recorddocs/images/demo-cli.gif.kodex/scripts/demo_diff_setup.sh && vhs .kodex/scripts/demo-diff.tape: re-recorddocs/images/demo-diff.gifvhs .kodex/scripts/demo-claude-code.tape: record the same question through/konstructin Claude Code (needs Claude Code signed in)
Agent Benchmark
.kodex/scripts/agent_benchmark.py runs a coding agent on Spider 2.0-DBT, dbt projects on DuckDB each with an instruction and a gold database, in two arms: the project as shipped, and the same project after konstruct build and konstruct install --target .. A third arm, --arms baseline konstruct konstruct-general, has only the general konstruct skill, so comparing it with konstruct measures what the workflow skills add. Both arms get the same prompt, which carries the benchmark’s own hints and does not mention Konstruct. As in the benchmark’s protocol, the agent runs dbt run itself, and the database the project’s dbt profile writes is scored as the agent leaves it, with the benchmark’s own duckdb_match.
uv run .kodex/scripts/agent_benchmark.py ~/Spider2/spider2-dbt --agent 'claude -p {prompt} --permission-mode acceptEdits'
{prompt} in the agent command is replaced by the prompt, quoted. Prepare the benchmark first as its README says (download its databases, run its setup.py). Each project copy is its own git repository, and --out (by default a new temporary directory) must not be inside a git repository or under a CLAUDE.md, CLAUDE.local.md, .claude or AGENTS.md, so the agent reads no instructions from above the project. --only runs some instances, --arms the arms to run, --install-for the agents konstruct install writes for (--agent, such as codex for an agent command that runs Codex; default both), --mcp also registers the MCP server (pass it to the agent too, such as --mcp-config .mcp.json; the baseline copy gets an empty .mcp.json, so the same command works in both arms), --dbt-run runs dbt run once more before scoring, and --timeout stops an agent and everything it started; whatever an agent leaves running is stopped before scoring. Each run writes the projects with their logs beside them, results.jsonl and summary.md; a run that stops resumes with the same options.
Answer Benchmark
.kodex/scripts/answer_benchmark.py asks an AI agent business questions about a dbt project and counts how often it gives a confidently wrong answer: a wrong number it says it is sure of. The questions are in .kodex/scripts/answer_questions/questions.yml, on the bundled example and on dbt Labs’ jaffle-shop at the commit tests/test_realworld.py pins. Each has one numeric answer, computed by a reference query committed with it that reads only the tables the project’s seeds load. Each question but a control has a trap (an answer that depends on a business definition, a filter a definition requires, a join that repeats the rows summed, an average over the wrong rows, amounts stored in cents, or a column with a plausible name and another value) and a naive query that falls into it; the dry run and the tests check that each naive query misses the answer by more than the tolerance. answer_questions/definitions/ holds business definitions written for the benchmark, which the harness adds to each project as answer-benchmark/definitions.yml: for the bundled example, terms beside its own definitions.yml; for jaffle-shop, the metrics its MetricFlow YAML declares.
The agent answers every question in four arms, each with a tool that runs one read-only query at a time on the warehouse, through verify’s engine, with 2 GB of memory and stopped after 30 seconds:
Arm |
What the agent is given besides the warehouse’s tables and columns |
|---|---|
|
Nothing |
|
Every |
|
The definitions and the dbt project’s SQL and YAML ( |
|
Konstruct’s MCP tools that answer from the built graph: |
The konstruct arm reaches the dbt SQL, descriptions and lineage through Konstruct as well as the definitions, so project is the arm to compare it with. The gate, diff and history tools need the project’s git history, and mutation_test and why run the models’ SQL, so the arm leaves them out. The graph each project was built with must stay the same through a run, and results.json records the digest of its graph.json; search saves the index it searches by meaning beside it, which the digest does not cover. A project the output directory already holds is built again when the question set or the harness has changed since it was built.
The agent answers through a submit_answer tool, with a number or null to abstain, whether it is sure, and any doubt. The rubric in the question set is deterministic: a number within the tolerance of the question’s unit (a cent, a twentieth of a percentage point, or exact for a count) is correct, any other number is wrong, and a wrong number the agent said it was sure of is confidently wrong. A conversation that ends with no submission (the turn limit, an answer in prose, a refusal) is unanswered, apart from a deliberate null. No model grades an answer.
uv run .kodex/scripts/answer_benchmark.py --plan # the questions; no warehouse, no API
uv run .kodex/scripts/answer_benchmark.py --dry-run # build the projects, check every answer and trap, write each arm's prompt
KONSTRUCT_BENCH_MODEL=<model id> ANTHROPIC_API_KEY=<key> \
uv run --with anthropic .kodex/scripts/answer_benchmark.py --out ~/answer-benchmark
A run calls the Anthropic Messages API with the model KONSTRUCT_BENCH_MODEL names, at the effort --effort sets (default high; none sends no effort setting), with no fallback model. Building the projects needs dbt and, for jaffle-shop, network access; --out must be outside any git repository. --corpus limits it to some projects, --arms to some arms, --only to some questions, --repeats asks each question that many times in each arm, and --max-turns sets the model replies per question (default 20). It writes the built projects, results.jsonl (one line per conversation), the transcripts, summary.md and results.json to --out. results.json holds the command, the model and its settings, the versions of Konstruct and of the question set, the harness digest and the git commit, each project’s graph digest, and each arm’s counts, rates and 95% Wilson intervals (over conversations, as if each were independent), overall, by trap, by project and by question template. A run that stops resumes with the same options and the same harness. A change to a question, a reference, an answer, a trap, the rubric, a definitions file an arm reads or a project’s pins changes the question set’s fingerprint: raise its version and record the fingerprint --plan prints, or the tests fail and a run refuses to start. KONSTRUCT_PAPER_ANSWER_BENCHMARK=1 uv run --with anthropic python paper/generate.py --only answers runs it for the paper’s numbers; without the flag the paper counts the questions only.
Public Project Survey
.kodex/scripts/survey.py builds a list of public dbt projects and reports, in aggregate only, what Konstruct finds in them: models, the share of columns with column lineage, conflicting definitions and their kinds, the size of the ontology, warnings by kind and build time. The list is a text file with one git URL per line, optionally followed by a commit, tag or branch.
uv run .kodex/scripts/survey.py projects.txt --out survey --deps
Each project is cloned and built without its warehouse: the script writes a DuckDB profile under each profile name the project uses, so dbt can produce the manifest. That runs dbt on the project, which renders its Jinja and runs the queries its macros issue, against an empty DuckDB database with file and network access turned off, with a home directory of its own and none of your environment variables but those needed to run and to reach a proxy. --deps runs dbt deps first, which downloads the packages the project names, so package macros render. Run it in a disposable container with no credentials in its environment. Each project is surveyed in a child process, stopped after --timeout seconds (900 by default) and recorded as timed out. The script writes projects.jsonl, one line per project, and summary.md, which names no project; a run that stops resumes where it left off.
Commits
Commits follow Conventional Commits, checked by commitizen and by .kodex/scripts/check_commit_message.py, which applies the Kodex Labs rules: a fixed list of scopes, sentence-case subjects, lines of at most 100 characters, no AI attribution lines, and no business or fundraising terms. On a pull request it also reads the interface snapshots each commit changes in tests/golden/interfaces/: a commit that removes or changes an interface there, or adds a required argument to one, must carry both ! after the type and a BREAKING CHANGE: footer. It also checks the range as a whole, from its merge base to its head, unless a commit in it carries both marks. Changes merge by rebase, so a merge commit is refused rather than checked: “merge commits are not checked; rebase instead”. A renamed snapshot counts as removed and added. As the commit-msg hook it compares the staged snapshots with HEAD, and refuses a merge being made, so a local commit is refused as it would be in CI. The scopes are airflow, dagster, dbt, definitions, iceberg, sql, warehouse, build, graph, html, lineage, query, report, viewer, cli, mcp, skill, examples, readme, tests, ci, config, deps, deps-dev and release.
Repository Layout
Path |
Contents |
|---|---|
|
|
|
|
|
|
|
|
|
Questions over the graph: |
|
|
|
|
|
|
|
The Rust core: the graph model, reading and writing graph.json and putting files on disk whole, graph walks and paths, name resolution, search, topic routing, the formula identity test, the readers of Airflow and Dagster files and |
|
The graph viewer’s source: Svelte 5 and TypeScript, built with Vite |
|
The viewer’s Node toolchain, at the root beside |
|
The GitHub Action |
|
dbt Labs’ jaffle_shop_duckdb (Apache-2.0, see its NOTICE) with an Airflow DAG |
|
The Claude Code plugin, rendered from |
|
|
|
Project scripts: example preparation, test services, commit rules, benchmarks, demo tapes |
|
Atheris fuzz harnesses for the readers of untrusted input (see the test table); pytest does not collect them |
CI/CD
Build and Test runs on every push to main and on pull requests: Ruff and mypy, cargo fmt, Clippy and the Rust tests, the Rust tests again on aarch64, riscv64, ppc64le, s390x and loongarch64 under qemu, a wheel for each platform built with maturin (Linux on all six architectures, macOS and Windows on x86_64 and arm64), a commit message check on pull requests, the test suite on Python 3.11, 3.12, 3.13 and 3.14 against Postgres and Iceberg REST catalog service containers, an end-to-end build of the jaffle_shop example that fails if it takes a minute or more, an install of the built wheel with uv tool install, an install from GitHub exactly as this README describes, the real-world jaffle-shop test, the Airflow parity test, and the Trino connector against a Trino server. The test job also runs the browser tests in Chromium and fails below 90% branch coverage. main is protected, and nothing pushes to it: a release is a tag and a GitHub release. Changes accumulate on a branch and merge in batches, about once an hour, by rebase, so each commit stays on main. The batch’s last commit prepares the release with uv run python .kodex/scripts/prepare_release.py, which reads every releasable commit on the branch since the last tag and sets the next version in pyproject.toml, uv.lock and konstruct/__init__.py, with its section at the top of CHANGELOG.md. While the version is 0.x a release is a patch, or the next minor for a breaking change; the script and the workflow both refuse 1.0. The Release preparation job checks on every pull request that a prepared release still matches main. Create Release runs after Build and Test passes on a push to main: it tags a tested commit whose version has no tag; otherwise, when main has releasable commits since the last tag (a Dependabot fix, say), it writes the next version on a commit above main and tags that. It pushes only tags, after the build succeeds, finishes a release left half done, moves the v0 tag (or re-points it when it lags), and publishes a GitHub release with the changelog section and sdist, a CycloneDX SBOM (uv export --format cyclonedx1.5) and the Sigstore bundle of a build provenance attestation for both (actions/attest-build-provenance, also recorded with GitHub artifact attestations), then builds the wheels from the tag and attaches them with an attestation of their own. Run by hand from the Actions tab, it only finishes or tags a prepared version. Publishing to PyPI is a separate job that stays off until the PUBLISH_TO_PYPI repository variable is true and a pypi environment with trusted publishing exists.
Docs builds the documentation, the demo viewer and the paper on every pull request and push. Its paper job runs paper/generate.py, which builds the example and dbt Labs’ jaffle-shop, verifies them, grades the Ossie portability ladder and runs the benchmark into paper/generated/ (which is not committed), checks the paper with tests/test_paper.py and builds the PDF with TeX Live from the Ubuntu 24.04 runner’s packages; a pull request’s run keeps the PDF and the generated numbers as an artifact. The site job joins the documentation with the PDF, when the paper job succeeded, and a push to main publishes them, with the paper at /paper/konstruct.pdf. A failed paper job stays red and the documentation is published without the paper.
Every action in the workflows and in action.yml is pinned to a full commit SHA, with its version in a comment, and the service containers to image digests, so a run is reproducible and a moved tag cannot change what CI executes. Dependabot moves the action SHAs; the image digests, which carry the tag they track, are updated by hand. OpenSSF Scorecard runs weekly and on every push to main, and publishes its results to the repository’s Security tab.
The zizmor workflow audits the workflows, action.yml, the Dependabot configuration and the pre-commit hooks with zizmor on every pull request (offline) and, with its online audits, on every push to main and weekly, and fails on any finding; a finding that is a deliberate choice carries a # zizmor: ignore[<audit>] comment beside it that says why. Its version is pinned in the workflow and updated by hand.
CodeQL scans the Python package, the viewer’s TypeScript, the workflows and action.yml (and the Rust core once its sources are in the repository) with the security-extended queries, weekly and on every pull request and push to main, and reports to code scanning. It uses an advanced setup, so code scanning’s default setup stays off for the repository.
The Dependency audit workflow checks every lock file in the repository (uv.lock, pnpm-lock.yaml, and a Cargo.lock wherever one is) against the OSV database with osv-scanner, daily and on every pull request and push to main, and fails on a known vulnerability. A vulnerability that does not affect Konstruct is ignored in the osv-scanner.toml beside its lock file, with the reason and a date to look at it again.
graph LR
subgraph "Source Control"
GitHub[GitHub<br/>Repository]
end
subgraph "Build & Test Workflow"
Lint[Ruff and mypy]
Commits[Commit messages]
Tests[Tests<br/>Python 3.11 to 3.14]
E2E[End to end<br/>jaffle_shop]
Package[Wheel install]
end
subgraph "Services"
Postgres[Postgres 18]
Iceberg[Iceberg REST<br/>catalog]
end
subgraph "Release Workflow"
Bump[Tag a prepared version<br/>or release from above main]
Release[GitHub release]
PyPI[PyPI<br/>off by default]
end
GitHub -->|Push/PR| Lint
GitHub -->|PR| Commits
GitHub -->|Push/PR| Tests
GitHub -->|Push/PR| E2E
GitHub -->|Push/PR| Package
Postgres --> Tests
Iceberg --> Tests
E2E -->|All pass on main| Bump
Bump --> Release
Release --> PyPI
style GitHub fill:#24292e,stroke:#333,stroke-width:2px,color:#fff
style Lint fill:#2088ff,stroke:#333,stroke-width:2px,color:#fff
style Commits fill:#2088ff,stroke:#333,stroke-width:2px,color:#fff
style Tests fill:#2088ff,stroke:#333,stroke-width:2px,color:#fff
style E2E fill:#2088ff,stroke:#333,stroke-width:2px,color:#fff
style Package fill:#2088ff,stroke:#333,stroke-width:2px,color:#fff
style Postgres fill:#336791,stroke:#333,stroke-width:2px,color:#fff
style Iceberg fill:#4a5568,stroke:#333,stroke-width:2px,color:#fff
style Bump fill:#6f42c1,stroke:#333,stroke-width:2px,color:#fff
style Release fill:#6f42c1,stroke:#333,stroke-width:2px,color:#fff
style PyPI fill:#f96,stroke:#333,stroke-width:2px