Worked examples
The two example projects under examples/projects/ are complete enough to feel like real codebases: a package.json
with a lockfile, a pyproject.toml with a uv.lock, a .chill-out.yaml config alongside, and a runnable demo script
that exercises the whole check + fix pipeline against them.
Both demos mock the registry so they run offline. The chill-out code path is the real one: configuration loading,
ecosystem detection, transitive attribution, cooldown evaluation, conflict-aware principal rollback, and the file edits
that fix would write are all the production functions, not stand-ins.
How fix decides what to write
For every violation with a known safe version, chill-out tries the simplest thing first: a direct pin in the project's
primary manifest (dependencies for npm, project.dependencies for pypi). Direct pins win because the resolver hoists
them above whatever the principal asks for.
For a transitive violation, the planner first checks whether the installed principal's declared range admits the safe
transitive. If it does, the direct pin is enough. If not, chill-out looks for an older principal whose declared range
does admit the safe transitive, and emits two pins: a rollback of the principal and the direct pin of the transitive.
If no compatible older principal exists the violation lands in FixPlan.unfixable with a structured reason so the CLI
can show actionable guidance instead of silently dropping the violation.
npm: a fresh direct + a fresh transitive
The fixture lives at examples/projects/npm-app/. Its package.json declares chalk, express, lodash, and a dev
dep on typescript. Both the lockfile and the demo's mocked registry agree that:
chalk@5.4.0was published two days ago. It is a direct dep, so chill-out proposes a direct pin to a safe older release.lodash.merge@4.6.3was published two days ago. It is pulled in bylodash, and the installedlodash@4.17.21declares its merge transitive with a range that does not admitlodash.merge@4.6.2. The planner walks back tolodash@4.17.20, whose declared range does admit4.6.2, and emits both the principal rollback and the transitive pin.
Run it from the repo root:
The output:
======================================================================
chill-out check: npm-app
======================================================================
checked 6 package(s)
! direct chalk@5.4.0
release_type=minor age=2d limit=14d safe=5.3.0
! transitive lodash.merge@4.6.3 (via lodash)
release_type=patch age=2d limit=7d safe=4.6.2
planned fix actions:
pin chalk -> 5.3.0
pin lodash -> 4.17.20
pin lodash.merge -> 4.6.2
applying fixes to a temporary copy of the project ...
apply log:
- pinned chalk -> 5.3.0
- pinned lodash -> 4.17.20
- pinned lodash.merge -> 4.6.2
- ran: npm install
resulting package.json:
{
"name": "chill-out-npm-example",
"version": "1.0.0",
...
"dependencies": {
"chalk": "5.3.0",
"express": "^4.19.2",
"lodash": "4.17.20",
"lodash.merge": "4.6.2"
},
"devDependencies": {
"typescript": "^5.4.0"
}
}
Three things to notice in the resulting manifest:
- The fresh direct dep (
chalk) became a hard pin independencies. The caret range was replaced with the exact safe version. - The fresh transitive (
lodash.merge) was promoted to a direct entry independencies. The npm resolver hoists this entry over whatever the principal asks for. - The principal (
lodash) was rolled back from^4.17.21to a hard pin at4.17.20, the most recent version whose declared range admits the safe transitive. This is the conflict-resolution step: without the rollback, the resolver would either fail or silently disagree about whichlodash.mergeto use.
Python: principal rollback through a transitive
The fixture at examples/projects/python-app/ declares fastapi, httpx, rich, and a dev dep on pytest. The
mocked PyPI plays the same trick:
httpx==0.27.0was published two days ago and is a direct dep. It gets pinned to0.26.0.anyio==4.3.0was published two days ago and is pulled in by fastapi. The installedfastapi==0.110.0declaresanyio>=4.3,<5, which excludes the safeanyio==4.2.0. Chill-out rolls fastapi back to0.109.2, whose declaredanyio>=3.7.1,<5admits4.2.0, and pins anyio directly.
Without the rollback, uv lock would refuse to resolve a pinned anyio==4.2.0 against fastapi's declared
anyio>=4.3,<5, and the manifest edit would leave the project in a half-applied state.
Run it:
The output:
======================================================================
chill-out check: python-app
======================================================================
checked 9 package(s)
! transitive anyio==4.3.0 (via fastapi)
release_type=minor age=2d limit=14d safe=4.2.0
! direct httpx==0.27.0
release_type=minor age=2d limit=14d safe=0.26.0
planned fix actions:
pin fastapi -> 0.109.2
pin anyio -> 4.2.0
pin httpx -> 0.26.0
applying fixes to a temporary copy of the project ...
apply log:
- pinned fastapi -> 0.109.2
- added anyio==4.2.0 to project.dependencies
- pinned httpx -> 0.26.0
- ran: uv lock
resulting pyproject.toml:
[project]
name = "chill-out-python-example"
version = "0.1.0"
...
dependencies = [
"fastapi==0.109.2",
"httpx==0.26.0",
"rich==13.7.1",
"anyio==4.2.0",
]
The Python ecosystem doesn't have npm-style overrides, so transitive pins land as direct entries in
project.dependencies. After the edit, uv lock runs and the lockfile reflects the rolled-back fastapi plus the pinned
anyio.
When the planner gives up
If a transitive conflict has no compatible older principal (for example, the principal first declared the offending
range several major versions back), the direct pin would still get attempted but the planner records an
UnfixableViolation. The CLI surfaces these explicitly:
1 violation(s) cannot be auto-fixed:
- leftpad==2.0.0: safe version 1.5.0 conflicts with parent@2.0.0
(declares leftpad>=2.0), and no older parent release that has cleared
its own cooldown declares a range that admits leftpad==1.5.0.
Options: downgrade parent manually, raise the safe target for leftpad,
or wait out the cooldown.
The reason string lists the three concrete actions a maintainer can take. Programmatic callers can iterate
FixPlan.unfixable and surface the same guidance in their own UIs.
How to copy this for your own project
The runnable demos are an honest template for two common automation patterns:
- A pre-merge CI check that runs
chill-out checkand fails the build if any cooldown violation is fresh enough to need attention. - A scheduled job that runs
chill-out fixand opens a PR with the resulting manifest changes. Pair with--fix-style compatibleif you'd rather emit ranges than hard pins so the resolver picks up safe future releases without anotherfixpass.
For real CI, drop the mocks and let chill-out talk to the real registry. The fixture projects exist so you can read a
known-good package.json and pyproject.toml side-by-side without standing up a real npm install or running uv lock
yourself.
Next stops
- Programmatic API for the function-by-function reference behind these scripts
- GitHub Actions for the CI patterns these examples mirror
- CLI for the command-line equivalents shown alongside the Python entry points
- Configuration for the
ChillOutConfigshape used in the custom-config example