Contributing to mklang¶
Thanks for your interest. mklang is a small, opinionated project: a language spec
plus a reference interpreter. Keep changes coherent with the design in
SPEC.md, the decisions in docs/adr/, and the
operating rules in docs/guides/best-practices.md
(especially layer discipline: language vs host tools vs surfaces).
Dev setup¶
uv run --extra dev --extra mcp pytest -q --cov=mklang # unit + conformance (no network — MockLLM); coverage gate ≥90% (needs --extra mcp: mcp/server.py counts toward the total)
MKLANG_LIVE=1 uv run --extra dev pytest -q tests/test_live.py # opt-in live smoke (active provider; MKLANG_LIVE_PROVIDER=… to override)
uv run --extra dev ruff check src tests scripts
uv run --extra dev ruff format --check src tests scripts # formatting (CI-gated); drop --check to fix
uv run --all-extras mypy # static types (zero suppressions)
uv run --extra dev --with pip-audit pip-audit # dependency CVE scan (CI-gated)
uv run mklang check examples/*.mkl # schema + semantic validation
uv run mklang lint --strict examples/*.mkl # + static analysis
uv run mklang test examples/triage.mkl --script examples/triage.test.yaml # scripted scenarios, no API keys
pytest already runs the conformance suite
(tests/conformance/test_conformance.py over conformance/cases/*.yaml). mklang test is
the same case format for author-facing scenario scripts next to a machine.
Optional pre-commit. .pre-commit-config.yaml
mirrors the ruff gate locally (pre-commit install, then
pre-commit run --all-files). CI remains the source of truth — hooks are a
convenience, not a substitute for the quality workflow.
Secrets live in .env (gitignored); copy .env.example and add provider keys for
live runs. The example runtime defaults to DeepSeek (DEEPSEEK_API_KEY +
active: deepseek). Never commit a key.
Plugin tools / hooks / providers¶
Third-party packages can register callables without patching core:
# in the plugin package's pyproject.toml
[project.entry-points."mklang.tools"]
my_search = "mypkg.tools:search"
[project.entry-points."mklang.hooks"]
amount_ok = "mypkg.hooks:amount_ok"
[project.entry-points."mklang.providers"]
my_vendor = "mypkg.providers:factory" # (ProviderConfig) -> LLM
- Tools:
(dict) -> str(tool-state observations). - Hooks:
(context: dict, output) -> bool(gate predicates). - Providers: factory
(ProviderConfig) -> LLM; unknown names fall back to the OpenAI-compatible adapter.
Entry-point plugins are host code and should be explicitly allowlisted in
production with MKLANG_ALLOWED_PLUGINS=name1,name2. An empty or unset value
keeps the development default of loading discovered plugins; a configured list
blocks every other plugin before it can register tools, hooks, or providers.
Tests for plugin policy and capability metadata belong in tests/ and must not
depend on external credentials.
The CLI loads load_tool_registry() / load_hook_registry() / the provider
registry (builtins + entry points). Library users may still pass explicit
tools= / hooks= to run().
The change checklist¶
A change to the language must land as a coherent set — in this order:
SPEC.md— describe the behavior (the spec is the source of truth).schema/mklang.schema.json— update the structural schema, then re-bundle the package copy:cp schema/mklang.schema.json src/mklang/data/mklang.schema.json(a test asserts the two stay identical).- Interpreter —
src/mklang/(model, loader/validator, engine, adapters, CLI). - Conformance — if the change touches language semantics (SPEC §5–§7), add or
update a case under
conformance/cases/(ADR 0009). - Examples — add/adjust a machine in
examples/that exercises the feature; where gate routing matters, add a sibling*.test.yaml— everyexamples/*.test.yamlruns in CI (tests/repo/test_examples.py). A machine copied into themachines/workspace must stay byte-identical to itsexamples/twin (enforced by the same test). - Tests — deterministic coverage with
MockLLMintests/; keepruffandmypyclean (zero suppressions) and coverage above thefail_under = 90gate. - Docs —
README.md,docs/guides/patterns.md,CHANGELOG.md, andROADMAP.md. A new page underdocs/must also be listed in themkdocs.ymlnav — the published site is built fromdocs/byscripts/build-docs.sh(seemkdocs.ymlfor the layout mapping).
Keep ruff format clean too — the format check is CI-gated (ruff format --check),
not only ruff check.
A change to the interpreter only (no language change) skips steps 1–2 and 4 unless the host tooling surface needs a new conformance-facing scripted binding.
Design decisions (ADRs)¶
Non-trivial or contentious decisions get a short ADR in docs/adr/NNNN-title.md
(Context / Decision / Consequences). See the ADR index
for the format and the existing decisions. Reference the ADR in your PR.
Versioning¶
- Spec version (
mklang:field) changes when the language changes. - Package version (
pyproject.toml, SemVer) changes when the interpreter/tooling changes. Record both inCHANGELOG.md. - The full stability & deprecation policy (SemVer from 1.0.0, spec 0.3 frozen, the deprecation cycle) lives in docs/guides/stability.md (ADR 0026).
Releases¶
Releases are provenance-bound: update pyproject.toml and mklang.__version__
together, record the package release in CHANGELOG.md, and publish a GitHub
Release whose tag is exactly v<package-version>. The release workflow runs the
same reusable quality gate as CI (lint, mypy, coverage, the multi-platform
offline matrix) pinned to the tag, strict docs/package checks, and the required
live-provider gate;
only its previously tested artifacts reach PyPI through the protected pypi
environment and Trusted Publishing. Do not upload a locally rebuilt artifact for
an existing tag.
Tag ↔ CHANGELOG invariant. Every CHANGELOG.md entry from 0.5.3 upward
must carry a matching v<version> git tag; entries at or below 0.5.2 are
pre-distribution history and are exempt (the first PyPI release was 0.5.4). This
is enforced offline by tests/repo/test_release.py
(test_changelog_entries_from_distribution_cutoff_are_tagged) — so a CHANGELOG
entry that was never released fails CI. Either tag it or drop it.
Publish cadence. A git tag is enough for a personal checkpoint; a PyPI publish is not free — it is a durable, irreversible artifact others may depend on. Publish to PyPI on a user-visible change (or a fixed interval, whichever is slower), not on every internal checkpoint. Batch churn between real releases behind local tags.
Sdist as a first-class consumer. The AUR recipe builds and tests from the
published sdist (packaging/arch/PKGBUILD check()). Paths excluded from the
sdist (packaging/, .git, …) are absent there: any offline test that opens
them must skip when missing, and the quality gate re-runs the suite on the
extracted sdist so a packaging-only failure cannot ship. After PyPI publish,
pin source/sha256sums and push the AUR package — see the
Arch packaging README
(not on the docs site; packaging/ is outside docs/).
Non-goals (don't propose these)¶
Pinning a concrete provider/model inside a .mkl — machines route by capability tier
only (ADR 0003). See SPEC.md §9 for the current non-goals.
Maintainer: GitHub repository settings¶
These are not files in the tree; keep them aligned with the quality gate:
- Ruleset on the default branch (not classic branch protection): required
status checks from the quality matrix (
checks+test (…)jobs),non_fast_forward, anddeletionblocked. Prefer repository rulesets over the legacy branch-protection API. Repository admins may bypass (solo maintainer direct-push); PR merges still need the required checks green. - Docs site: the
docsworkflow builds with MkDocs and deploys viaactions/deploy-pages(OIDC /github-pagesenvironment). Pagesbuild_typeis workflow, not the legacygh-pagesbranch publish. - Dependabot: version updates via
.github/dependabot.yml; also keep Dependabot alerts and Dependabot security updates on in the repo Security settings. - Secret scanning and push protection should stay on (public repo default).
- Vulnerability reports go through private
Security Advisories
— see
SECURITY.md.