diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..e571614 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,80 @@ +name: Bug report +description: Report a reproducible problem with boundver. +title: "bug: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for helping improve boundver. Remove secrets and proprietary source before submitting. + - type: input + id: version + attributes: + label: boundver version + description: Paste the output of `boundver --version`. + placeholder: boundver 0.10.0 + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: Include Python, operating system, and Git versions. + placeholder: Python 3.12, Ubuntu 24.04, Git 2.43 + validations: + required: true + - type: textarea + id: description + attributes: + label: What happened? + description: Describe the observed behavior and its impact. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: Provide the smallest repository layout, sanitized config, and commands that reproduce the problem. + placeholder: | + 1. Create ... + 2. Run `boundver ...` + 3. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: output + attributes: + label: Relevant output + description: Paste logs, tracebacks, or JSON output. Remove credentials and sensitive paths. + render: shell + - type: dropdown + id: interface + attributes: + label: Where did this occur? + options: + - CLI + - GitHub Action + - Python API + - Packaging or installation + - Documentation + - Other + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Submission checks + options: + - label: I searched existing issues for this problem. + required: true + - label: I removed secrets and proprietary content from this report. + required: true + - label: This is not a vulnerability; security reports belong in private vulnerability reporting. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a2d00f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Documentation + url: https://github.com/yzm1/boundver#readme + about: Read the overview, quick start, and links to detailed guides. + - name: Private security report + url: https://github.com/yzm1/boundver/security/advisories/new + about: Report suspected vulnerabilities privately; do not open a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..1828733 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Feature request +description: Suggest an improvement to boundver. +title: "feature: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Describe the user problem first. Concrete workflows help evaluate the smallest useful solution. + - type: textarea + id: problem + attributes: + label: Problem or workflow + description: Who encounters this problem, and what are they trying to accomplish? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed outcome + description: Describe the behavior you would like, including a CLI or config example when relevant. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What workarounds or other designs have you tried? + - type: textarea + id: compatibility + attributes: + label: Compatibility considerations + description: Note any effect on lockfiles, config, exit codes, providers, or CI integrations. + - type: checkboxes + id: checks + attributes: + label: Submission checks + options: + - label: I searched existing issues and documentation for this request. + required: true + - label: I can help test or document this change if it is accepted. + required: false diff --git a/.github/actions/boundver/action.yml b/.github/actions/boundver/action.yml deleted file mode 100644 index 6fe8277..0000000 --- a/.github/actions/boundver/action.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: "boundver verify" -description: "Zero-setup boundver verification with optional diff-on-failure" -inputs: - config: - description: "Path to boundary config" - required: false - default: "boundary.config.json" - lock: - description: "Path to lockfile" - required: false - default: "boundary.lock.json" - source: - description: "Fingerprint source mode" - required: false - default: "head" - components: - description: "Optional comma-separated component subset" - required: false - default: "" - python-version: - description: "Python version used by action" - required: false - default: "3.12" - version: - description: "boundver version to install (default: latest from PyPI, or '.' for local)" - required: false - default: "boundver" - show-diff-on-failure: - description: "Generate and print lockfile diff on verify failure" - required: false - default: "true" -runs: - using: "composite" - steps: - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: ${{ inputs.python-version }} - - - name: Install boundver - shell: bash - run: | - python -m pip install --upgrade pip - pip install "${{ inputs.version }}" - - - name: Verify lockfile - shell: bash - run: | - set -euo pipefail - EXTRA_ARGS=() - if [ -n "${{ inputs.components }}" ]; then - EXTRA_ARGS+=(--components "${{ inputs.components }}") - fi - boundver verify \ - --config "${{ inputs.config }}" \ - --lock "${{ inputs.lock }}" \ - --source "${{ inputs.source }}" \ - "${EXTRA_ARGS[@]}" - - - name: Diff on failure - if: ${{ failure() && inputs.show-diff-on-failure == 'true' }} - shell: bash - run: | - set -euo pipefail - boundver generate --config "${{ inputs.config }}" --out /tmp/boundary.lock.new.json --source "${{ inputs.source }}" || true - if [ -f "/tmp/boundary.lock.new.json" ]; then - boundver diff "${{ inputs.lock }}" /tmp/boundary.lock.new.json || true - fi diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..51f1370 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## What changed + +Describe the problem and the resulting behavior. Link related issues with +`Fixes #...` when appropriate. + +## How it was verified + +List the tests or manual checks you ran. + +## Checklist + +- [ ] The change is focused and does not include unrelated generated files. +- [ ] Tests cover new behavior or the reason tests are unnecessary is explained. +- [ ] User-facing CLI, config, schema, exit-code, or lockfile changes are documented. +- [ ] Compatibility and migration impact has been considered. +- [ ] `CHANGELOG.md` is updated for a user-visible change. +- [ ] No secrets, private data, or proprietary fixtures are included. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3539962..09c7041 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Checkout uses: actions/checkout@v6 @@ -82,17 +82,36 @@ jobs: - name: Build package run: | python -m pip install --upgrade pip - pip install build - python -m build - - - name: Install and smoke-test - run: | - pip install dist/*.whl - boundver --help - boundver init --out /tmp/test.config.json --force + bash scripts/packaging_smoke.sh - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ + + action: + name: Public Action contract + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify with the public interface + uses: ./ + with: + facets: boundary,compat + + - name: Exercise hostile input as data + id: hostile + continue-on-error: true + uses: ./ + with: + components: 'boundver; touch /tmp/boundver-action-injected' + + - name: Confirm input was not executed + shell: bash + run: test ! -e /tmp/boundver-action-injected diff --git a/.github/workflows/manual-test.yml b/.github/workflows/manual-test.yml deleted file mode 100644 index 93cd8c7..0000000 --- a/.github/workflows/manual-test.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Test manually - -on: - workflow_dispatch: - -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.8", "3.12"] - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dev deps - run: | - python -m pip install --upgrade pip - pip install -e .[dev] - - - name: Run tests - run: pytest -q - - - name: Build package - run: python -m build - - - name: Install built wheel - run: pip install dist/*.whl - - - name: CLI smoke - run: | - boundver --help - boundver init --out /tmp/boundary.config.json --force diff --git a/.github/workflows/pr-lite.yml b/.github/workflows/pr-lite.yml deleted file mode 100644 index a32cc87..0000000 --- a/.github/workflows/pr-lite.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: PR lite checks - -on: - pull_request: - paths: - - "src/**" - - "tests/**" - - "pyproject.toml" - - "boundary.config.schema.json" - - ".github/workflows/pr-lite.yml" - -jobs: - lite: - runs-on: ubuntu-latest - concurrency: - group: pr-lite-${{ github.ref }} - cancel-in-progress: true - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install - run: | - python -m pip install --upgrade pip - pip install -e .[dev] - - - name: Test - run: pytest -q diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f5e471d..79384b6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,26 +6,110 @@ on: - "v[0-9]+.[0-9]+.[0-9]+" jobs: - publish: + verify-release: runs-on: ubuntu-latest - environment: pypi permissions: - id-token: write contents: read steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v6 with: python-version: "3.12" - - name: Build package + - name: Validate tag and package version + shell: bash + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python - "$RELEASE_TAG" <<'PY' + import pathlib + import sys + import tomllib + + expected = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"] + actual = sys.argv[1] + if actual != f"v{expected}": + raise SystemExit(f"tag {actual!r} does not match package version v{expected}") + PY + + - name: Test and build run: | python -m pip install --upgrade pip - pip install build + python -m pip install -e '.[dev]' twine + pytest -q python -m build + python -m twine check dist/* + python -m zipfile --list dist/*.whl + tar -tzf dist/*.tar.gz + + - name: Upload verified distributions + uses: actions/upload-artifact@v4 + with: + name: release-dist + path: dist/ + + publish: + runs-on: ubuntu-latest + needs: verify-release + environment: pypi + permissions: + id-token: write + contents: read + steps: + - name: Download verified distributions + uses: actions/download-artifact@v4 + with: + name: release-dist + path: dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + release: + runs-on: ubuntu-latest + needs: publish + permissions: + contents: write + steps: + - name: Checkout the released commit + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Download verified distributions + uses: actions/download-artifact@v4 + with: + name: release-dist + path: dist + + - name: Advance the stable v0 tag + shell: bash + env: + RELEASE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + git tag --force v0 "$RELEASE_SHA" + git push origin refs/tags/v0 --force + remote_sha=$(git ls-remote origin refs/tags/v0 | awk '{print $1}') + test "$remote_sha" = "$RELEASE_SHA" + + - name: Create the GitHub Release from verified artifacts + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release upload "$RELEASE_TAG" dist/* --clobber + else + gh release create "$RELEASE_TAG" dist/* \ + --verify-tag \ + --title "boundver ${RELEASE_TAG#v}" \ + --generate-notes + fi diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index e89878a..ebddfa3 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -2,11 +2,12 @@ name: boundver verify language: python entry: boundver - args: [verify, --source, head] + args: [verify, --source, index] pass_filenames: false description: > - Verify that boundary.lock.json is up to date with the current HEAD. - Fails (exit 1) when fingerprints drift; blocks the commit until you run + Verify that boundary.lock.json is up to date with the staged snapshot. + Fails with the documented facet-specific exit code when fingerprints drift; + blocks the commit until you run `boundver generate` and stage the updated lockfile. additional_dependencies: [] always_run: true @@ -16,10 +17,10 @@ name: boundver generate language: python entry: boundver - args: [generate, --source, head] + args: [generate, --source, index] pass_filenames: false description: > - Regenerate boundary.lock.json from HEAD. Use this hook on the + Regenerate boundary.lock.json from the staged snapshot. Use this hook on the pre-commit stage to auto-update the lockfile before committing. additional_dependencies: [] always_run: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 7721dc6..2f88e5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,54 +2,119 @@ All notable changes to this project will be documented in this file. -The format is based on Keep a Changelog. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +No changes yet. + +## [0.10.0] - 2026-08-12 + +### Breaking changes + +- Lockfiles now use `boundary-lock/v2` and a length-delimited, domain-separated + hashing format. This removes ambiguous byte framing in v1. +- **Migration:** v1 fingerprints cannot be converted safely. Regenerate them from + repository content with `boundver generate` after upgrading. The + `migrate-lock` command reports this requirement instead of relabeling old + fingerprints. +- Git-backed source modes enumerate tracked files only. Untracked working-tree + files no longer enter fingerprints implicitly. + ### Added -- Behavior tier: fourth fingerprint (`behavior`) forming containment hierarchy `exact ⊇ behavior ⊇ boundary`; hashes user-declared behavioral-contract files; `behavior` slice mode; advisory warning when `behavior.paths` is not a superset of `boundary.paths`. -- Provider architecture Phases 1–3: `BoundaryProvider` protocol with `resolve()`, `validate_config()`, `explain_diff()`; `JsonCanonicalProvider` (RFC 8785); `OpenApiCanonicalProvider` (strips non-contract content). -- Custom provider loading: `--allow-custom-providers` flag; `custom.*` namespace enforcement; module/class validation. -- `boundver why `: shows which facets drifted, change classification, modified files. -- `boundver discover`: detects npm/pnpm workspaces, pyproject.toml, Cargo.toml, go.mod. -- `boundver init --discover`: auto-generates config from discovered components. -- Shell completions (bash/zsh/fish) via `boundver completions` subcommand. -- Glob patterns in boundary source paths (`*`, `?`, `[`). -- Config format support: `boundary.config.yaml` and `boundary.config.toml` alongside JSON. -- Config includes/extends design (not yet implemented). -- Batch git reads: `git cat-file --batch` for O(1) subprocess count. -- Standalone `.pyz` build via `scripts/build_standalone.py`. -- Docker image for CI without Python. -- GitHub Action (`action.yml`) for marketplace. -- Pre-commit hooks: `boundver-verify` and `boundver-generate`. -- Lockfile migration: `boundver migrate-lock [--dry-run]`. -- JSON schemas for CLI outputs (`spec/cli-output.*.schema.json`). -- `--fail-fast` flag on `verify`. -- Color TTY output with automatic suppression for piped/JSON output. -### Fixed -- Source-purity: all three modes (head/index/working-tree) are fully source-pure for enumeration and content reading. -- Binary blob reads: `_git_cat_blob` and `_git_batch_cat` use bytes mode (no text-mode CRLF conversion). -- Version extraction is source-aware via `_SourceAccessor.version_read_file`. -- Option injection guards on git subprocess calls. -- Size guardrails on file reads (10 MiB hash, 50 MiB blob). -- Path traversal prevention with `_is_within` checks. -- Structured exception hierarchy replacing broad `except Exception`. +- Facet-scoped verification with `verify --facets` and + `defaults.verify_facets`. Drift outside the selected gate is reported as an + observation instead of failing the gate. +- Severity-aware verification exit codes: `1` for exact or metadata drift, `3` + for behavior drift, `4` for boundary drift, and `5` for compatibility drift; + `2` remains reserved for usage or input errors. +- `verify --update` for reviewing drift and refreshing the lockfile in one + command. +- A `behavior` fingerprint and behavior-mode slices for declared behavioral + contracts such as defaults, configuration, and migrations. +- Glob patterns in `boundary.paths` and `behavior.paths`; newly tracked matching + files change the corresponding fingerprint. +- Validated component `consumers` relationships. Boundary and compatibility + drift now identifies declared downstream consumers. +- Git-aware `discover` and `init --discover` support for npm, Python, Cargo, and + Go manifests. Discovery uses tracked files, skips duplicate component + directories, and emits ecosystem-specific version fields. +- Built-in and custom boundary-provider protocols, including JSON canonical and + OpenAPI canonical providers, provider validation, metadata, and diff + explanations. +- `why`, `discover`, shell-completion, `validate-config`, `check-config`, and + `migrate-lock` commands, plus `--fail-fast` verification. +- JSON, YAML, and TOML config loading, with a conditional `tomli` dependency for + Python 3.9-3.10. +- JSON schemas for configuration, v2 lockfiles, and machine-readable CLI output. +- Distribution options for PyPI, a standalone `.pyz`, Docker, pre-commit, and a + hardened composite GitHub Action suitable for Marketplace use. +- Public project metadata and community files for security reports, support, + contributions, issue reports, and pull requests. ### Changed -- Exit code semantics: 1 = drift detected, 2 = usage/input error (config, missing files). -- `generated_at` removed from lockfiles; deterministic output always. -- `--json` flag replaced with `--format json|text`. + +- Lockfile output is deterministic and no longer includes `generated_at`. +- Machine-readable commands use `--format json`; color is limited to interactive + text output. +- Partial component generation reconciles removed components and recomputes all + configured slices, preventing stale aggregate fingerprints. +- Config mutation commands refuse YAML or TOML output instead of silently + rewriting those files as JSON. +- Strict config validation uses the schema bundled in installed wheels, not only + a schema found in a source checkout. +- The GitHub Action now accepts structured inputs, installs the tagged action + source, preserves JSON output, and exposes issues, observations, and the + severity exit code. +- The supported Python floor is now 3.9. Python 3.8 is upstream-EOL and cannot + use the Setuptools version required for modern SPDX package metadata. + +### Fixed + +- Generation now fails when exact, behavior, boundary, or compatibility inputs + cannot be computed; verification no longer accepts matching null digests. +- Invalid `--changed-from` refs fail closed, and config-file changes select all + components for verification. +- Hash framing no longer permits different path/content layouts to produce the + same digest. +- NUL-delimited Git parsing preserves non-ASCII and unusual filenames across + HEAD, index, status, and diff operations. +- Missing, malformed, truncated, oversized, and non-blob Git objects are + reported instead of being hashed as empty content. +- OpenAPI canonicalization removes documentation fields only where they are + annotations; schema properties named `description`, `example`, or `x-*` + remain contract-significant. +- Verification now checks component metadata, digest errors, filtered slices, + removed components, and removed slices as well as fingerprint values. +- Custom providers use an isolated registry per operation and cannot be enabled + by repository configuration alone; callers must opt in explicitly. +- Malformed non-object config and lockfile roots produce usage errors instead of + uncaught attribute errors. +- Version extraction is source-aware, binary and symlink content remains + byte-accurate, and large-file guardrails fail with actionable errors. +- GitHub Action inputs are passed through environment variables and shell arrays + to prevent command injection and JSON/stderr corruption. ## [0.9.1] - 2026-05-03 ### Fixed -- TOML regex fallback: anchor end-of-line to reject invalid TOML on Python 3.8–3.10 (no built-in `tomllib`). -- Symlink hash parity: working-tree accessor now reads `os.readlink()` for symlinks, matching git blob storage. + +- TOML regex fallback: anchor end-of-line to reject invalid TOML on Python + 3.8-3.10 when the built-in `tomllib` is unavailable. +- Symlink hash parity: working-tree access reads `os.readlink()`, matching Git + blob storage. - Python 3.8 type annotation compatibility in test helpers. -- CI: use `source=head` in examples test to avoid CRLF/LF hash mismatch across platforms. +- CI examples use `source=head` to avoid cross-platform checkout conversion + differences. ### Added -- Boundary extraction status model (`ok` / `partial` / `error`) per component in generated lockfiles. -- Basic project governance docs: `LICENSE`, `CONTRIBUTING.md`. + +- Boundary extraction status (`ok`, `partial`, or `error`) for generated + component entries. +- Basic project governance documents: `LICENSE` and `CONTRIBUTING.md`. - Tests for boundary extraction status behavior. + +[Unreleased]: https://github.com/yzm1/boundver/compare/v0.10.0...HEAD +[0.10.0]: https://github.com/yzm1/boundver/releases/tag/v0.10.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..077853b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,37 @@ +# Code of Conduct + +## Our pledge + +We pledge to make participation in boundver welcoming and harassment-free for +everyone, regardless of background, identity, experience, or level of +contribution. + +## Expected behavior + +Examples of behavior that supports a healthy community include: + +- being respectful, patient, and constructive; +- giving and accepting specific technical feedback in good faith; +- acknowledging mistakes and correcting them; +- focusing on what is best for users and the project. + +Unacceptable behavior includes harassment, discriminatory language, personal +attacks, threats, sexualized content, deliberate disruption, or publishing +someone else's private information without permission. + +## Scope and enforcement + +This code applies in project spaces and when representing the project elsewhere. +The maintainer may edit or remove contributions that violate it and may warn, +temporarily restrict, or permanently ban participants when necessary. + +To report a conduct concern, contact the maintainer through the +[maintainer's GitHub profile](https://github.com/yzm1) and request a private +conversation. Do not include sensitive details in a public issue. Reports will +be handled as privately and promptly as practical. For abuse that occurs on +GitHub, you may also use [GitHub's abuse reporting](https://support.github.com/contact/report-abuse). + +## Attribution + +This policy is adapted from the +[Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e6d8bf..f45b212 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in improving boundver. ## Development setup -- Python 3.8+ +- Python 3.9+ - Git Run tests: diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md deleted file mode 100644 index ad54ef7..0000000 --- a/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,170 +0,0 @@ -# Implementation Plan: boundver - -_Updated 2026-05-03 — 771 tests passing, 2 skipped (symlinks on Windows). Near-term, CLI polish, code health, distribution, provider Phases 1–3 complete. Behavior tier implemented. PyPI live._ - ---- - -## Near-term - -### Portability - -- [x] Decouple `service-definition` boundary kind from core validation — dead code removed. -- [x] Fail with actionable guidance when config references unavailable boundary sources — `version_source` validated in `validate_config`; boundary path error now includes `component/file` path and actionable hint; tests added. - -### Testing - -- [x] Per-module unit tests (canonical JSON, sha256, semver, TOML/YAML extraction) — `tests/test_versions.py` (parse_semver, JSON/TOML/YAML extraction, extract_version) and `tests/test_hashing.py` (canonical_json, sha256_hex, source_tree_digest) created; 65 new tests. -- [x] Integration tests using temporary git repos (`git init` + commits in tmp dirs). -- [x] Edge cases: no version source, vendored copy drift, repo with no commits. -- [x] Parser coverage: 3-level TOML paths (`tool.poetry.version`), representative YAML patterns — covered in `tests/test_versions.py`. - -### Publishing - -- [x] Publish to PyPI — tag-triggered workflow (`on: push: tags: v*`); OIDC trusted publishing via `pypa/gh-action-pypi-publish`; `environment: pypi` with `id-token: write`; package live on PyPI as `boundver>=0.9.0`. - ---- - -## CLI polish - -- [x] `--format json|text` for all commands (`--json` replaced with `--format json|text`, `table` deferred). -- [x] Document `--exit-code` behavior for `verify` — exit code table (0/1/2) added to README; `verify` subparser description updated with structured exit code semantics. -- [x] Color output in TTY mode — `_green`, `_red`, `_yellow`, `_bold` helpers; applied to verify ok/fail, validate-config ok/fail, diff +/-/~, status warnings. Suppressed automatically for piped/JSON output. -- [x] Shell completions (bash, zsh, fish) — `completions` subcommand added; static scripts for bash/zsh/fish embedded in `core.py`; works without a git repo; 6 tests added. - ---- - -## Code health - -- [x] Split `core.py` into modules: `_git.py`, `_hashing.py`, `_config.py`, `_lockfile.py`, `_diff.py`, `_output.py`, `_completions.py`. `core.py` is now a 406-line re-export shim + `main()`. All 297 tests pass. -- [x] Batch git reads — `_git_batch_cat` implemented using `git cat-file --batch`; `source_tree_digest`, `boundary_paths_digest`, and `_content_only_digest` all use it for `head`/`index` sources, replacing O(N) subprocesses with O(1). -- [x] Binary blob reads — `_git_cat_blob` uses binary subprocess (no text-mode CRLF conversion); `_read_path_content` for head/index uses it. Working-tree reads normalize CRLF→LF for cross-platform consistency. -- [x] Shared utilities module — `_utils.py`: `SourceMode` enum (`head`/`index`/`working-tree`) inheriting from `str` for transparent string comparison; structured exception hierarchy (`BoundverError` → `ConfigError`, `LockfileError`, `ProviderError`, `GuardrailError`); `_is_glob`, `boundary_provider_name`, `_short` helpers migrated from scattered modules. - ---- - -## Documentation - -- [x] README: badges, comparison table, fixed stale `--json` flag references, docs index section. -- [x] `docs/getting-started.md` — install → first config → first lockfile → CI step. -- [x] `docs/gradual-adoption.md` — staged adoption from implicit provider to full boundary + compat coverage. -- [x] `docs/ci-cookbook.md` — GitHub Actions, GitLab, cache keys, pre-commit, JSON output scripting. -- [~] Docs site (mkdocs-material): rendered site with nav, search, and versioning — **DEFERRED**. - ---- - -## Distribution - -- [x] GitHub Action (`action.yml`) — composite action; `uses: yzm1/boundver@main`; inputs: `command`, `args`, `version`; outputs: `exit-code`, `issues`. -- [x] Standalone single-file download option — `scripts/build_standalone.py` produces `dist/boundver.pyz` (26 KB, no deps). -- [x] Docker image for CI without Python — `Dockerfile` + `.dockerignore`; `docker run --rm -v "$(pwd):/repo" -w /repo boundver verify`. -- [ ] Homebrew formula (stretch). -- [x] Publish GitHub Action to marketplace — `action.yml` has all required fields (`name`, `description`, `author`, `branding: {icon: lock, color: blue}`); file is at repo root. **One manual step remaining:** create a versioned GitHub release and check "Publish this Action to the GitHub Marketplace" in the release UI. No code changes needed. - ---- - -## Future (v0.10.0+) - -- [ ] `boundver watch` — regenerate on save. - - **Design:** Poll/watch the working tree for changes to any file under a declared component path or to the config file itself, then re-run `generate --source working-tree` with debounce. - - - **Dependency:** optional `watchdog>=3` via `pip install boundver[watch]`; fall back to polling (`--poll`, 1 s interval) when unavailable. - - **Watched paths:** all component paths in config + the config file itself. Re-read config on config-file change so new components are picked up without restart. - - **Debounce:** 500 ms quiet period after last event before regenerating (configurable via `--debounce MS`). - - **Interface:** `boundver watch [--config FILE] [--out FILE] [--source working-tree] [--debounce MS] [--poll]` - - **Output:** on each regeneration, print a timestamped one-line summary of which component(s) changed and the resulting classification (`implementation-only` / `behavioral` / `boundary` / `breaking`). Full status on first run. - - **Exit:** clean on `SIGINT`/`SIGTERM`; non-zero if config is invalid at startup. - - **Scope:** generates only; does not verify against a committed lockfile (that is `pre-commit`'s job). - - **Implementation path:** thin wrapper around `generate_lockfile_for_components()` triggered by watchdog `FileSystemEventHandler`; no new core logic needed. - -- [ ] Config includes/extends. - - **Design:** Two distinct mechanisms with separate semantics. - - **`includes`** (additive merge): - ```json - { - "project": "platform", - "includes": ["services/billing/boundary.config.json", "services/auth/boundary.config.json"], - "slices": { "all": { "mode": "boundary", "components": ["billing", "auth"] } } - } - ``` - - Paths are relative to the file containing `includes`. - - Included files' `components` and `slices` are merged into the root config. - - Conflict (same component or slice name) is a hard error at load time. - - Included files may themselves use `includes` (recursive); circular includes are detected and rejected. - - Included files' `defaults` are ignored — root `defaults` win. - - Max depth: 8 levels. - - **`extends`** (inheritance / override): - ```json - { - "extends": "../../base.config.json", - "project": "billing", - "components": { "billing": { "path": ".", "boundary": { "provider": "openapi", "paths": ["openapi.yaml"] } } } - } - ``` - - Inherits `defaults` and `providers` list from base; local values override. - - Does **not** inherit `components` or `slices` — those are always local to the extending file. - - Cannot extend a file that itself uses `extends` (single-level inheritance only; avoids deep chains). - - Primary use case: shared `defaults.compat_mode` and shared `providers` list across a monorepo. - - **Security constraints (both mechanisms):** - - Included/extended paths must resolve within the repository root (`_is_within` check); escaping with `..` is a hard error. - - Paths are resolved relative to the referencing file, not `cwd`. - - **Load-time implementation:** `load_config_file()` resolves includes/extends before returning; `validate_config()` operates on the merged result and sees no `includes`/`extends` keys. No changes to any downstream function signatures. -- [x] Glob patterns in boundary sources — `fnmatch`-based glob support in `PathHashProvider.resolve()` and `_config.validate_config()`; `*`/`?`/`[` patterns expand against component files; `..` rejected; 10 new tests. -- [x] Pre-commit hook integration — `.pre-commit-hooks.yaml` at repo root; `boundver-verify` and `boundver-generate` hooks; `language: python`; `always_run: true`. -- [x] `boundver why ` — compares current fingerprints against the lockfile; shows which facets drifted (exact/behavior/boundary/compat), change-type classification, modified files under component path; exits 0 (up to date) / 1 (drifted) / 2 (error); shell completions updated; 8 new tests. -- [x] Support `boundary.config.yaml` / `.toml` — `find_config_file()` probes alternatives when default `.json` is absent; `load_config_file()` dispatches on extension (JSON built-in, YAML via PyYAML, TOML via `tomllib`/`tomli`); all config-loading sites in `core.py` updated; 11 new tests. **474 tests pass**. - ---- - -## Product-model maturity (post-v1) - -- [x] Real provider architecture: `extract`, `normalize`, `digest`, `validate_config`, `explain_diff` interface — designed in `docs/design/07-provider-architecture.md`. Protocol: `BoundaryProvider` (resolve/validate_config/explain_diff), `ProviderContext`, `ResolvedBoundary`; registry; 3-phase migration plan; security constraints for custom providers. -- [x] **Phase 1 — Protocol + built-in wrappers** (`src/boundver/providers.py`): `ProviderContext`, `ResolvedBoundary`, `BoundaryProvider` protocol; `PathHashProvider`, `ImplicitProvider`, `LeafProvider`, `OpenApiProvider`, `JsonFileProvider`, `PythonExportsProvider`, `TypeScriptExportsProvider`; provider registry; `compute_boundary()` (only SHA-256 call for boundary digests); `generate_lockfile()` now delegates to `compute_boundary()`. Added `tests/test_providers.py` (34 tests). **Zero digest drift — 352 tests pass**. -- [x] **Phase 2 — `options` + custom provider loading**: `boundary.options` added to config schema; `providers` top-level config key validated; `load_custom_providers()` in `providers.py`; `--allow-custom-providers` flag on `generate`/`verify`/`validate-config`/`check-config`/`status`; `BOUNDVER_ALLOW_CUSTOM_PROVIDERS` env var; `custom.*` namespace enforced; `generate_lockfile()` raises if providers declared without flag; registry isolation in tests. **463 tests pass**. -- [x] **Phase 3 — Semantic built-ins**: `JsonCanonicalProvider` (`json-canonical`) re-serialises JSON as RFC 8785 canonical form — stable across key reordering and whitespace. `OpenApiCanonicalProvider` (`openapi-canonical`) strips `info`/`servers`/`tags` top-level blocks and recursively removes `description`, `summary`, `externalDocs`, `example`, `examples`, and `x-*` extension keys — digest stable across docs edits, changes on endpoint/parameter/schema changes. Both registered at import time; `known_providers` updated in `_config.py`. 22 new tests (14 unit + 8 integration). **438 tests pass**. -- [x] **Behavior tier** — fourth fingerprint (`behavior`) forming containment hierarchy `exact ⊇ behavior ⊇ boundary`; hashes user-declared behavioral-contract files (config, migrations, contract tests); `behavior` slice mode; `config_warnings()` emits non-fatal advisory when `behavior.paths` is not a superset of `boundary.paths`; `validate-config` surfaces warnings in yellow without failing; `_diff.py` classifies `exact+behavior` changes as "behavioral contract changed (API shape stable)"; lockfile schema, config schema, spec, and examples all updated; 30+ new tests. **771 tests pass**. -- [ ] Semantic/canonical providers: TS/public API, Python/public symbol. -- [ ] Multi-boundary components (REST/events/CLI/schema per component). -- [ ] Dependency/impact model: component graph, `impact`, `affected`, `why` commands. -- [ ] Richer identity model: clarify exact/boundary/compat/api-version separation. - ---- - -## Governance & contracts (post-v1) - -- [x] Migration policy: `migrate_lockfile()` in `_lockfile.py`; `MigrationError` for unknown schemas; `boundver migrate-lock [--lock FILE] [--dry-run]` CLI subcommand — reads, migrates, writes in-place (strips legacy `generated_at`); dry-run prints without writing; 12 new tests. **486 tests pass**. -- [x] Stable machine contracts: JSON schemas for CLI outputs (verify, status, diff, discover) in `spec/cli-output.*.schema.json`; 6 conformance tests added (`tests/test_cli_output_schemas.py`). -- [ ] Security model: custom provider execution policy, CI allowlists, path escape constraints. -- [ ] Custom provider execution model: config shape (`type: command`/`type: python`), sandboxing, `--allow-custom-providers` flag. -- [x] Resolve `generated_at` in deterministic mode — removed entirely; lockfiles are always deterministic; `git log` provides timestamps. -- [ ] Maintainer sustainability: ownership/triage expectations, bus factor > 1. - ---- - -## Strategic checkpoints - -- Confirm at least one real team using boundver in CI; treat their friction as top priority. -- Validate persona: CI/platform users → prioritize Action/docs over runtime rewrites. -- [x] Dogfood boundver on itself — `boundary.config.json` tracks `src/boundver/` with all 11 source files as boundary paths; `boundary.lock.json` committed. Validated and generates cleanly. -- Re-evaluate quarterly against Nx/Turborepo/Bazel/Pants overlap. - -## Non-goals (for now) - -- No rewrite (Go/Rust) before spec + adoption proven. -- No plugin marketplace before provider interface and demand are clear. -- No over-splitting modules before contracts stabilized. -- No docs-site ceremony before spec + examples + CI path are solid. - ---- - -## Decisions - -- **CI cost control:** automated CI re-enabled at v0.9.0; see `docs/CI_REENABLE_PLAN.md`. -- **Schema validation:** optional `jsonschema` for strict mode; stdlib fallback for zero-dep baseline. -- **Terminology:** `boundary` is the only mode name. No `api` alias exists in schema or runtime. -- **Lockfile determinism:** `generated_at` removed entirely. Lockfiles are always deterministic. Use `git log` for generation timestamps. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cc03a63 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +include CHANGELOG.md +include CODE_OF_CONDUCT.md +include CONTRIBUTING.md +include LICENSE +include README.md +include SECURITY.md +include SUPPORT.md +include boundary.config.schema.json +recursive-include docs *.md +recursive-include examples *.json *.md *.py *.toml *.ts *.yaml +recursive-include spec *.json *.md +recursive-include tests *.py diff --git a/README.md b/README.md index ba61191..08eb941 100644 --- a/README.md +++ b/README.md @@ -1,294 +1,113 @@ -# boundver +# boundver — know whether a change is internal, behavioral, API-facing, or breaking +[![CI](https://github.com/yzm1/boundver/actions/workflows/ci.yml/badge.svg)](https://github.com/yzm1/boundver/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/boundver)](https://pypi.org/project/boundver/) -[![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue)](https://pypi.org/project/boundver/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![No runtime dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml) +[![Python 3.9+](https://img.shields.io/pypi/pyversions/boundver)](https://pypi.org/project/boundver/) +[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/yzm1/boundver/blob/main/LICENSE) -**Automated change-type classification for components that lack static verification.** +**boundver is Git-aware API contract and breaking-change detection for polyglot repositories.** It classifies component drift into exact, behavior, boundary, and compatibility facets, so CI can block consumer-impacting changes without rejecting every internal refactor. -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. +## Try it in one minute -## 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](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:** `boundary` and `behavior` are **declared-file fingerprints**, not semantic analysis. They detect changes in files you declare as contract-relevant. The `openapi-canonical` and `json-canonical` providers 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 successfully -- `partial`: boundary provider is `implicit` and no boundary paths are declared (API fingerprint is `null`) -- `error`: explicit boundary provider has no paths, or declared paths produced no API digest - -## What it detects (and what you do about it) - -When you run `boundver verify` (e.g. in a PR CI check), it compares the current repo state against `boundary.lock.json`. If fingerprints diverge, it exits non-zero and tells you which tier changed: - -| Detection | Meaning | Action | -|---|---|---| -| Only `exact` changed | Internal refactor (e.g. handler logic, comments) | Safe to merge — no consumer impact | -| `behavior` changed | Config/defaults/migrations shifted | Consumers may be affected — review needed but not necessarily breaking | -| `boundary` changed | API surface moved (e.g. new endpoint in OpenAPI spec, new export in `__init__.py`) | Consumers must re-verify compatibility | -| `compat` changed | Major version bumped | Deployment coordination required | - -### Concrete scenarios - -- **Someone edits an API spec without updating the frontend** — PR CI fails `boundver verify`, reviewer sees "auth-service boundary changed" and knows to check frontend compatibility. - -- **Shared library adds/removes a public export** — slice fingerprint changes. Any downstream deploy pipeline keyed on that slice hash knows to rebuild. - -- **Service schema changes** — boundary fingerprint changes. The consuming team knows to verify their integration still works. - -- **Safe internal refactor** — someone rewrites a handler's internals. Only `exact` changes. Slice fingerprints are stable. CI passes. No false alarm. - -### The workflow in practice - -The lockfile commit becomes an explicit acknowledgment: *"yes, I intentionally changed this boundary."* Reviewers see the diff in `boundary.lock.json` and immediately know the blast radius without reading every file. - -**What it does NOT do:** It doesn't block merges automatically or run consumer tests. It's a signal — the enforcement policy (required check, Slack alert, auto-trigger downstream CI) is up to you. - -## Quick start +Run these commands from your repository root: ```bash -# Install -pip install boundver - -# Create a starter config -boundver init -# Or auto-discover components from common manifests +python -m pip install boundver 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 +boundver validate-config +boundver generate --source working-tree +boundver verify --source working-tree --facets boundary,compat +``` -# Verify only components changed since main -boundver verify --changed-from origin/main +Review and commit `boundary.config.json` and `boundary.lock.json`. In CI, verify the committed snapshot with `source: head`. -# JSON output for automation -boundver verify --format json +No useful manifests discovered? `boundver init` creates a minimal scaffold you can edit. -# Logging controls -boundver --quiet status -boundver --verbose verify +## Why teams use it -# Diff two lockfiles -boundver diff old.lock.json boundary.lock.json +A service, package, schema, or config-driven component often has consumers that no compiler can verify. A generic “files changed” check is too noisy; a handwritten list of affected systems is easy to forget. boundver records deterministic fingerprints for the parts that matter and reports the direct consumers of a changed contract. -# Inspect a specific slice -boundver slice auth-api +- Gate only on the risk you care about: `boundver verify --facets boundary,compat`. +- Match contract families with globs such as `*.service-definition.json`; newly added matching files cannot stay invisible. +- Declare `consumers` beside each producer to expose the immediate blast radius. +- Refresh an intentional change with `boundver verify --update` after review. +- Use severity-specific exit codes without parsing console text. +- Keep polyglot monorepos on one small, Git-based contract. -# Preview discovered components -boundver discover --format json -``` +## The four facets -## 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 +| Facet | Question | Input | +|---|---|---| +| `exact` | Did any tracked component content change? | Every tracked file below the component path | +| `behavior` | Did declared observable behavior change? | Boundary files plus config, migrations, contract tests, or other declared paths | +| `boundary` | Did a declared API or contract artifact change? | Provider output for `boundary.paths` | +| `compat` | Did the compatibility family change? | The configured version and compatibility mode | -### `boundary.config.json` +The facets let an internal edit remain visible without making it a merge blocker. For example, a team can record exact drift while requiring only boundary and compatibility stability in CI. -Schema file: `boundary.config.schema.json` (Draft 2020-12). +> boundver detects drift in **declared artifacts**. It is not proof that two implementations are semantically compatible, and it does not replace consumer tests. Canonical providers can remove non-contract noise, but a passing fingerprint check means the declared inputs stayed stable—not that every runtime behavior is equivalent. -> **Config format:** boundver accepts `.json`, `.yaml`/`.yml`, and `.toml` config files. -> When no explicit `--config` is given, it probes `boundary.config.json`, then -> `boundary.config.yaml` / `.yml` / `.toml` in order. +## A practical configuration ```json { "$schema": "https://raw.githubusercontent.com/yzm1/boundver/main/boundary.config.schema.json", - "project": "my-project", + "project": "payments-platform", "defaults": { - "compat_mode": "major" + "compat_mode": "major", + "verify_facets": ["boundary", "compat"] }, "components": { - "component-name": { - "path": "relative/path/from/repo/root", - "ecosystem": "python | typescript | cloudformation", - "version_source": { - "file": "package.json", - "field": "version" - }, + "payment-api": { + "path": "services/payment", + "version_source": {"file": "package.json", "field": "version"}, "boundary": { - "provider": "openapi | python-exports | typescript-exports | leaf | implicit", - "paths": ["openapi.yaml"], - "note": "optional explanation" + "provider": "openapi", + "paths": ["openapi/*.yaml", "*.service-definition.json"] }, "behavior": { - "paths": ["openapi.yaml", "config/defaults.json"] + "paths": ["openapi/*.yaml", "*.service-definition.json", "config/*.json"] }, - "vendored_copies": ["path/to/vendored/copy/"] + "consumers": ["admin-portal", "checkout-web"] } }, "slices": { - "slice-name": { - "description": "Human-readable purpose", - "mode": "exact | behavior | boundary | compat", - "components": ["component-a", "component-b"] + "checkout-contracts": { + "description": "Contracts required by checkout", + "mode": "boundary", + "components": ["payment-api"] } } } ``` -### Version source options - -```json -// 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. - +Paths are relative to the component. `*`, `?`, character classes, and recursive `**` patterns are supported by path-hashing providers. A glob that matches nothing is an error, making accidental omissions visible. Canonical JSON/OpenAPI providers currently require explicit files. -## Near-term implementation focus +`behavior.paths` normally includes every boundary path plus runtime-relevant configuration. `consumers` names direct downstream systems; boundary and compatibility failures report them so reviewers know whom to re-verify. -boundver remains a public, language-agnostic tool. Near-term work is focused on: +## Choose the CI policy -- 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) +The command line overrides `defaults.verify_facets`: -Short term deliverables: `validate-config`, strict digest selection, explicit source modes, and public examples that avoid proprietary dependencies. - -## CI integration +```bash +# Recommended starting gate: internal refactors do not fail CI +boundver verify --facets boundary,compat -For lockfile merge conflict handling, see [docs/LOCKFILE_MERGE.md](docs/LOCKFILE_MERGE.md). +# Stricter contract gate +boundver verify --facets behavior,boundary,compat -### GitHub Actions — PR verification +# Audit every recorded facet +boundver verify --facets exact,behavior,boundary,compat +``` -For a full set of patterns (conditional builds, cache keys, GitLab, pre-commit), see [docs/ci-cookbook.md](docs/ci-cookbook.md). +Drift outside the selected gate is reported as a non-gating observation. Config, lockfile structure, digest errors, and metadata integrity remain safety checks. -#### Option A: use bundled composite action +## GitHub Actions ```yaml -name: Boundary check +name: Contract boundary on: [pull_request] + jobs: verify: runs-on: ubuntu-latest @@ -296,163 +115,99 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: ./.github/actions/boundver + - uses: yzm1/boundver@v0 with: config: boundary.config.json lock: boundary.lock.json source: head - show-diff-on-failure: "true" + facets: boundary,compat ``` -#### Option B: explicit steps - -```yaml -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 -``` +The Action installs the version bundled with its release and exposes issues, observations, and the exit code. See the [CI cookbook](https://github.com/yzm1/boundver/blob/main/docs/ci-cookbook.md) for changed-component checks, GitLab, pre-commit, and cache recipes. -### Conditional builds using slice fingerprints +## Review and accept intentional drift ```bash -# 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) +# See the affected facet and direct consumers +boundver verify --source working-tree --facets boundary,compat +boundver why payment-api --source working-tree -```bash -# Verifies exact/boundary fingerprints against HEAD using git + jq + sha256sum -scripts/boundver-verify.sh boundary.config.json boundary.lock.json +# After review, regenerate in the same source mode +boundver verify --source working-tree --facets boundary,compat --update +git diff -- 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 +Use the same source mode for generation and verification. If you want to refresh non-gating exact or behavior drift too, include those facets in the update command. -`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.) | +## Source modes -`validate-config` exits `0` on success, `1` on validation errors. -`generate` exits `0` on success, `1` on config/generation error. +| Mode | Snapshot | Typical use | +|---|---|---| +| `head` | Files committed at `HEAD` | CI and clean local checkouts | +| `index` | Files staged in Git | Pre-commit workflows | +| `working-tree` | On-disk content of tracked files | Reviewing local edits | -## Design decisions +Untracked files are intentionally excluded after the repository has its first commit. In an unborn repository, `working-tree` uses a bounded filesystem fallback so initial setup can succeed; review and stage those files before committing the lock. Stage a new contract file before using `index`, or `git add` it before relying on tracked working-tree discovery. -- **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.** `exact` uses one canonical SHA-256 file-content digest model for `head`, `index`, and `working-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. +## Providers -## Examples +| Provider | Use it for | +|---|---| +| `openapi` | Raw OpenAPI or Swagger artifacts | +| `openapi-canonical` | OpenAPI structure with documentation noise removed | +| `json-file` | Raw JSON contracts | +| `json-canonical` | Formatting-insensitive JSON contracts | +| `python-exports` | Python export files such as `__init__.py` | +| `typescript-exports` | TypeScript declarations or export barrels | +| `leaf` | A component with no downstream contract | +| `implicit` | Exact tracking before a boundary is declared | -- `examples/openapi/` -- `examples/json-file/` -- `examples/implicit-and-leaf/` -- `examples/python-package/` -- `examples/typescript-package/` +Custom providers are supported only with an explicit trusted-code opt-in. See [public and custom providers](https://github.com/yzm1/boundver/blob/main/docs/public-vs-custom-providers.md). -## Documentation +## Exit codes -- [Getting started](docs/getting-started.md) — install, first config, first lockfile, CI setup -- [Gradual adoption guide](docs/gradual-adoption.md) — incremental adoption from one component to full coverage -- [CI cookbook](docs/ci-cookbook.md) — GitHub Actions, cache keys, GitLab, pre-commit -- [Why boundver?](docs/WHY_BOUNDVER.md) — tool comparison and positioning -- [Custom vs public providers](docs/public-vs-custom-providers.md) — when to use `custom.*` -- [Lockfile merge handling](docs/LOCKFILE_MERGE.md) — resolving merge conflicts +| Code | Highest selected failure | +|---:|---| +| `0` | Clean; selected facets match | +| `1` | Exact or metadata drift | +| `2` | Usage or configuration error | +| `3` | Behavior drift | +| `4` | Boundary drift | +| `5` | Compatibility drift | -## Validation dependencies +When several selected facets drift, the highest-severity code wins. -- **Runtime dependencies:** none (stdlib + git only). -- **Optional enhanced schema validation:** install `jsonschema` for stricter JSON Schema engine checks in `validate-config`. -- **Optional enhanced YAML extraction:** install `PyYAML` for robust YAML parsing in version extraction. +## Useful commands ```bash -pip install "boundver[schema]" -pip install "boundver[yaml]" +boundver discover # preview Git-tracked manifest discovery +boundver status # summarize a lockfile +boundver verify --changed-from main # show changed components; verify the full lock +boundver verify --update # accept reviewed drift in one step +boundver diff old.lock.json boundary.lock.json +boundver slice checkout-contracts +boundver completions --shell bash ``` -Without `jsonschema`, boundver still runs and applies built-in semantic validation checks. - -## Release +## Installation and requirements -- PyPI publish workflow: `.github/workflows/publish.yml` -- Trigger: push a version tag matching `v*` (for example `v0.3.0`) +boundver supports Python 3.9+ and Git. It has no third-party dependency on Python 3.11+; Python 3.9–3.10 install `tomli` for TOML support. -## 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 .` before `boundver generate --source working-tree`. -- **Adding new boundary files** — a new `openapi.yaml` won'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: +```bash +python -m pip install boundver +python -m pip install "boundver[schema,yaml]" # optional validation/YAML support +``` -- maximum files hashed per digest: `50,000` -- maximum size per hashed file: `50 MiB` +## Learn more -If exceeded, boundver records explicit digest errors on affected components. +- [Getting started](https://github.com/yzm1/boundver/blob/main/docs/getting-started.md) +- [Examples](https://github.com/yzm1/boundver/blob/main/examples/README.md) +- [CI cookbook](https://github.com/yzm1/boundver/blob/main/docs/ci-cookbook.md) +- [Gradual adoption](https://github.com/yzm1/boundver/blob/main/docs/gradual-adoption.md) +- [Why boundver?](https://github.com/yzm1/boundver/blob/main/docs/WHY_BOUNDVER.md) +- [Lockfile merge strategy](https://github.com/yzm1/boundver/blob/main/docs/LOCKFILE_MERGE.md) +- [Changelog](https://github.com/yzm1/boundver/blob/main/CHANGELOG.md) -## License +For questions and ideas, see [support](https://github.com/yzm1/boundver/blob/main/SUPPORT.md); bugs belong in [GitHub Issues](https://github.com/yzm1/boundver/issues). Contributions are welcome—start with the [contributing guide](https://github.com/yzm1/boundver/blob/main/CONTRIBUTING.md) and [security policy](https://github.com/yzm1/boundver/blob/main/SECURITY.md). -MIT +MIT licensed. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b48f6bc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Supported versions + +Security fixes are provided for the latest release and the 0.10 release line. + +| Version | Supported | +| --- | --- | +| Latest release | Yes | +| 0.10.x | Yes | +| Earlier versions | No | + +## Reporting a vulnerability + +Please report suspected vulnerabilities through GitHub's +[private vulnerability reporting](https://github.com/yzm1/boundver/security/advisories/new). +Do not disclose security-sensitive details in a public issue, discussion, or +pull request. + +Include, when possible: + +- the affected boundver version and environment; +- a minimal reproduction or proof of concept; +- the impact and any known prerequisites; +- suggested mitigations, if you have them. + +The maintainer will acknowledge the report as soon as practical, investigate it, +and coordinate disclosure and a fix with the reporter. Please allow a reasonable +amount of time for remediation before publishing details. + +## Scope + +Reports about boundver's source, packaged CLI, GitHub Action, and release +artifacts are in scope. Vulnerabilities in third-party services should be +reported to the service owner unless boundver's integration is the cause. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..57e701a --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,22 @@ +# Support + +boundver is maintained as an open source project on a best-effort basis. + +Before requesting help: + +1. Check the [README](README.md) and the guides in [`docs/`](docs/). +2. Search [existing issues](https://github.com/yzm1/boundver/issues) for the same + error or use case. +3. Retry with the latest supported release when practical. + +For reproducible bugs, open a +[bug report](https://github.com/yzm1/boundver/issues/new/choose) and include a +minimal repository layout, sanitized config, command, output, boundver version, +Python version, operating system, and Git version. For product ideas, use the +feature request template. A blank issue is available for focused usage +questions that do not fit either template. + +Please do not use public issues for vulnerabilities. Follow +[`SECURITY.md`](SECURITY.md) to report them privately. Questions about third-party +services or unsupported versions may need to be handled by those providers or by +the wider community. diff --git a/action.yml b/action.yml index a6f72af..a6242b9 100644 --- a/action.yml +++ b/action.yml @@ -1,76 +1,153 @@ name: boundver -description: > - Detect whether component changes are internal, behavioral, boundary-affecting, - or compatibility-breaking. +description: Detect API boundary, behavioral contract, and component changes in CI. author: yzm1 branding: - icon: lock + icon: shield color: blue inputs: - command: - description: > - boundver command to run. One of: verify, generate, validate-config, - status, diff, discover. Default is 'verify'. + config: + description: Path to boundary.config.json. required: false - default: verify - - args: - description: > - Additional arguments passed directly to the boundver command. - Example: "--source head --components svc,worker" + default: boundary.config.json + lock: + description: Path to boundary.lock.json. + required: false + default: boundary.lock.json + source: + description: Snapshot to verify (head, index, or working-tree). + required: false + default: head + facets: + description: Comma-separated facets that fail the gate. + required: false + default: boundary,compat + components: + description: Optional comma-separated component subset. required: false default: "" - - version: - description: > - pip install specifier for boundver. Defaults to installing from the - main branch on GitHub. Once published on PyPI, set to e.g. "boundver==0.9.0". + changed-from: + description: Optional Git ref used to report changed components while verifying the full lock. + required: false + default: "" + fail-fast: + description: Report only the highest-severity gated issue. + required: false + default: "false" + update: + description: Refresh the lockfile after reporting drift. + required: false + default: "false" + python-version: + description: Python version used by the action. required: false - default: "boundver>=0.9.0" + default: "3.12" outputs: exit-code: - description: Exit code from the boundver command (0=ok, 1=drift, 2=usage error). - value: ${{ steps.run.outputs.exit-code }} - + description: "0 clean, 1 exact/metadata, 2 usage, 3 behavior, 4 boundary, 5 compatibility." + value: ${{ steps.verify.outputs.exit-code }} issues: - description: > - Newline-separated list of issues found (verify command only). - Empty when the lockfile is up to date. - value: ${{ steps.run.outputs.issues }} + description: Newline-separated gated issues. + value: ${{ steps.verify.outputs.issues }} + observations: + description: Newline-separated drift outside the selected gate. + value: ${{ steps.verify.outputs.observations }} runs: using: composite steps: - - name: Install boundver + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + + - name: Install this boundver release shell: bash - run: pip install --quiet ${{ inputs.version }} + env: + BOUNDVER_ACTION_PATH: ${{ github.action_path }} + run: python -m pip install --disable-pip-version-check --quiet "$BOUNDVER_ACTION_PATH" - - name: Run boundver - id: run + - name: Verify declared facets + id: verify shell: bash + env: + BOUNDVER_CONFIG: ${{ inputs.config }} + BOUNDVER_LOCK: ${{ inputs.lock }} + BOUNDVER_SOURCE: ${{ inputs.source }} + BOUNDVER_FACETS: ${{ inputs.facets }} + BOUNDVER_COMPONENTS: ${{ inputs.components }} + BOUNDVER_CHANGED_FROM: ${{ inputs.changed-from }} + BOUNDVER_FAIL_FAST: ${{ inputs.fail-fast }} + BOUNDVER_UPDATE: ${{ inputs.update }} run: | + set -euo pipefail + + case "$BOUNDVER_SOURCE" in + head|index|working-tree) ;; + *) echo "Invalid source: $BOUNDVER_SOURCE" >&2; exit 2 ;; + esac + case "$BOUNDVER_FAIL_FAST" in + true|false) ;; + *) echo "fail-fast must be true or false" >&2; exit 2 ;; + esac + case "$BOUNDVER_UPDATE" in + true|false) ;; + *) echo "update must be true or false" >&2; exit 2 ;; + esac + + command=( + boundver verify + --config "$BOUNDVER_CONFIG" + --lock "$BOUNDVER_LOCK" + --source "$BOUNDVER_SOURCE" + --facets "$BOUNDVER_FACETS" + --format json + ) + if [[ -n "$BOUNDVER_COMPONENTS" ]]; then + command+=(--components "$BOUNDVER_COMPONENTS") + fi + if [[ -n "$BOUNDVER_CHANGED_FROM" ]]; then + command+=(--changed-from "$BOUNDVER_CHANGED_FROM") + fi + if [[ "$BOUNDVER_FAIL_FAST" == true ]]; then + command+=(--fail-fast) + fi + if [[ "$BOUNDVER_UPDATE" == true ]]; then + command+=(--update) + fi + + result_file="${RUNNER_TEMP:-/tmp}/boundver-result.json" set +e - output=$(boundver ${{ inputs.command }} ${{ inputs.args }} --format json 2>&1) + "${command[@]}" >"$result_file" code=$? + set -e echo "exit-code=$code" >> "$GITHUB_OUTPUT" - # Extract issues array from JSON output when available - issues=$(echo "$output" | python3 -c " - import json, sys + + for field in issues observations; do + delimiter="BOUNDVER_${field}_${RANDOM}_${RANDOM}" + { + echo "$field<<$delimiter" + python - "$result_file" "$field" <<'PY' + import json + import pathlib + import sys + + path = pathlib.Path(sys.argv[1]) + field = sys.argv[2] try: - data = json.load(sys.stdin) - for issue in data.get('issues', []): - print(issue) - except Exception: - pass - " 2>/dev/null || true) - { - echo "issues<> "$GITHUB_OUTPUT" - # Re-run in text mode for human-readable CI log output - boundver ${{ inputs.command }} ${{ inputs.args }} || true - exit $code + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + payload = {} + for item in payload.get(field, []): + print(item) + PY + echo "$delimiter" + } >> "$GITHUB_OUTPUT" + done + + if [[ -s "$result_file" ]]; then + python -m json.tool "$result_file" + fi + exit "$code" diff --git a/boundary.config.json b/boundary.config.json index 85e5b61..60469fd 100644 --- a/boundary.config.json +++ b/boundary.config.json @@ -2,14 +2,16 @@ "$schema": "https://raw.githubusercontent.com/yzm1/boundver/main/boundary.config.schema.json", "project": "boundver", "defaults": { - "compat_mode": "major" + "compat_mode": "major", + "verify_facets": [ + "boundary", + "compat" + ] }, "components": { "boundver": { "path": "src/boundver", - "version_source": { - "git_tag_prefix": "v" - }, + "version_source": null, "boundary": { "provider": "python-exports", "paths": [ @@ -20,6 +22,9 @@ }, "spec": { "path": "spec", + "consumers": [ + "boundver" + ], "boundary": { "provider": "json-file", "paths": [ diff --git a/boundary.config.schema.json b/boundary.config.schema.json index d8f52f8..a9b50b2 100644 --- a/boundary.config.schema.json +++ b/boundary.config.schema.json @@ -5,84 +5,120 @@ "type": "object", "required": ["project", "components"], "properties": { - "project": { "type": "string", "minLength": 1 }, + "$schema": {"type": "string"}, + "project": {"type": "string", "minLength": 1}, "providers": { "type": "array", - "description": "Custom provider registrations. Requires allow_custom_providers: true or --allow-custom-providers at runtime.", + "description": "Custom providers. Loading requires an explicit --allow-custom-providers flag or trusted environment opt-in.", "items": { "type": "object", "required": ["module", "class"], "properties": { - "module": { "type": "string", "minLength": 1 }, - "class": { "type": "string", "minLength": 1 } + "module": {"type": "string", "minLength": 1}, + "class": {"type": "string", "minLength": 1}, + "name": {"type": "string", "pattern": "^custom\\."} }, "additionalProperties": false } }, - "allow_custom_providers": { - "type": "boolean", - "description": "When true, custom providers declared in 'providers' are loaded without requiring the --allow-custom-providers CLI flag.", - "default": false - }, "defaults": { "type": "object", "properties": { "compat_mode": { "type": "string", "enum": ["major", "semver_major", "semver_major_minor"] + }, + "verify_facets": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": ["exact", "behavior", "boundary", "compat"] + } } }, - "additionalProperties": true + "additionalProperties": false }, "components": { "type": "object", + "minProperties": 1, "additionalProperties": { "type": "object", "required": ["path", "boundary"], "properties": { - "path": { "type": "string", "minLength": 1 }, - "ecosystem": { "type": "string" }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!\\.?/?$)(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" + }, + "ecosystem": {"type": "string"}, "version_source": { "oneOf": [ {"type": "null"}, - {"type": "object"} + { + "type": "object", + "required": ["file", "field"], + "properties": { + "file": {"type": "string", "minLength": 1}, + "field": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["git_tag_prefix"], + "properties": { + "git_tag_prefix": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } ] }, "boundary": { "type": "object", "required": ["provider"], "properties": { - "provider": { "type": "string", "minLength": 1 }, + "provider": {"type": "string", "minLength": 1}, "paths": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*\\\\).+$"} }, "options": { "type": "object", - "description": "Provider-specific options passed to the provider via ProviderContext.", + "description": "Provider-specific options passed through ProviderContext.", "additionalProperties": true }, - "note": { "type": "string" } + "note": {"type": "string"} }, - "additionalProperties": true + "additionalProperties": false }, "behavior": { "type": "object", - "description": "Behavioral contract paths — a superset of boundary that includes config, migrations, contract tests, etc.", + "description": "Behavior-relevant paths, normally a superset of boundary paths.", "properties": { "paths": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*\\\\).+$"} } }, "additionalProperties": false }, "vendored_copies": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" + } + }, + "consumers": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true } }, - "additionalProperties": true + "additionalProperties": false } }, "slices": { @@ -91,16 +127,16 @@ "type": "object", "required": ["components"], "properties": { - "description": { "type": "string" }, - "mode": { "type": "string", "enum": ["exact", "behavior", "boundary", "compat"] }, + "description": {"type": "string"}, + "mode": {"type": "string", "enum": ["exact", "behavior", "boundary", "compat"]}, "components": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} } }, - "additionalProperties": true + "additionalProperties": false } } }, - "additionalProperties": true + "additionalProperties": false } diff --git a/boundary.lock.json b/boundary.lock.json index 78f65c2..8c079aa 100644 --- a/boundary.lock.json +++ b/boundary.lock.json @@ -1,17 +1,19 @@ { "$schema": "https://raw.githubusercontent.com/yzm1/boundver/main/spec/boundary.lock.schema.json", - "schema": "boundary-lock/v1", + "schema": "boundary-lock/v2", "project": "boundver", "components": { "boundver": { "version": null, "path": "src/boundver", "boundary_provider": "python-exports", + "boundary_provider_version": "1", "boundary_status": "ok", + "consumers": [], "fingerprints": { - "exact": "28ec2585699ec4823aff8568615dcdeaaab634a675c3cf318c7327a01ef4a9ef", + "exact": "fa117a8c7cdb96eb0147fcaef4bec9cfed038c5b63ba676ec05dd6ae2f4f28c0", "behavior": null, - "boundary": "9ffd37eb7eafd3db1a30c562d3e63bc4efec8f478bb17a0f2a8f3e1c51db2862", + "boundary": "3cec60f18f8753487958a060f9b166a20ebd5b4a79e5cb8228bd232e76177343", "compat": null }, "semver": { @@ -24,11 +26,15 @@ "version": null, "path": "spec", "boundary_provider": "json-file", + "boundary_provider_version": "1", "boundary_status": "ok", + "consumers": [ + "boundver" + ], "fingerprints": { - "exact": "b573703461d2fad8de397bd085302935974ff3c3ef9087d31353e410f59537c8", + "exact": "1d5782f2f165d797d1c380ae0eb25f926182b01433f9877845d05c6739a64948", "behavior": null, - "boundary": "23b615d0a98a22b3a6c2d4f625b004dd02ae80ff81b35fd675250e3c3e4996ae", + "boundary": "cc3665b68205d96779234f047fa195a5988a26e7ae1bcf645c2a3d80509a13c0", "compat": null }, "semver": { @@ -45,9 +51,9 @@ "components": [ "boundver" ], - "fingerprint": "85929c659c9ce419aaef218933d8c5eed42f382404dd1327bf4f95f4e57ce783", + "fingerprint": "65f50aad6677a19bc1b0550ac38eba682910be3d1da06936cbf1321c4268ee3b", "component_digests": { - "boundver": "9ffd37eb7eafd3db1a30c562d3e63bc4efec8f478bb17a0f2a8f3e1c51db2862" + "boundver": "3cec60f18f8753487958a060f9b166a20ebd5b4a79e5cb8228bd232e76177343" } }, "spec": { @@ -56,9 +62,9 @@ "components": [ "spec" ], - "fingerprint": "7297a30943da98ffa81c71454ee2098cd491c90d0032bd1d889212f2b77b948d", + "fingerprint": "dbfdc0d193b641a99471b577e357a57d47332f2efccfd20826a9d46af8d7ed72", "component_digests": { - "spec": "b573703461d2fad8de397bd085302935974ff3c3ef9087d31353e410f59537c8" + "spec": "1d5782f2f165d797d1c380ae0eb25f926182b01433f9877845d05c6739a64948" } }, "all": { @@ -68,10 +74,10 @@ "boundver", "spec" ], - "fingerprint": "3bc32c4007f42c248a18bda13fc84e7ffd2a69872555a261cbf6f1c765826b74", + "fingerprint": "8634efcdfc2fe16f63d764808a84c30352c622210aad0e8adea5dbc945fd1561", "component_digests": { - "boundver": "28ec2585699ec4823aff8568615dcdeaaab634a675c3cf318c7327a01ef4a9ef", - "spec": "b573703461d2fad8de397bd085302935974ff3c3ef9087d31353e410f59537c8" + "boundver": "fa117a8c7cdb96eb0147fcaef4bec9cfed038c5b63ba676ec05dd6ae2f4f28c0", + "spec": "1d5782f2f165d797d1c380ae0eb25f926182b01433f9877845d05c6739a64948" } } } diff --git a/docs/CI_REENABLE_PLAN.md b/docs/CI_REENABLE_PLAN.md deleted file mode 100644 index 22f2ddc..0000000 --- a/docs/CI_REENABLE_PLAN.md +++ /dev/null @@ -1,30 +0,0 @@ -# CI Re-enable Plan (v1.0.0 Gate) - -## Current State (as of 2026-05-02) -- `.github/workflows/ci.yml` stays manual-only (`workflow_dispatch`). -- Automated PR/push CI is intentionally disabled to prevent runaway GitHub Actions costs during rapid development. - -## Re-enable Preconditions -1. **Release gate reached**: version branch/tag for `v1.0.0` is being cut. -2. **Budget controls set**: - - Monthly Actions spend alert configured. - - Workflow concurrency enabled to cancel superseded runs. - - Path filters and trigger scope reviewed to avoid unnecessary runs. -3. **Owner assigned**: one maintainer accountable for CI cost/health rollback decisions. - -## Re-enable Steps -1. Restore PR/push triggers for `main`. -2. Enable required checks: - - `pytest -q` - - packaging smoke build/install (`wheel` + `sdist` + `boundver --help`) -3. Keep optional checks (lint/type) non-blocking for initial rollout week. -4. Add weekly spend review for first 4 weeks post-enable. - -## Rollback Policy -- If spend exceeds expected weekly envelope or queue contention impacts productivity, - revert to manual-only dispatch and open a follow-up issue with run breakdown. - -## Success Criteria -- CI is auto-triggered on PR/push with stable pass/fail signal. -- Spend remains within budget envelope for 4 consecutive weeks. -- Required checks block regressions without repeated emergency disablement. diff --git a/docs/LOCKFILE_MERGE.md b/docs/LOCKFILE_MERGE.md index 0ae8841..bca73c9 100644 --- a/docs/LOCKFILE_MERGE.md +++ b/docs/LOCKFILE_MERGE.md @@ -1,37 +1,98 @@ -# Lockfile Merge Strategy +# Lockfile merge strategy -`boundary.lock.json` is a generated artifact. When parallel branches both regenerate it, Git conflicts are expected. +`boundary.lock.json` is generated from the repository's source and configuration. When branches change components concurrently, regenerate the complete lockfile from the real merged tree instead of combining fingerprint JSON by hand. -## Future-proof rule -Do **not** hand-edit lockfile conflict hunks. -Always regenerate from `boundary.config.json`. +## Why not use a merge driver? + +A Git merge driver runs while Git is still constructing the merge. It receives individual conflict stages, not a guaranteed materialization of the final source tree, and it may run on a machine without the expected boundver version. Regenerating there can bless fingerprints from the wrong snapshot. + +Keep the lockfile as ordinary text in `.gitattributes`. Perform regeneration only after the source and `boundary.config.json` represent the intended merged result. + +## Resolve a lockfile conflict + +From the repository root: + +```bash +# 1. Resolve source and configuration conflicts first. +git status --short + +# 2. Regenerate the whole lock from the materialized merged tree. +boundver validate-config +boundver generate --source working-tree + +# 3. Check the exact same snapshot and inspect the generated diff. +boundver verify \ + --source working-tree \ + --facets exact,behavior,boundary,compat +git diff -- boundary.lock.json + +# 4. Mark the generated file resolved, then finish the merge. +git add boundary.config.json boundary.lock.json +git status --short +git commit +``` + +Only add `boundary.config.json` in step 4 if the merge actually changed it. Add every resolved source file separately as usual. + +After the merge commit exists, verify the committed snapshot: -## Manual resolution ```bash -# After merge conflict appears -boundver generate +boundver verify --source head --facets exact,behavior,boundary,compat +``` + +The source pairing matters: use `working-tree` before the merge commit, then `head` after it. `head` during conflict resolution still names the pre-merge commit and cannot represent both branches. -git add boundary.lock.json +## Clean merge but stale lockfile + +Git may merge the JSON without a textual conflict even though the combined source requires a different aggregate or slice fingerprint. Run the same regeneration after every merge that touches components, configuration, or the lockfile: + +```bash +boundver generate --source working-tree +boundver verify \ + --source working-tree \ + --facets exact,behavior,boundary,compat +git diff --exit-code -- boundary.lock.json || { + echo "Review and commit the regenerated boundary.lock.json" +} ``` -## Optional Git merge driver (recommended) +## Optional post-merge hook -1. Add to `.gitattributes`: +A local post-merge hook runs after Git has materialized the merged tree. It can regenerate and leave any required lockfile update visible for review: -```gitattributes -boundary.lock.json merge=boundver-lock +```sh +#!/bin/sh +# .git/hooks/post-merge +set -eu + +if command -v boundver >/dev/null 2>&1 && test -f boundary.config.json; then + boundver generate --source working-tree + boundver verify \ + --source working-tree \ + --facets exact,behavior,boundary,compat + + if ! git diff --quiet -- boundary.lock.json; then + echo "boundver regenerated boundary.lock.json; review and commit it." + fi +fi ``` -2. Register merge driver locally: +Make the hook executable: ```bash -git config merge.boundver-lock.name "boundver lockfile regenerate" -git config merge.boundver-lock.driver "scripts/boundver-merge-driver.sh %A" +chmod +x .git/hooks/post-merge ``` -3. Ensure `boundver` is available in your environment. +Hooks are local and are not cloned with the repository. Treat this as a convenience, not enforcement. The authoritative safeguard is a CI job that runs: + +```bash +boundver verify --source head --facets boundary,compat +``` -Now when `boundary.lock.json` conflicts, Git invokes the driver, which regenerates deterministic lock output and writes `%A`. +## Rules of thumb -## CI note -If your CI runs `boundver verify`, merge-driver output is naturally validated during PR checks. +- Resolve configuration and source before regenerating the lockfile. +- Regenerate the full lockfile; a partial refresh is inappropriate for a merge. +- Never hand-edit fingerprint values. +- Review direct consumer changes and slice changes in the generated diff. +- Keep CI on `head` so it verifies exactly what the pull request commits. diff --git a/docs/PROJECT_REVIEW.md b/docs/PROJECT_REVIEW.md new file mode 100644 index 0000000..4bd8a12 --- /dev/null +++ b/docs/PROJECT_REVIEW.md @@ -0,0 +1,171 @@ +# Project review and remediation record + +- Date: 2026-08-12 +- Baseline: `55dd961` (`main`, package version `0.9.1`) +- Remediation target: current working tree (planned package version `0.10.0`) + +This document records the pre-release review of boundver's correctness, +security, packaging, onboarding, documentation, automation, and public project +surface. “Resolved” means the repository now contains the implementation, +regression coverage, or repeatable source-level verification named below. It +does not mean the release candidate has passed external CI or been published. + +## Executive summary + +All findings identified in the baseline review and the subsequent focused +security/correctness passes are resolved in the current tree. Fingerprint +generation and verification now fail closed, hashing uses an unambiguous v2 +wire format, Git paths are handled as NUL-delimited literal data, custom Python +providers require caller-controlled opt-in, and the public Action treats inputs +as data. Lock/config validation, partial updates, metadata/provider checks, +source modes, and OpenAPI canonicalization have focused regression coverage. + +The usability and visibility work is also represented in the repository: the +README has an immediate workflow, examples use the installed CLI, one public +Action is documented, package metadata and community files are present, and +the supported Python floor is consistently 3.9. Release readiness remains +conditional on the unchecked execution and publication gates at the end of +this document; no claim is made here that external CI, GitHub Marketplace, or +PyPI has completed those gates. + +## Baseline findings (BV-001–BV-042) + +| ID | Severity | Area | Resolution evidence | Status | +|---|---|---|---|---| +| BV-001 | Critical | Generation | Strict generation rejects `version_errors`, `exact_errors`, `behavior_errors`, and boundary errors; missing/oversized/raced-file regressions are in `tests/test_hashing_contract.py` and `tests/test_boundary_lock.py`. | Resolved | +| BV-002 | Critical | Verification | `changed_components_since_ref` rejects invalid refs and conservatively covers config/unmapped changes and slices; see `test_verify_invalid_changed_from_exits_usage` and changed-from tests in `tests/test_cli_main.py`. | Resolved | +| BV-003 | Critical | Hashing | `_hash_framed_entries` uses a versioned, domain-separated, length-prefixed binary frame; collision and known-vector tests are in `tests/test_hashing_contract.py`. | Resolved | +| BV-004 | High | Git paths | Git filenames use NUL-delimited byte output plus filesystem decoding; Unicode, newline, non-UTF-8, and literal-backslash regressions are in `tests/test_hashing_contract.py`. | Resolved | +| BV-005 | High | Canonical OpenAPI | `_strip_openapi` distinguishes annotation fields from user-named maps; `tests/test_provider_contract.py` covers annotation-looking schema/property names. | Resolved | +| BV-006 | High | Guardrails | `_git_batch_cat` rejects missing, malformed, truncated, non-blob, and oversized responses; focused tests are in `tests/test_hashing_contract.py` and `tests/test_coverage_gaps.py`. | Resolved | +| BV-007 | High | Custom providers | `_resolve_allow_custom` accepts only caller flag/environment authorization; `load_custom_providers` validates module/class/name inputs. Coverage is in `tests/test_providers.py`, `tests/test_edge_cases.py`, and `tests/test_cli_main.py`. | Resolved | +| BV-008 | High | GitHub Actions | Root `action.yml` passes inputs through environment variables and a quoted Bash array, validates enums, and installs from `github.action_path`; `.github/workflows/ci.yml` contains a hostile-input contract check. | Resolved | +| BV-009 | High | Source modes | `_list_files_for_source` treats the successful Git index as authoritative and working-tree mode as tracked-only; see the corresponding tests in `tests/test_hashing_contract.py`. | Resolved | +| BV-010 | High | Lock semantics | `COMPONENT_METADATA_FIELDS`, structure validation, and `verify_lockfile` cover path, version, provider identity/version, status, SemVer, consumers, metadata, vendored data, and recorded errors. | Resolved | +| BV-011 | High | Partial generation | `generate_lockfile_for_components` requires a valid v2 base, recomputes current entries, rejects stale unselected entries, reconciles removals, and recomputes all slices; see partial-generation tests in `tests/test_core_branches.py`. | Resolved | +| BV-012 | Medium | Installed validation | The config schema is package data under `src/boundver/`; `scripts/packaging_smoke.sh` inspects the wheel and validates an installed copy in an unrelated repository. | Resolved | +| BV-013 | Medium | Config loading | Config and lock loaders reject non-object roots, and hand validation covers malformed nested data; see `tests/test_core_branches.py`, `tests/test_boundary_lock.py`, and `tests/test_cli_main.py`. | Resolved | +| BV-014 | Medium | Config mutation | `_ensure_json_mutation_path` prevents `init`, `add`, and `remove` from serializing JSON into YAML/TOML paths. | Resolved | +| BV-015 | Medium | Discovery | Git-aware discovery uses manifest-specific version sources and deduplicates directories; see `test_discover_components_uses_tracked_manifests_and_deduplicates_dirs`. | Resolved | +| BV-016 | Medium | First run | `init --discover` discloses an empty result instead of inventing `src`, and working-tree validation rejects missing component roots; covered in discovery/init and config-validation tests. | Resolved | +| BV-017 | Medium | Status UX | `print_status` shows component identity, path, version, provider/status, short fingerprints, consumers, and all error categories with corrected guidance; status tests cover text and JSON. | Resolved | +| BV-018 | Medium | Action contract | The duplicate repository-local Action was removed; root `action.yml` is the documented interface and emits command-aware JSON outputs. | Resolved | +| BV-019 | Medium | CLI protocol | Runtime JSON and schemas agree, including status warnings; `tests/test_cli_output_schemas.py` validates real command payloads. | Resolved | +| BV-020 | Medium | Provider protocol | Provider metadata, validation, and explanations are wired, and operations use isolated registries; contract tests are in `tests/test_provider_contract.py`. | Resolved | +| BV-021 | Medium | Completions | Completion scripts cover every parser subcommand and supported options; see completion tests in `tests/test_boundary_lock.py` and `tests/test_cli_main.py`. | Resolved | +| BV-022 | Medium | Module entry point | `src/boundver/__main__.py` exists, and `scripts/packaging_smoke.sh` exercises installed `python -m boundver --version`. | Resolved | +| BV-023 | Medium | Python support | `tomli` is conditional for Python below 3.11, and the supported/build/test floor is consistently Python 3.9 in `pyproject.toml`, docs, and workflow matrices. External matrix execution remains a release gate. | Resolved | +| BV-024 | Medium | Release safety | `.github/workflows/publish.yml` validates exact tag/package-version equality, runs tests, builds, checks distributions, and publishes only the verified artifact job output. | Resolved | +| BV-025 | Medium | Documentation | `README.md` leads with the canonical workflow; `docs/ci-cookbook.md` uses the public Action or explicit installation, and packaging smoke covers the installed workflow. | Resolved | +| BV-026 | Medium | Documentation accuracy | README/getting-started/CI guidance pairs source modes and describes declared-artifact drift rather than semantic compatibility proof. | Resolved | +| BV-027 | Medium | Examples | Example READMEs use `boundver`; `test_examples_expected_lockfiles_are_current` verifies their expected lockfiles. | Resolved | +| BV-028 | Medium | Package metadata | `pyproject.toml` contains keywords, classifiers, project URLs, and publication-safe documentation links. Rendered PyPI verification remains a publication gate. | Resolved | +| BV-029 | Medium | Public repository | `SECURITY.md`, `CODE_OF_CONDUCT.md`, `SUPPORT.md`, issue forms, and the pull-request template are present. | Resolved | +| BV-030 | Low | Test portability | Subprocess tests use the active interpreter (`sys.executable`) or platform-appropriate installed commands instead of assuming a `python` shim. | Resolved | +| BV-031 | Low | Distribution contents | Package-data/MANIFEST configuration includes runtime schema, type marker, specs, policies, and tests needed by the sdist; `scripts/packaging_smoke.sh` inspects wheel and sdist members. | Resolved | +| BV-032 | Low | Packaging lifecycle | `pyproject.toml` uses the SPDX `license = "MIT"` form plus `license-files`, and its Python 3.9 floor is compatible with the security-patched `setuptools>=78.1.1` build requirement. | Resolved | +| BV-033 | Medium | Diff reporting | `_diff.py` compares the shared `COMPONENT_METADATA_FIELDS` as well as fingerprints and reports metadata-only changes. | Resolved | +| BV-034 | Medium | Shell verifier | The divergent standalone shell verifier was retired; `spec/HASHING.md` and Python v2 hashing are the supported contract. | Resolved | +| BV-035 | Medium | Line endings | All source modes use the same text CRLF normalization while preserving binary bytes; cross-source and CRLF tests are in `tests/test_hashing.py`, `tests/test_hashing_contract.py`, and `tests/test_providers.py`. | Resolved | +| BV-036 | High | CI policy | CLI/config `verify_facets`, non-gating observations, JSON output, and Action inputs separate exact-only observations from gated facets; see facet/update tests in `tests/test_cli_main.py`. | Resolved | +| BV-037 | High | Exit protocol | `core.py` defines distinct usage, behavior, boundary, and compatibility exit codes and chooses the highest gated severity; CLI tests cover boundary, behavior, and compatibility exits. | Resolved | +| BV-038 | High | Consumer impact | Config validation and lock metadata support `consumers`; verify/why report affected consumers. `MainSeverityAndConsumerTests` provides end-to-end coverage. | Resolved | +| BV-039 | Medium | Discovery scale | Discovery prefers NUL-safe `git ls-files`, deduplicates directories, excludes known dependency/build/vendor directories, and retains a bounded non-Git fallback; `test_discover_components_excludes_ignored_dirs` covers the exclusions. | Resolved | +| BV-040 | Medium | Contract additions | Glob behavior is documented and tested for matching, traversal rejection, newly added files, and content changes in `tests/test_providers.py`. | Resolved | +| BV-041 | Medium | Merge workflow | The unsound merge-driver script was retired; `docs/LOCKFILE_MERGE.md` specifies post-merge full regeneration and verification. | Resolved | +| BV-042 | Medium | Update UX | `verify --update` recomputes successfully before atomically replacing the lock via `_write_text_atomic`; update behavior is covered in `tests/test_cli_main.py`. | Resolved | + +## Follow-up security and correctness findings (BV-043 onward) + +These findings were discovered during adversarial re-review after the baseline +remediation. They are listed separately to preserve the audit trail rather than +folding them invisibly into the broader baseline items. + +| ID | Severity | Area | Finding and resolution evidence | Status | +|---|---|---|---|---| +| BV-043 | High | Changed selection | A component configured at `.` was not selected for root-file changes. `_git.py::changed_components_since_ref` now handles root paths; `ChangedFromRootComponentTests` verifies the mapping. | Resolved | +| BV-044 | High | Provider path identity | Root-component label slicing could drop the first filename character, making a rename hash-insensitive. `_component_relative_path` now derives exact labels; `RootPathBoundaryIdentityTests` verifies rename sensitivity. | Resolved | +| BV-045 | High | POSIX filenames | Replacing backslashes in Git-returned paths collapsed distinct POSIX names such as `a\b` and `a/b`. Labels now preserve literal backslashes; `test_literal_backslash_filename_is_not_treated_as_a_separator` covers it. | Resolved | +| BV-046 | High | Git pathspecs | Component names beginning with Git pathspec magic, such as `:(literal)foo`, could select a different tree. Hashing and diagnostics now pass `--literal-pathspecs` in `_git.py` and `_output.py`. | Resolved | +| BV-047 | High | Partial locks | Component-scoped generation could create an incomplete first lock or relabel v1 digests as v2. It now requires an existing structurally valid v2 lock; `test_missing_existing_lockfile_requires_full_generation` and `test_non_v2_existing_lockfile_requires_full_generation` cover both cases. | Resolved | +| BV-048 | High | Partial locks | A valid-looking partial update could retain stale unselected component/config/provider data. Partial generation now recomputes the full current lock and rejects stale unselected entries before merging, then rebuilds every slice. | Resolved | +| BV-049 | High | Declared paths | Providers treated the declaration set as valid when one path matched even if another literal/glob did not. Raw and canonical providers now track every unmatched declaration and return an error; provider tests cover missing literals and globs. | Resolved | +| BV-050 | High | Versions | A configured version source that was missing, unparsable, or non-SemVer could produce `null` compatibility data without failing strict generation. `_compute_component_entry` records `version_errors`, and `parse_semver` uses full-string validation; version-source and trailing-junk tests cover it. | Resolved | +| BV-051 | High | Tag versions | `--changed-from` could omit tag-derived components because tags do not appear in a file diff. `_git.py` always includes components using `git_tag_prefix`; the tagged selector assertion in `test_verify_changed_from_checks_unselected_component_metadata` exercises it. | Resolved | +| BV-052 | High | Verification preflight | Unknown explicit `--components` entries could be intersected away by `--changed-from` and return clean. `_cmd_verify` validates requested names before selection; `test_verify_unknown_components_exits_2` covers the controlled error. | Resolved | +| BV-053 | High | Verification preflight | Unknown facets, malformed locks, component/slice set drift, and recorded lock errors could be bypassed by an empty changed set. `_cmd_verify` performs these preflight checks before changed-path scheduling; CLI malformed/facet/ref tests cover the paths. | Resolved | +| BV-054 | Medium | Malformed locks | Nested v2 fields such as fingerprints, SemVer, consumers, error arrays, vendored metadata, and slice members could crash verify/status/slice/why. `_lockfile_structure_issues` now validates consumed types and command handlers use it; `MainMalformedV2LockTests` verifies controlled errors. | Resolved | +| BV-055 | Medium | Malformed config | Nested non-object/non-string config values could reach provider/path logic when optional `jsonschema` was absent. Hand validation now guards defaults, providers, components, boundaries, behaviors, versions, consumers, vendored paths, and slices; malformed-config tests run with the schema engine disabled. | Resolved | +| BV-056 | High | Source modes | Public/core API source typos silently behaved like working-tree mode. `_normalize_source` now accepts only `head`, `index`, or `working-tree`, and all lock operations/accessors call it. | Resolved | +| BV-057 | High | Self-referential locks | A lock output inside a component, especially a root component, became part of its own exact fingerprint. Config rejects root components and `_ensure_lock_outside_components` guards CLI and public API generate/verify paths. | Resolved | +| BV-058 | High | Traversal and symlinks | Component, boundary, behavior, version-source, and vendored paths could traverse or follow working-tree symlinks outside the repository. `_config.py`, `_SourceAccessor`, and hashing containment checks reject unsafe paths or hash Git symlink blobs as link text; focused containment tests cover component roots, version files, vendored paths, boundary paths, and cross-source symlink-blob parity. | Resolved | +| BV-059 | High | Public API | `boundver.generate()` and `boundver.verify()` bypassed full config/source validation, robust lock loading, and self-lock guards. `src/boundver/__init__.py` now shares `validate_config`, `_load_lockfile`, and `_ensure_lock_outside_components` with the CLI. | Resolved | +| BV-060 | Medium | Source-aware validation | `head`/`index` operations validated required files only against the working disk, rejecting valid committed snapshots after local deletion. `validate_config(source=...)` now defers snapshot existence to Git; `SourceAwareValidationTests` covers a deleted working-tree component still present at HEAD. | Resolved | +| BV-061 | High | Project metadata | A missing or changed lockfile project could pass when fingerprints matched. Lock structure requires a non-empty project and verification compares it with config before component work. | Resolved | +| BV-062 | High | Changed-from integrity | With no selected path changes, `--changed-from` returned before current metadata/provider versions were recomputed. It now falls through to full integrity verification; `test_verify_changed_from_no_paths_still_checks_provider_version` covers the failure. | Resolved | +| BV-063 | High | Changed-from integrity | The first fix still skipped unselected entries whenever any component was selected (for example, a tag-versioned component). Changed paths are now reporting-only while all entries are recomputed; `test_verify_changed_from_checks_unselected_component_metadata` covers the two-component tagged/tampered case. | Resolved | +| BV-064 | High | Canonical OpenAPI | Additional arbitrary-name maps—including paths/webhooks, component maps, security requirements, callbacks, links, server variables, schema maps, headers, encodings, and mappings—could lose keys named `description`, `example`, or `x-*`. `_OPENAPI_COMPONENT_MAPS` and `_OPENAPI_NAMED_MAP_KEYS` enumerate those contexts; `tests/test_provider_contract.py` explicitly exercises schema/property/definition and callback/link/variable names. | Resolved | +| BV-065 | Medium | Fail-fast severity | `--fail-fast` could return a lower-severity first component drift while a later component had compatibility drift. Verification now evaluates all selected entries, chooses the global highest-severity issue, and only then limits the report. | Resolved | +| BV-066 | Medium | Slice exits | Slice mismatches omitted their mode, so the exit-code mapper could not assign behavior/boundary/compatibility severity. Slice messages now include `.` and `_drift_exit_code` applies the same severity contract. | Resolved | +| BV-067 | High | Git failure handling | A Git listing failure inside a real repository could fall back to approximate filesystem enumeration and produce a false-clean fingerprint. `_list_files_for_source` now re-raises in real repositories and reserves the bounded fallback for non-Git/unborn setup. | Resolved | +| BV-068 | Medium | Diagnostics | Explain/why diagnostics used line-delimited Git names and non-literal pathspecs, corrupting quoted, newline, or pathspec-magic filenames. `_output.py` now uses `--name-status -z`, `_parse_name_status_z`, filesystem decoding, and `--literal-pathspecs`. | Resolved | +| BV-069 | Medium | Schema-independent identity | Project type and component/slice key types depended on optional `jsonschema`, allowing schema-invalid identities on a base install. Explicit checks were added; `SchemaIndependentConfigValidationTests` patches out the schema engine and verifies rejection. | Resolved | +| BV-070 | Medium | Build/runtime floor | The earlier Python 3.8 support claim conflicted with the setuptools build-backend floor. `pyproject.toml`, CI matrices, README, maintained guides, and `CHANGELOG.md` now consistently declare Python 3.9+. External matrix execution remains a release gate. | Resolved | +| BV-071 | Medium | Atomic writes | Direct lock/config replacement could leave truncated JSON if writing failed. `_write_text_atomic` writes and fsyncs a sibling temporary file before `os.replace`, and generate, verify-update, migration, init, add, and remove route mutations through it. | Resolved | +| BV-072 | Medium | Documentation lifecycle | The historical implementation plan described a retired Action interface and linked deleted design/CI files, while a provider docstring repeated one stale link. The obsolete plan is retired and the provider points to the maintained custom-provider guide. | Resolved | + +## Verification index + +The main repeatable local evidence is grouped here to keep the finding tables +readable: + +- `tests/test_hashing_contract.py`: v2 framing, filename byte safety, tracked + source semantics, guardrails, malformed Git batch data, and read races. +- `tests/test_provider_contract.py` and `tests/test_providers.py`: provider + framing/metadata/hooks, registry isolation, OpenAPI named maps, declared path + matching, canonicalization, and globs. +- `tests/test_security_regressions.py`: root-path identity and selection, + schema-independent identity validation, and source-aware validation. +- `tests/test_cli_main.py`: verification preflight, changed-from full integrity, + malformed nested locks, facets/update behavior, exit severity, and consumers. +- `tests/test_boundary_lock.py`, `tests/test_core_branches.py`, and + `tests/test_edge_cases.py`: lock generation/verification, partial locks, + metadata, discovery, versions, config validation, source modes, and commands. +- `tests/test_cli_output_schemas.py`: real JSON payloads against the published + command schemas. +- `scripts/packaging_smoke.sh`: distribution membership and installed CLI/module + entry-point workflow. + +The committed release candidate passed all 834 unit/integration tests in four +balanced shards without an environment shim. Fresh wheel and sdist builds from +that exact commit contained the required runtime/audit assets; the extracted +wheel passed module-entry-point, config validation, generate, verify, and status +smoke checks in an unrelated Git repository. These local results do not +substitute for the supported-version CI matrix or the publication workflow. + +## Visibility and adoption baseline + +At review time, the public repository had one star, no forks, no GitHub +Releases, and an existing Marketplace listing at version `0.9.1`. PyPI's latest +release was also `0.9.1`, published through trusted publishing. The remediation +tree now configures project links, discovery keywords, clearer outcome-led +messaging, an immediate copyable workflow, and one canonical Action. Those +changes are not described as publicly released until the publication gates +below complete. + +The intended audience is teams maintaining polyglot repositories, services, +or dynamically typed libraries whose consumer-facing contracts are represented +by files such as OpenAPI, JSON Schema, or public export modules. Messaging makes +clear that boundver detects drift in declared artifacts; it does not prove +semantic or backward compatibility. + +## Release gate + +- [x] Every finding above is marked resolved with a verification reference. +- [x] Unit and integration suites pass without environment shims (834 tests). +- [ ] Supported Python versions pass in external CI. +- [ ] Root Action passes its external workflow test with safe input handling. +- [x] Wheel and sdist inspection plus installed-package smoke tests pass for the exact local release commit. +- [ ] Tag exactly matches `pyproject.toml` and `boundver.__version__`. +- [ ] GitHub branch/PR checks pass before the release tag is created. +- [ ] GitHub Release, Marketplace tag, and PyPI publication all point to the same commit. diff --git a/docs/WHY_BOUNDVER.md b/docs/WHY_BOUNDVER.md index 6310866..44efeed 100644 --- a/docs/WHY_BOUNDVER.md +++ b/docs/WHY_BOUNDVER.md @@ -21,7 +21,7 @@ boundver provides **machine-verifiable change classification** at these boundari ### You probably **do** need boundver if… - Your components have consumers but lack static verification of their interface. - You want to distinguish "internals changed" from "behavior changed" from "boundary changed" from "compatibility broke" — automatically, not via commit messages. -- You need a portable, source-controlled lockfile (`boundary-lock/v1`) that CI, scripts, and downstream tools can consume. +- You need a portable, source-controlled lockfile (`boundary-lock/v2`) that CI, scripts, and downstream tools can consume. - You want stable CI cache keys and verification signals without migrating your entire build stack. ## Positioning @@ -62,7 +62,7 @@ These are **conscious scope boundaries**, not bugs: | **Behavioral change in an undeclared file** | If a file changes behavior but isn't in `behavior.paths`, boundver can't know about it. The user must declare what matters. | | **Protocol/wire-format semantic change** | If the `.proto` or schema file type is unchanged but the runtime interpretation differs, no file content changes. | -For the last case — where no static file analysis can detect the change — a custom provider that hashes test output can bridge the gap. See [design/08-behavior-tier.md](design/08-behavior-tier.md#extension-test-output-fingerprinting-via-custom-provider). +For the last case — where no static file analysis can detect the change — a trusted custom provider can hash a deterministic test-output artifact. Treat such providers as executable code and enable them only through the explicit trusted-code opt-in. ## Adoption pattern diff --git a/docs/ci-cookbook.md b/docs/ci-cookbook.md index 7c08ff1..9f5804d 100644 --- a/docs/ci-cookbook.md +++ b/docs/ci-cookbook.md @@ -1,16 +1,12 @@ -# CI Cookbook +# CI cookbook -Practical recipes for integrating boundver into CI/CD pipelines. +These recipes keep the source snapshot, lockfile, and enforcement policy explicit. For pull requests, `head` compares the committed PR tree with the committed lockfile. -## GitHub Actions - -### Basic PR verification - -Blocks merging if the lockfile is stale. Shows a diff when it fails. +## GitHub Actions: recommended boundary gate ```yaml # .github/workflows/boundary-check.yml -name: Boundary check +name: Contract boundary on: [pull_request] jobs: @@ -21,205 +17,172 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: yzm1/boundver@v0 with: - python-version: '3.11' - - - run: pip install boundver - - - name: Verify lockfile - 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 + config: boundary.config.json + lock: boundary.lock.json + source: head + facets: boundary,compat ``` -### Verify only components that changed in this PR +This gate permits exact-only internal refactors while failing declared boundary and compatibility changes. The public Action installs the boundver version bundled with `yzm1/boundver@v0`; no separate Python setup or install step is needed. -Skips components that weren't touched, which is faster for large repos: +The four shown inputs are the portable baseline: -```yaml -- name: Verify changed components only - run: | - boundver verify --changed-from origin/${{ github.base_ref }} -``` +- `config`: configuration path relative to the checkout root +- `lock`: lockfile path relative to the checkout root +- `source`: `head`, `index`, or `working-tree` +- `facets`: comma-separated fingerprints that should fail the job -If no components changed, the command exits 0 immediately. If a changed component's lockfile entry is stale, it exits 1. +The Action also supports `components`, `changed-from`, `fail-fast`, `update`, and `python-version` for specialized workflows. -### Generate and commit the lockfile in CI (for auto-update workflows) +## Pick a signal-to-noise policy -Some teams prefer a CI job that regenerates and commits the lockfile automatically rather than requiring developers to regenerate locally: +The most useful first gate is usually: ```yaml -name: Update boundary lockfile -on: - push: - branches: [main] - -jobs: - update-lockfile: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 +facets: boundary,compat +``` - - uses: actions/setup-python@v5 - with: - python-version: '3.11' +Other common policies are: - - run: pip install boundver +| Policy | Facets | What fails | +|---|---|---| +| Consumer-facing | `boundary,compat` | Declared API and compatibility drift | +| Behavior-sensitive | `behavior,boundary,compat` | Runtime contract, API, and compatibility drift | +| Full lock hygiene | `exact,behavior,boundary,compat` | Any tracked component drift | - - name: Regenerate lockfile - run: boundver generate +You can keep policy in the repository instead of the workflow: - - name: Commit if changed - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add boundary.lock.json - git diff --cached --quiet || git commit -m "chore: regenerate boundary lockfile [skip ci]" - git push +```json +{ + "defaults": { + "verify_facets": ["boundary", "compat"] + } +} ``` -> **Note:** Lockfiles are always deterministic (no timestamps). The lockfile only changes when actual content changes. - ---- +Then omit `facets` when invoking the CLI directly. The Action intentionally defaults to `boundary,compat`. -## Using slice fingerprints as cache keys +Drift outside the gate is returned as a non-gating observation. Structural, metadata, and digest errors remain failures because they make the comparison unreliable. -### GitHub Actions cache +## Verify only components touched by a pull request -Gate downstream jobs on whether the relevant API slice changed: +Fetch history and compare with the base branch: ```yaml -jobs: - check-api-change: - runs-on: ubuntu-latest - outputs: - api-fingerprint: ${{ steps.fp.outputs.fingerprint }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - run: pip install boundver - - name: Extract slice fingerprint - id: fp - run: | - FP=$(python -c " - import json - lock = json.load(open('boundary.lock.json')) - print(lock['slices']['auth-api']['fingerprint'][:16]) - ") - echo "fingerprint=$FP" >> "$GITHUB_OUTPUT" - - build-consumers: - needs: check-api-change - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: consumer-build/ - key: consumer-build-${{ needs.check-api-change.outputs.api-fingerprint }} - - name: Build consumers (only runs if cache miss) - run: make build-consumers +- uses: actions/checkout@v4 + with: + fetch-depth: 0 + +- uses: yzm1/boundver@v0 + with: + config: boundary.config.json + lock: boundary.lock.json + source: head + facets: boundary,compat + changed-from: origin/${{ github.base_ref }} ``` -The build step is a no-op on cache hit. The cache key rotates automatically when the `auth-api` boundary fingerprint changes — and only then. +`--changed-from` reports which components and slices are affected by Git-tracked +paths, while still recomputing the full lock for integrity. This deliberately +prevents an unchanged path set from hiding stale provider metadata or a tampered +entry. An invalid or unavailable ref is a usage error rather than a silent pass, +and configuration changes affect every component because they can add, remove, +or redefine contracts independently of source-file paths. -### Conditional downstream trigger +## Explicit install instead of the Action -```bash -# Read fingerprint from lockfile -CURRENT_FP=$(python -c " -import json -lock = json.load(open('boundary.lock.json')) -print(lock['slices']['auth-api']['fingerprint']) -") - -# Compare to last-known fingerprint stored as a CI artifact or env var -if [ "$CURRENT_FP" != "$LAST_KNOWN_FP" ]; then - echo "API boundary changed — triggering downstream pipeline" - # curl to trigger another pipeline, dispatch a workflow, etc. -fi +Pin the package version for reproducibility: + +```yaml +steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - run: python -m pip install "boundver==0.10.0" + - run: boundver verify --source head --facets boundary,compat ``` ---- +Use this form when your organization mirrors PyPI or centrally manages Python environments. -## GitLab CI +## Inspect failures without regenerating in CI + +Lockfile updates are review decisions, so a PR gate should normally report drift and leave the checkout unchanged: ```yaml -boundary-verify: - stage: validate - image: python:3.11-slim - script: - - pip install boundver - - boundver verify - only: - - merge_requests +- name: Verify contracts + run: boundver verify --source head --facets boundary,compat + +- name: Print machine-readable details + if: failure() + run: boundver verify --source head --facets boundary,compat --format json ``` ---- +Locally, the author can inspect and accept a gated change in one step: -## Handling merge conflicts in the lockfile +```bash +boundver verify --source working-tree --facets boundary,compat +boundver why payment-api --source working-tree +boundver verify --source working-tree --facets boundary,compat --update +git diff -- boundary.lock.json +``` -The lockfile is a JSON file. Merge conflicts happen when two branches update different components concurrently. See [LOCKFILE_MERGE.md](LOCKFILE_MERGE.md) for the recommended merge driver setup. +To refresh non-gating exact and behavior drift too, pass all four facets to the update command. -Quick summary: use `boundver generate` on the merged result and let the tool recompute from source truth rather than trying to resolve JSON conflicts by hand. +## Exit-code-aware automation ---- +`verify` returns the highest selected severity: -## JSON output for scripting +| Code | Meaning | +|---:|---| +| `0` | Clean | +| `1` | Exact or metadata drift | +| `2` | Usage or configuration error | +| `3` | Behavior drift | +| `4` | Boundary drift | +| `5` | Compatibility drift | -All commands that produce output support `--format json` for machine-readable results: +For example, a shell job can distinguish an invalid invocation from contract drift: ```bash -# Check if lockfile is up to date and capture result -RESULT=$(boundver verify --format json) -OK=$(echo "$RESULT" | python -c "import json,sys; print(json.load(sys.stdin)['ok'])") - -if [ "$OK" != "True" ]; then - echo "Lockfile out of date" - echo "$RESULT" | python -c "import json,sys; [print(i) for i in json.load(sys.stdin)['issues']]" - exit 1 -fi -``` +set +e +boundver verify --source head --facets behavior,boundary,compat --format json > boundver-result.json +code=$? +set -e -```bash -# Get slice fingerprint as a single value -python -c " -import json -lock = json.load(open('boundary.lock.json')) -print(lock['slices']['my-slice']['fingerprint']) -" +case "$code" in + 0) echo "Declared contracts are current" ;; + 2) echo "boundver could not perform a reliable check" >&2; exit 2 ;; + 3) echo "Behavior contract changed" >&2; exit 3 ;; + 4) echo "Boundary changed; re-verify consumers" >&2; exit 4 ;; + 5) echo "Compatibility family changed" >&2; exit 5 ;; + *) echo "Unexpected boundver result: $code" >&2; exit "$code" ;; +esac ``` ---- +The Action exposes `exit-code`, newline-separated `issues`, and `observations` outputs for workflows that use `continue-on-error` and apply their own policy. -## Pre-commit hook - -Add a pre-commit hook to catch stale lockfiles before they're pushed: +## GitLab CI -```bash -# .git/hooks/pre-commit -#!/bin/bash -set -e -if [ -f boundary.config.json ]; then - boundver verify --source working-tree || { - echo "Lockfile is stale. Run: boundver generate --source working-tree" - exit 1 - } -fi +```yaml +boundary-verify: + stage: test + image: python:3.12-slim + before_script: + - python -m pip install "boundver==0.10.0" + script: + - boundver verify --source head --facets boundary,compat + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" ``` -Or use the pre-commit framework: +## Pre-commit + +Working-tree verification should be paired with working-tree updates: ```yaml # .pre-commit-config.yaml @@ -227,22 +190,54 @@ repos: - repo: local hooks: - id: boundver-verify - name: Verify boundary lockfile - entry: boundver verify --source working-tree + name: Verify declared boundaries + entry: boundver verify --source working-tree --facets boundary,compat language: system pass_filenames: false always_run: true ``` ---- +If the hook finds intentional drift, review it and run: + +```bash +boundver verify --source working-tree --facets boundary,compat --update +git add boundary.lock.json +``` + +New files must already be known to Git for working-tree and index modes. -## Skipping verification for non-component changes +## Use a slice fingerprint as a cache key -If your repo has docs, CI config, or root-level files that shouldn't trigger a lockfile update, scope your `verify` call to components actually relevant to the change: +A slice combines one facet from several components. Read its committed fingerprint into a workflow output: -```bash -# Only verify components changed since the base branch -boundver verify --changed-from origin/main +```yaml +jobs: + contract-key: + runs-on: ubuntu-latest + outputs: + fingerprint: ${{ steps.key.outputs.fingerprint }} + steps: + - uses: actions/checkout@v4 + - id: key + shell: bash + run: | + value=$(python -c 'import json; print(json.load(open("boundary.lock.json"))["slices"]["checkout-contracts"]["fingerprint"])') + echo "fingerprint=$value" >> "$GITHUB_OUTPUT" + + build-consumers: + needs: contract-key + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: consumer-build + key: consumer-${{ needs.contract-key.outputs.fingerprint }} + - run: make build-consumers ``` -Components whose paths weren't touched are silently skipped, so unrelated PRs (e.g., README edits) pass without needing a lockfile update. +A boundary-mode slice rotates only when one of its member boundaries changes. It is a deterministic cache key, not a substitute for running consumer tests after a contract change. + +## Concurrent lockfile updates + +Do not hand-edit JSON conflict hunks and do not run generation inside a Git merge driver. Finish merging source and configuration, then regenerate from the materialized working tree. See [Lockfile merge strategy](LOCKFILE_MERGE.md) for the command sequence and an optional post-merge hook. diff --git a/docs/design/01-correctness-portability.md b/docs/design/01-correctness-portability.md deleted file mode 100644 index c4077dc..0000000 --- a/docs/design/01-correctness-portability.md +++ /dev/null @@ -1,26 +0,0 @@ -# 01 — Correctness & Portability Design - -## Goal -Make boundary fingerprinting behavior explicit, deterministic, and portable for public users. - -## Scope -- Normalize boundary extraction outcomes (`ok`, `partial`, `error`) and publish behavior contract. -- Eliminate assumptions tied to internal/proprietary artifacts. -- Provide actionable failures when configured boundaries are unavailable. - -## Design -1. **Boundary status contract** - - `ok`: declared boundary paths exist and API digest is produced. - - `partial`: boundary intentionally implicit (`kind=implicit`) and no boundary paths are declared. - - `error`: boundary is explicit but no paths or no digest can be produced. -2. **Provider portability layer** - - Separate public providers (`openapi`, `python-exports`, `typescript-exports`, `json-schema`) from optional org-specific providers. - - Unknown/custom providers can be registered through config-driven adapters (see doc 05). -3. **Strict diagnostics** - - Include `boundary_errors` in lock output and `status` command. - - Preserve strict mode behavior in slices; `--allow-partial` remains explicit escape hatch. - -## Deliverables -- Documented status model in README and config reference. -- Provider capability matrix with public-first defaults. -- Clear CLI warnings and failure messages. diff --git a/docs/design/02-testing-strategy.md b/docs/design/02-testing-strategy.md deleted file mode 100644 index ce4e656..0000000 --- a/docs/design/02-testing-strategy.md +++ /dev/null @@ -1,25 +0,0 @@ -# 02 — Testing Strategy Design - -## Goal -Raise confidence from smoke-level checks to production-grade coverage. - -## Scope -- Unit, integration, snapshot, and edge-case testing for fingerprint correctness. - -## Design -1. **Unit tests** - - Canonical JSON and hash determinism. - - SemVer parsing modes. - - TOML/YAML/JSON version extraction. -2. **Integration tests (temp git repos)** - - `git init` + commits and tag workflows. - - Source mode behavior (`head`, `index`, `working-tree`). -3. **Snapshot tests** - - Golden lockfile fixtures for representative repositories. -4. **Edge cases** - - Missing/implicit boundaries, empty slices, no commits, unknown components. - -## Deliverables -- Test matrix with required cases and expected outcomes. -- Stable fixture generators for temporary repos. -- Coverage targets for critical modules. diff --git a/docs/design/03-ci-quality-gates.md b/docs/design/03-ci-quality-gates.md deleted file mode 100644 index 351ff73..0000000 --- a/docs/design/03-ci-quality-gates.md +++ /dev/null @@ -1,23 +0,0 @@ -# 03 — CI Quality Gates Design - -## Goal -Provide predictable quality gates while controlling cost during rapid development. - -## Scope -- Define staged CI re-enable plan and required checks. - -## Design -1. **Stage A (current / cost-controlled)** - - Manual (`workflow_dispatch`) CI only. - - Keep tests runnable locally and in ad hoc CI. -2. **Stage B (pre-v1 hardening)** - - PR-triggered tests on key versions. - - Lint/type checks as non-blocking signals. -3. **Stage C (v1.0 and after)** - - Required PR checks (test + lint + type). - - Optional nightly broader matrix. - -## Deliverables -- Cost-aware CI schedule. -- Policy for when checks become required. -- Rollback/fallback if CI spend spikes. diff --git a/docs/design/04-runtime-version-support.md b/docs/design/04-runtime-version-support.md deleted file mode 100644 index e981bc3..0000000 --- a/docs/design/04-runtime-version-support.md +++ /dev/null @@ -1,21 +0,0 @@ -# 04 — Runtime Version Support Design - -## Goal -Define supported Python versions and upgrade cadence. - -## Scope -- Current supported floor (3.8+) and future expansion. - -## Design -1. **Support tiers** - - **Tier 1 (required CI)**: Python 3.8–3.12. - - **Tier 2 (forward-compat validation)**: Python 3.13–3.14. -2. **Compatibility policy** - - Avoid runtime dependencies where possible. - - Keep stdlib-only runtime behavior. -3. **Release criteria** - - Any v1.0 release must pass Tier 1 and Tier 2 smoke tests. - -## Deliverables -- Version support table in README/docs. -- CI matrix plan including 3.13 and 3.14 before v1.0 finalization. diff --git a/docs/design/05-custom-boundary-extension.md b/docs/design/05-custom-boundary-extension.md deleted file mode 100644 index 1aa7fe9..0000000 --- a/docs/design/05-custom-boundary-extension.md +++ /dev/null @@ -1,45 +0,0 @@ -# 05 — Custom Boundary Extension Design - -## Goal -Make custom boundaries first-class so users can extend boundver without modifying core logic. - -## Problem statement -Users need a clear way to hash API boundaries that are not built-in provider types. - -## Design -1. **Provider interface contract** - - Input: component root, boundary config, source mode. - - Output: `digest`, `status`, `errors`, optional metadata. -2. **Config-driven provider registration** - - Add `boundary.provider` and `boundary.options` in config. - - Support built-ins and local plugin providers. -3. **Execution model** - - Provider resolves concrete files/content. - - Core canonicalizes resolved payload and hashes deterministically. -4. **Safety constraints** - - Providers are pure/read-only. - - Fail closed (explicit `error`) if provider cannot resolve artifacts. - -## Example config sketch -```json -{ - "components": { - "billing": { - "path": "services/billing", - "boundary": { - "kind": "custom", - "provider": "jsonpath-extract", - "options": { - "file": "contract.json", - "select": ["$.paths", "$.components.schemas"] - } - } - } - } -} -``` - -## Deliverables -- Provider API doc. -- Two reference custom providers. -- End-to-end examples showing custom boundary setup. diff --git a/docs/design/06-config-schema-validation.md b/docs/design/06-config-schema-validation.md deleted file mode 100644 index 26d6e01..0000000 --- a/docs/design/06-config-schema-validation.md +++ /dev/null @@ -1,23 +0,0 @@ -# 06 — Config Schema & Validation Design - -## Goal -Prevent invalid configurations early with schema + semantic validation. - -## Scope -- JSON Schema publishing, editor support, and runtime validation messages. - -## Design -1. **Schema layer** - - Publish `boundary.config.schema.json` with `$schema` support. - - Validate structure, required fields, enums, and types. -2. **Semantic validation layer** - - Cross-reference slices/components. - - Detect duplicate component paths. - - Validate provider requirements and boundary path existence. -3. **DX improvements** - - Actionable error messages with component/slice names and remediation hints. - -## Deliverables -- Schema file versioned with releases. -- Validation command output contract. -- IDE autocompletion setup instructions. diff --git a/docs/design/07-provider-architecture.md b/docs/design/07-provider-architecture.md deleted file mode 100644 index 6ce8aa5..0000000 --- a/docs/design/07-provider-architecture.md +++ /dev/null @@ -1,334 +0,0 @@ -# 07 — Provider Architecture Design - -## Problem - -Today `boundary.provider` is a validation label only. Every provider (`openapi`, -`python-exports`, `typescript-exports`, `json-file`, `leaf`, `implicit`) runs the -same code path: list `boundary.paths`, read raw bytes, hash. No provider can -normalize its content, explain a diff, or carry options beyond a path list. - -Consequences: -- A comment added to an OpenAPI file changes the boundary digest even when the - API contract is identical. -- Custom provider logic lives outside boundver and must own its own hashing, - removing the guarantee that fingerprints are computed consistently. -- `explain_diff` can only say "bytes changed", not "endpoint `/billing` removed". - ---- - -## Goals - -1. Each provider controls **what content** is hashed, not how it is hashed. -2. Core always owns the **canonical digest step** — same algorithm, same encoding. -3. Providers are **pure/read-only** — no side effects. -4. Built-in providers are thin wrappers; their current behavior is preserved - exactly until a semantic provider replaces them. -5. Custom providers plug in without modifying core. - ---- - -## Core types - -```python -from __future__ import annotations -from dataclasses import dataclass, field -from pathlib import Path -from typing import Callable, Dict, List, Optional, Protocol, runtime_checkable - - -# ------------------------------------------------------------------ -# Passed to every provider method -# ------------------------------------------------------------------ -@dataclass -class ProviderContext: - repo_root: Path - component_path: str # repo-relative, e.g. "services/billing" - boundary_cfg: dict # full boundary config dict - source: str # "head" | "index" | "working-tree" - # Injected by core so providers never call git directly - read_file: Callable[[str], bytes] # repo_rel_path → bytes - list_files: Callable[[str], List[str]] # repo_rel_prefix → [repo_rel_path, ...] - - -# ------------------------------------------------------------------ -# What a provider returns from resolve() -# ------------------------------------------------------------------ -@dataclass -class ResolvedBoundary: - # Ordered list of (label, content) pairs. - # Core hashes them as: sha256(concat("entry: