cerberus
Verifies repository invariants — CI workflow structure, justfile and dependency conventions, CODEOWNERS, and release-version bumps — as a per-repo linter against a checkout.
Requirements
uvand Python 3.14
The justfile bite shells out to just, which ships with the package (via rust-just) — no separate install. The jscpd and fallow bites run their tools via pnpx at exact versions pinned in tool_pins.py, so every cerberus release measures with the same tools everywhere; pnpm must be on PATH.
Lint a repo
uv run cerberus # lint the current directory
uv run cerberus PATH # lint a checkout at PATH
Runs every bite and exits non-zero on any failure or error, so it drops into CI like any linter. Run cerberus list to see every bite, its scope, and what it verifies.
| Option | Description |
|---|---|
--check NAME |
Limit to named bite(s); repeatable |
--config PATH |
Overlay file applied in place of the repo root cerberus.toml |
--fix |
Auto-fix fixable problems (e.g. trailing whitespace) |
--verbose/-v |
Itemize what each bite measured (clones, dead-code issues) |
A repo switches a bite off with off = true in that bite's cerberus.toml table (see Config below); naming an off bite with --check still runs it.
Bites
| ID | Scope | Verifies |
|---|---|---|
justfile |
content | Canonical baseline block (byte-exact, --fixable), recipe names, aliases, check pipeline, local cerberus run, wrapped tool calls, no trailing whitespace |
ci_workflow_gate |
content | ci.yml exists, exposes a ci check, runs on PRs (push to main recommended) |
ci_check_sequence |
content | ci.yml runs the canonical check sequence per stack |
ci_cerberus_step |
content | A CI workflow runs cerberus to self-verify org invariants |
workflow_toolchain_only |
content | Workflows set up only the workspace toolchain (uv, pnpm), not extra tools |
pyrefly |
content | All code, tests included, type-checks under strict pyrefly with no relaxations |
ruff |
content | ruff runs standalone in preview with select = ["ALL"]; relaxations stay sanctioned |
line_length |
content | ruff line-length and prettier printWidth both match the configured width (120) |
rumdl |
content | .rumdl.toml carries the org-canonical rule config (per-repo exclude allowed) |
knip |
content | knip config is standalone, never inline in package.json; knip.prod.json runs the entry-exports pass and exempts exactly the repo's published npm targets |
vitest |
content | TypeScript tests run on vitest, never bun's runner (package.json, justfile, CI), and the root vitest.config.* coverage.thresholds meet the floor (90%) |
tsc |
content | TypeScript typecheck runs via project references (tsc -b), not a per-package fan-out |
catalog_pinned_deps |
content | Every workspace package.json dependency pins via catalog: or workspace: |
story_tests_lockstep_py |
content | tests/**/stories/*.md criteria have a matching, title-matched pytest test |
story_tests_lockstep_ts |
content | tests/**/stories/*.md criteria have a matching, title-matched vitest test |
cli_ts_test_seam |
content | CLI apps export only the root seam; story tests reach workspace code via fixture aliases |
lib_ts_test_seam |
content | Libraries export only the root seam; story tests reach workspace code via fixture aliases |
fixture_roles_ts |
content | Torn-out TS test suites compose fixtures from role modules: #fixtures targets fixtures/index.ts and only act.ts imports the subject package (its ./contracts seam excepted) |
cli_py_test_seam |
content | CLI apps' story tests import only their root module or cli entry module |
lib_py_test_seam |
content | Libraries' story tests import only their root module |
release_surface_version_bump |
git-history | A published target's version is bumped by exactly one step whenever its release surface changes |
codeowners_coverage |
content | CODEOWNERS present and covers /.github/ |
pytest |
content | pyproject.toml [tool.coverage.report] fail_under meets the floor (90%) |
jscpd |
content | Copy-paste duplication per language stays under the configured jscpd threshold |
fallow |
content | fallow finds no unused code, circular imports, or functions above its complexity thresholds |
zyplux_deps_latest |
content | Every @zyplux/* npm package, zyplux-* PyPI distribution, and ghcr.io/zyplux image is used at its latest release |
tool_pins_latest |
content | The npm tool versions pinned in cerberus source are the latest npm releases (skips repos not carrying the pin source) |
The justfile baseline
Every repo's justfile must start with the line # BASELINE, carry the canonical block from baseline.just byte-for-byte, and close it with a # CUSTOM line. Everything after # CUSTOM is the repo's own (extra aliases, recipes, set/mod statements, variables). With both markers present, --fix restores a drifted baseline region and leaves the custom tail untouched; the zyplux repo's own justfile mirrors the packaged canonical, and cerberus keeps the two identical.
Config
Every default lives in cerberus.toml: shared source ownership under [source], and check settings under their bite's table ([justfile], [pytest], [jscpd], …). The bundled file is the single home of the defaults; missing required keys are errors. A repo adjusts them with a root cerberus.toml, overlaid key by key. An explicit --config PATH stands in for that repository file. Lists replace the corresponding default list.
Every bite table also takes a common off key, handled by the runner: off = true removes the bite from the run entirely — no output line — and an overlay's off = false re-enables a bite the bundled defaults ship off. tool_pins_latest ships off for exactly that reason: only the repo carrying the pin source can act on it, and that repo's overlay switches it on.
Shared source ownership
[source]
production_roots = ["apps/*", "packages/*", "infra"]
test_files = [
"**/tests/**",
"**/__tests__/**",
"**/*.test.*",
"**/*.spec.*",
"**/test_*.py",
"**/*_test.py",
"**/conftest.py",
]
Production roots are repository-relative directory globs; their descendants belong to production, including source assets, build configuration, and deployment infrastructure. Test-file globs take precedence even inside a production root. * matches one path segment and ** spans directories. Files outside both selections are other maintained files, such as development tooling. These conventions apply independently of which bites are enabled.
Knip intersects this ownership with registered JavaScript workspaces; a standalone root package remains production. Pyrefly requires coverage of production and test Python source, including flat infra/deploy.py and deeply nested roots. It reports a production root's src subtree when the file lives there. Both checks consume the same classification; [knip].prod_workspaces and [pyrefly].prod_workspaces must be replaced by [source].production_roots, with conflicting lists reconciled by the repository owner.
Other tools can consume the public API without scanning files or invoking the CLI:
from pathlib import Path
from cerberus import load_source_scope
scope = load_source_scope(Path("/path/to/repository"))
scope.is_production_file("infra/deploy.py") # True with bundled defaults
scope.is_test_file("apps/widget/tests/widget.test.tsx") # True
scope.find_production_root("apps/widget/src/widget.tsx") # "apps/widget"
load_source_scope reads the defaults and repository overlay. The returned SourceScope classifies repository-relative POSIX paths without filesystem access; the caller owns file discovery and any generated-file filtering.
zyplux_deps_latest queries npm, PyPI, and GHCR at lint time; a failed lookup is reported as an error, never a silent pass. It has no --fix — run just upgrade to catch up.
tool_pins_latest guards the jscpd/fallow pins the same way, but runs only in the repo that carries tool_pins.py — the one place a pin can be bumped. Consumer repos never see it (bundled off = true) and pick new pins up with the next cerberus release, which zyplux_deps_latest already forces them onto.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zyplux_cerberus-0.19.0.tar.gz.
File metadata
- Download URL: zyplux_cerberus-0.19.0.tar.gz
- Upload date:
- Size: 63.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ce99d8c59d751e01c302817f0d0852a8ab2bbdd53708e001181fdd8304468a3
|
|
| MD5 |
17d6466b5d26817cb8939085a3176486
|
|
| BLAKE2b-256 |
58636121b66622caa5dce658bfd50f28f3f7d7edd2349faa4089b30e0845f419
|
File details
Details for the file zyplux_cerberus-0.19.0-py3-none-any.whl.
File metadata
- Download URL: zyplux_cerberus-0.19.0-py3-none-any.whl
- Upload date:
- Size: 93.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d31f876757d1157478102b04b4ed689d37fde1da3e536a15f7dceec7ac10f807
|
|
| MD5 |
6a4208ab1ac65698f35f0bb97564038c
|
|
| BLAKE2b-256 |
c5130fa0067a3246aab4bec385c5e55fa1603f090a8d3761b0e71025e99aee30
|