2. Architecture Constraints

What was fixed before any design decision was made, and therefore shaped all of them.

2.1 Technical constraints

Constraint

Detail

Python, near-stdlib

src/denver.py and src/denver_providers/ depend on nothing beyond the standard library plus PyYAML (dependencies = ["pyyaml>=6"] — the single runtime dependency). No plugin framework, no templating engine, no async runtime.

requires-python = ">=3.9"

Sets the language floor. Newer syntax is only usable behind from __future__ import annotations.

Single-process CLI, no daemon

denver is invoked, resolves config, runs stages, and then exec()s the final command — replacing its own process image. There is no server, no IPC, and no persistent state beyond the filesystem.

Runnable without installation

src/denver.py <env> must work straight from a checkout, alongside the installed denver console script and the frozen single-file binary. All three enter the same main().

Version derived from git tags

setuptools-scm computes the version from git describe; nothing is hand-maintained. scm_version() in src/denver.py reports the same string the docs sidebar shows.

Subprocess-shaped integration with every tool

denver never imports uv, conan, west or docker as a library. Every invocation goes through Context.run(), every lookup through Context.which(). This single seam is what makes both the test suite and --dry-run possible at all.

Config is data, not a language

denver.yml has no conditionals, loops or expressions. The only computation available to an author is ${VAR} interpolation plus the two import: mechanisms. Anything more dynamic belongs in a hook script.

100 % coverage, enforced

poe test runs pytest with --cov-fail-under=100 over denver, denver_errors and denver_providers. Not aspirational: a new branch without a test fails the build, which in practice keeps functions small and side effects funnelled through Context.

Cognitive-complexity gate

complexipy with max-complexity-allowed = 8. A function that grows past it has to be split, which is why the orchestrator is a long list of small _helper() functions rather than a few large ones.

Lint, type and security gates

ruff (lint + format), mypy, pyright, bandit and pip-audit all run in poe all and in CI. Code that cannot satisfy them does not land.

2.2 Organizational constraints

Constraint

Detail

examples/ is content, not framework

Nothing under src/ special-cases the bundled environments. Any directory containing a denver.yml, anywhere, works identically — which is what keeps denver usable outside this repository.

Pre-1.0: breaking changes are made directly

The schema has been renamed repeatedly within its own lifetime (nature:stages:, pipuv, conan.sourcesconan.recipe-dirs, TOML → YAML and back) with no deprecation period, every bundled env updated in the same commit. Tenable only while there is no external consumer base; denver-version: exists so an env can at least state the floor it needs.

Docs live beside the code

doc/ — this chapter included — is expected to change in the same commit as the behavior it describes. There is no separate wiki.

conan_scripts/ is treated as an external tool

src/denver_providers/conan_scripts/ (catalog.py, generate.py) ships alongside the package but is not part of its importable module tree and is driven only via subprocess. It carries its own Conan-API dependency and sits outside the coverage gate — see chapter 11.

2.3 Conventions

Convention

Detail

Kebab-case in config, snake_case in Python

denver.yml keys are kebab-case (no-index, recipe-dirs, skip-if, depends-on); Python identifiers are snake_case. No key transformation happens anywhere — providers read the kebab-case key as written.

Runtime toggles come only from flags

--force, --ci, --fast, -q, -v set fields on Context once, at construction. None of them is ever read back out of a same-named environment variable, so behavior cannot change because of what happens to be exported in the calling shell.

Fail loud on the unexpected

An unknown top-level key, an unknown key in a stage’s section, a stage id that isn’t declared, a provider that isn’t registered — each is a fatal error naming the mistake (and, where possible, the closest match), never a silent no-op.

poe is the task interface

pyproject.toml’s [tool.poe.tasks] is the canonical way to lint, test, build and document. CI runs those same tasks rather than duplicating their logic.