Automated change-type classification for components that lack static verification
Project description
boundver
Automated change-type classification for components that lack static verification.
boundver answers four questions per component — did anything change?, did the behavioral contract change?, did the declared boundary change?, is it still compatible? — using content-addressed fingerprints derived from Git state and declared boundary files. No external dependencies. No build system required.
Why
When a component has consumers but no compiler or type system verifying its interface — services exposing OpenAPI specs, Python libraries, config-driven systems, internal platforms — there's no machine that tells you whether a change is internal, boundary-affecting, or breaking.
boundver fills that gap. It lets you declare what constitutes your component's boundary, then automatically classifies every change into one of four categories:
- Implementation-only — internals changed, boundary stable, consumers unaffected.
- Behavioral contract change — defaults/config/migrations changed, API shape stable, consumers may need to re-verify.
- Boundary change — the declared contract changed, consumers should re-verify.
- Compatibility break — the compatibility family changed, deployment coordination required.
This is the information that CI, consumers, and operators each need — derived deterministically from repo state, not from human discipline or commit-message conventions.
When to use boundver
boundver is for any component whose boundary has consumers but no static verification — no compiler checking signatures, no type system enforcing contracts. That includes most services, most Python/Go libraries, most YAML/JSON-defined APIs, and most internal platforms.
| Tool | Sweet spot | Skip if… |
|---|---|---|
| Nx / Turborepo | JS/TS monorepos with task graphs and caching | You have a polyglot repo or can't adopt a full task runner |
| Bazel / Pants | Large-scale build + dependency graph orchestration | Adoption cost exceeds value for your team size |
| TypeScript / Rust compiler | Statically verified API contracts within a single language | Your entire stack is one statically-typed language |
| boundver | Any language — automated change classification where no static verifier exists | You already have affected-graph + cache-key tooling that satisfies all four questions |
For full tool-selection guidance, see docs/WHY_BOUNDVER.md.
How it works
Each component gets four fingerprints forming a strict containment hierarchy (exact ⊇ behavior ⊇ boundary):
| Fingerprint | Question it answers | What it hashes |
|---|---|---|
exact |
Did anything change? | All tracked files in the component path |
behavior |
Did the behavioral contract change? | Declared contract files: boundary + config + migrations + contract tests |
boundary |
Did the API surface shape change? | Only the declared boundary files (e.g. openapi.yaml, __init__.py) |
compat |
Is it still in the same compatibility family? | Derived from SemVer major version |
This gives you four distinct change classifications:
| What changed | Meaning |
|---|---|
Only exact |
Pure internal refactor — consumers unaffected |
exact + behavior |
Behavioral contract changed (defaults, config, migrations) — API shape stable but consumers may be affected |
exact + behavior + boundary |
API surface changed — consumers must re-verify |
| All four | Breaking change — compatibility family changed |
Components are grouped into slices — named subsets with their own stable fingerprints. Adding an unrelated component changes the full-project hash but leaves existing slice fingerprints untouched.
Note:
boundaryandbehaviorare declared-file fingerprints, not semantic analysis. They detect changes in files you declare as contract-relevant. Theopenapi-canonicalandjson-canonicalproviders go further — they strip non-contract content (descriptions, comments, formatting) so only structural changes trigger the fingerprint.
Each component also reports boundary_status in lock output:
ok: boundary paths were declared and hashed successfullypartial: boundary provider isimplicitand no boundary paths are declared (API fingerprint isnull)error: explicit boundary provider has no paths, or declared paths produced no API digest
Quick start
# Install
pip install boundver
# Create a starter config
boundver init
# Or auto-discover components from common manifests
boundver init --discover
# Custom path / overwrite existing
boundver init --out boundary.config.json --force
# Or create manually (see Config Reference below)
cat > boundary.config.json << 'EOF'
{
"project": "my-project",
"components": {
"auth-service": {
"path": "services/auth",
"version_source": { "file": "package.json", "field": "version" },
"boundary": {
"provider": "openapi",
"paths": ["openapi.yaml"]
},
"behavior": {
"paths": ["openapi.yaml", "config/defaults.json"]
}
}
},
"slices": {
"auth-api": {
"description": "Auth service public API",
"mode": "boundary",
"components": ["auth-service"]
}
}
}
EOF
# Generate the lockfile
boundver generate
# Regenerate only selected components (and affected slices)
boundver generate --components auth-service,billing-service
# Preview generation without writing boundary.lock.json
boundver generate --dry-run
# Check current status
boundver status
# Verify lockfile matches repo state
boundver verify
# Verify only selected components
boundver verify --components auth-service,billing-service
# Verify only components changed since main
boundver verify --changed-from origin/main
# JSON output for automation
boundver verify --format json
# Logging controls
boundver --quiet status
boundver --verbose verify
# Diff two lockfiles
boundver diff old.lock.json boundary.lock.json
# Inspect a specific slice
boundver slice auth-api
# Preview discovered components
boundver discover --format json
Behavior matrix
| Event | exact | behavior | boundary | compat |
|---|---|---|---|---|
| Bug fix (no API change) | ✓ changes | unchanged | unchanged | unchanged |
| Config/default/migration change | ✓ changes | ✓ changes | unchanged | unchanged |
| New API endpoint added | ✓ changes | ✓ changes | ✓ changes | unchanged |
| Breaking change + major bump | ✓ changes | ✓ changes | ✓ changes | ✓ changes |
| Internal refactor | ✓ changes | unchanged | unchanged | unchanged |
| New unrelated component added | slice unchanged | slice unchanged | slice unchanged | n/a |
Config reference
boundary.config.json
Schema file: boundary.config.schema.json (Draft 2020-12).
Config format: boundver accepts
.json,.yaml/.yml, and.tomlconfig files. When no explicit--configis given, it probesboundary.config.json, thenboundary.config.yaml/.yml/.tomlin order.
{
"$schema": "https://raw.githubusercontent.com/yzm1/boundver/main/boundary.config.schema.json",
"project": "my-project",
"defaults": {
"compat_mode": "major"
},
"components": {
"component-name": {
"path": "relative/path/from/repo/root",
"ecosystem": "python | typescript | cloudformation",
"version_source": {
"file": "package.json",
"field": "version"
},
"boundary": {
"provider": "openapi | python-exports | typescript-exports | leaf | implicit",
"paths": ["openapi.yaml"],
"note": "optional explanation"
},
"behavior": {
"paths": ["openapi.yaml", "config/defaults.json"]
},
"vendored_copies": ["path/to/vendored/copy/"]
}
},
"slices": {
"slice-name": {
"description": "Human-readable purpose",
"mode": "exact | behavior | boundary | compat",
"components": ["component-a", "component-b"]
}
}
}
Version source options
// From a JSON/TOML/YAML file field:
"version_source": { "file": "pyproject.toml", "field": "project.version" }
// From git tags:
"version_source": { "git_tag_prefix": "auth-service-v" }
// No version tracking:
"version_source": null
Boundary providers
| Provider | Meaning |
|---|---|
openapi |
OpenAPI/Swagger spec defines the API surface |
python-exports |
__init__.py or __all__ exports define the boundary |
typescript-exports |
.d.ts or index.ts exports define the boundary |
json-file |
Generic JSON boundary artifact defines the contract |
custom.example.service-definition.v1 |
Example custom provider namespace |
leaf |
No downstream consumers — boundary is the component itself |
implicit |
No explicit boundary artifact yet (boundary fingerprint will be null) |
Provider capability matrix
| Provider | Semantic parser? | Requires paths |
Empty paths allowed |
Output |
|---|---|---|---|---|
openapi |
No (raw file digest) | Yes | No | Raw boundary digest |
python-exports |
No (raw file digest) | Yes | No | Raw boundary digest |
typescript-exports |
No (raw file digest) | Yes | No | Raw boundary digest |
json-file |
No (raw file digest) | Yes | No | Raw boundary digest |
leaf |
n/a | No | Yes | No boundary digest required |
implicit |
n/a | No | Yes | boundary_status=partial |
custom.* |
Depends on implementation | Usually | Depends | Raw digest by default |
Built-in providers are currently raw-boundary artifact hashers, not semantic API diff engines.
Near-term implementation focus
boundver remains a public, language-agnostic tool. Near-term work is focused on:
- strict config validation and no silent fingerprint fallback
- explicit source mode behavior (
head,index,working-tree) - portability for external users (no implicit dependency on internal/proprietary boundary artifacts)
Short term deliverables: validate-config, strict digest selection, explicit source modes, and public examples that avoid proprietary dependencies.
CI integration
For lockfile merge conflict handling, see docs/LOCKFILE_MERGE.md.
GitHub Actions — PR verification
For a full set of patterns (conditional builds, cache keys, GitLab, pre-commit), see docs/ci-cookbook.md.
Option A: use bundled composite action
name: Boundary check
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/boundver
with:
config: boundary.config.json
lock: boundary.lock.json
source: head
show-diff-on-failure: "true"
Option B: explicit steps
name: Boundary check
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: boundver verify
- name: Show diff on failure
if: failure()
run: |
boundver generate --out boundary.lock.new.json
boundver diff boundary.lock.json boundary.lock.new.json
Conditional builds using slice fingerprints
# Only rebuild if the API slice actually changed
NEW_FP=$(python -c "
import json
lock = json.load(open('boundary.lock.json'))
print(lock['slices']['my-api']['fingerprint'][:12])
")
if [ "$NEW_FP" != "$CACHED_FP" ]; then
echo "API changed — rebuilding consumers"
# ... trigger downstream builds
fi
Shell verifier (portability proof)
# Verifies exact/boundary fingerprints against HEAD using git + jq + sha256sum
scripts/boundver-verify.sh boundary.config.json boundary.lock.json
Environment variables
| Variable | Effect |
|---|---|
BOUNDVER_ALLOW_CUSTOM_PROVIDERS=1 |
Equivalent to passing --allow-custom-providers on every invocation. Accepts 1, true, or yes. |
Exit codes
boundver verify uses structured exit codes for reliable CI scripting:
| Code | Meaning |
|---|---|
0 |
Lockfile matches current repo state |
1 |
Lockfile is out of date (fingerprint mismatches found) |
2 |
Usage error (unknown component, config missing, etc.) |
validate-config exits 0 on success, 1 on validation errors.
generate exits 0 on success, 1 on config/generation error.
Design decisions
- No external dependencies. Only Git and Python stdlib. Runs anywhere Python 3.8+ and Git are available.
- Deterministic output. Canonical JSON (sorted keys, compact separators) ensures two machines produce identical hashes from identical repo state.
- Canonical exact hashing across source modes.
exactuses one canonical SHA-256 file-content digest model forhead,index, andworking-tree, enabling direct cross-source comparison. - Config/lockfile split. Config is human-maintained (what exists). Lockfile is machine-generated (current state). Mirrors
package.json/package-lock.json. - Language-agnostic boundaries. Instead of parsing ASTs, you declare which files constitute the public boundary. Works with any language or artifact format.
Examples
examples/openapi/examples/json-file/examples/implicit-and-leaf/examples/python-package/examples/typescript-package/
Documentation
- Getting started — install, first config, first lockfile, CI setup
- Gradual adoption guide — incremental adoption from one component to full coverage
- CI cookbook — GitHub Actions, cache keys, GitLab, pre-commit
- Why boundver? — tool comparison and positioning
- Custom vs public providers — when to use
custom.* - Lockfile merge handling — resolving merge conflicts
Validation dependencies
- Runtime dependencies: none (stdlib + git only).
- Optional enhanced schema validation: install
jsonschemafor stricter JSON Schema engine checks invalidate-config. - Optional enhanced YAML extraction: install
PyYAMLfor robust YAML parsing in version extraction.
pip install "boundver[schema]"
pip install "boundver[yaml]"
Without jsonschema, boundver still runs and applies built-in semantic validation checks.
Release
- PyPI publish workflow:
.github/workflows/publish.yml - Trigger: push a version tag matching
v*(for examplev0.3.0)
Source modes
| Mode | File list | Content read from | Default for |
|---|---|---|---|
head |
git ls-tree HEAD |
committed git blobs | generate, verify, status, why |
index |
git ls-files --cached |
staged blobs | — |
working-tree |
git ls-files (tracked) |
disk bytes (CRLF→LF) | explain |
Important: working-tree only sees tracked files
--source=working-tree hashes the on-disk content of files that are already tracked by git.
It does not include untracked files. If you just created a new file but haven't run
git add, that file will not appear in any fingerprint until it is tracked.
This matters most during:
- Initial setup — run
git add .beforeboundver generate --source working-tree. - Adding new boundary files — a new
openapi.yamlwon't affect digests until tracked. - CI with uncommitted generated files — prefer
--source head(the default) in CI.
Ignore behavior
--source=working-tree prefers Git-backed tracked-file enumeration (git ls-files) when available.
In non-git fallback contexts, local file traversal is used.
Symlinks are hashed as link-target text (not dereferenced bytes) for cross-source consistency.
Requirements
- Python 3.8+
- Git
- No pip packages needed
Hash guardrails
To avoid pathological repository scans, hashing enforces built-in guardrails:
- maximum files hashed per digest:
50,000 - maximum size per hashed file:
50 MiB
If exceeded, boundver records explicit digest errors on affected components.
License
MIT
Project details
Release history Release notifications | RSS feed
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 boundver-0.9.1.tar.gz.
File metadata
- Download URL: boundver-0.9.1.tar.gz
- Upload date:
- Size: 120.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46a4299e615753da66ae695402d653a7b837ff25b37f0abb8a215887a9090bd7
|
|
| MD5 |
088dae7ba251a45590914ea755b1bc87
|
|
| BLAKE2b-256 |
766b9ec0e5f82216ec7624693efb84c57ebf3d5a02492ce4604ed2404458b698
|
Provenance
The following attestation bundles were made for boundver-0.9.1.tar.gz:
Publisher:
publish.yml on yzm1/boundver
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boundver-0.9.1.tar.gz -
Subject digest:
46a4299e615753da66ae695402d653a7b837ff25b37f0abb8a215887a9090bd7 - Sigstore transparency entry: 1435178111
- Sigstore integration time:
-
Permalink:
yzm1/boundver@770fc12419295e2c3b14da4a768ab649e6eef69b -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/yzm1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@770fc12419295e2c3b14da4a768ab649e6eef69b -
Trigger Event:
push
-
Statement type:
File details
Details for the file boundver-0.9.1-py3-none-any.whl.
File metadata
- Download URL: boundver-0.9.1-py3-none-any.whl
- Upload date:
- Size: 55.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc974cf9e07261d80feb1548b30d7946801b72e981fdc1889e15cf09f6ac8980
|
|
| MD5 |
6a36f3e451fdd3c10bb54ddcee66727a
|
|
| BLAKE2b-256 |
0284ba93c89eecb9a34a5a2f713d8c18ad00dc72a3bffb6669b1a4c8a3fef627
|
Provenance
The following attestation bundles were made for boundver-0.9.1-py3-none-any.whl:
Publisher:
publish.yml on yzm1/boundver
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boundver-0.9.1-py3-none-any.whl -
Subject digest:
fc974cf9e07261d80feb1548b30d7946801b72e981fdc1889e15cf09f6ac8980 - Sigstore transparency entry: 1435178255
- Sigstore integration time:
-
Permalink:
yzm1/boundver@770fc12419295e2c3b14da4a768ab649e6eef69b -
Branch / Tag:
refs/tags/v0.9.1 - Owner: https://github.com/yzm1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@770fc12419295e2c3b14da4a768ab649e6eef69b -
Trigger Event:
push
-
Statement type: