Skip to content

Versioning & SemVer policy

This page defines what counts as a breaking change for every public surface packageprobe exposes, and the deprecation window each surface gets before something is removed. It is the contract downstream consumers — CLI users, GUI automation, FFI embedders, and Fleet API clients — can rely on.

packageprobe ships as a single versioned unit. Releases are git tags matching vMAJOR.MINOR.PATCH (release workflow pattern v[0-9]+.[0-9]+.[0-9]+*), with optional pre-release suffixes (-rc.1, -test.N). That one tag drives every artifact — the CLI/FFI binaries, the macOS DMG, and the Homebrew formulae.

The version is not stored in the Cargo manifest: app/Cargo.toml deliberately stays at the 0.0.0-dev placeholder. The release value is injected at build time from the PACKAGEPROBE_VERSION env var (set by the release workflow from the tag) and surfaced through packageprobe_core::VERSION via build.rs — read that constant rather than env!("CARGO_PKG_VERSION").

Following Semantic Versioning 2.0.0:

BumpMeaning
MAJORA breaking change to any public surface below.
MINORBackward-compatible additions (new command, flag, symbol, route, field, config key).
PATCHBackward-compatible bug fixes with no surface change.

A commit that breaks a public surface carries ! before the colon and a BREAKING CHANGE: footer (Conventional Commits). Those footers force the next MAJOR and populate the release notes’ Breaking changes section.

While the version is 0.y.z, a MINOR bump may carry a breaking change (standard SemVer for the 0.x series). Even so, the per-surface definitions below still classify what breaking means, and every such change is still announced. Once 1.0.0 ships, breaking changes are confined to MAJOR bumps.

SurfaceArtifactWhat is stable
CLIpackageprobe binarySubcommand names, flag long-names, documented exit codes, --format json output shape
FFIlibpackageprobe_ffi + packageprobe_ffi.hExported packageprobe_* C symbols, their signatures, the JSON schema of returned strings, the free contract
GUI AIDsSwiftUI .accessibilityIdentifier() valuesThe identifier strings themselves — a stable API for automation/XCUITest
Service REST APIsfleet, vuln-proxy, license-server HTTP endpointsRoute paths, HTTP methods, request/response JSON, auth header names/semantics
Config schema~/.packageprobe/config.yaml + admin policy fileConfig keys, their types, and defaults

Everything not in this table carries no SemVer guarantee — see Not a public surface at the end.

Breaking (MAJOR):

  • Removing or renaming a subcommand or a flag long-name.
  • Changing a flag’s default or meaning such that the same invocation produces different output or behaviour.
  • Removing, renaming, or retyping a field in --format json output.
  • Changing documented exit-code semantics (e.g. --policy returning non-zero on violation).
  • Making a previously optional argument required.

Non-breaking (MINOR/PATCH):

  • Adding a subcommand, a flag, or an accepted value to an enum flag (e.g. a new --format value).
  • Adding a field to --format json output.
  • Changing human-readable (non-JSON) output text, --help copy, or log wording.

Deprecation: a deprecated flag or subcommand keeps working for ≥ 1 MINOR release or 90 days, whichever is longer, emitting a one-line deprecation warning to stderr (never stdout — stdout stays machine-parseable) and noted in the release notes. Removal happens only on a MAJOR. Short flags (-o, -v) are aliases of their long form and share its lifecycle.

The FFI surface (header generated by cbindgen) is consumed in-process by the native GUIs and any third-party embedder.

Breaking (MAJOR):

  • Removing or renaming an exported packageprobe_* symbol.
  • Changing a function’s signature — parameter list, types, or return type.
  • Changing the allocation/free contract (all returned strings are heap-allocated UTF-8 freed with packageprobe_free_string).
  • Changing the JSON schema of a returned string non-additively (removing, renaming, or retyping a field).
  • Changing the meaning of a returned status code.

Non-breaking (MINOR/PATCH):

  • Adding a new exported symbol.
  • Adding a field to a returned JSON object.
  • Adding a new status/enum value that older callers ignore gracefully.

Deprecation: new behaviour that would change a signature ships as a new symbol (e.g. packageprobe_scan_v2) rather than mutating the existing one. A symbol marked deprecated (header comment + docs) is kept for ≥ 2 MINOR releases and removed only on a MAJOR. Because the header is cbindgen-generated, any symbol removal or signature change shows up as a header diff — treat a header diff that deletes or renames a symbol, or changes a signature, as a MAJOR gate.

Every significant SwiftUI view carries an .accessibilityIdentifier() following the <area>.<component>[.<sub>] convention (dynamic segments use the runtime value, e.g. settings.languageCard.node). These strings are a stable API for UI automation and XCUITests.

Breaking (MAJOR):

  • Renaming or removing an existing identifier.
  • Changing the identifier value bound by an existing element that automation or XCUITests target.

Non-breaking (MINOR/PATCH):

  • Adding an identifier to a newly introduced element.
  • Adding a new dynamic sub-segment under an existing namespace.

Policy: any AID change must update the accessibility-identifiers reference in the same PR (a documented rule). A rename is breaking; where feasible, keep the old identifier as an additional identifier on the element for ≥ 1 MINOR release to give automation a migration window before the old value is dropped on a MAJOR.

Service REST APIs (Fleet, vuln-proxy, license-server)

Section titled “Service REST APIs (Fleet, vuln-proxy, license-server)”

The Fleet, vuln-proxy, and license-server services expose HTTP APIs. The breaking-change definitions below apply uniformly to all three; the versioning mechanism differs by endpoint class.

Coverage is by prefix, not by enumeration: the entire /api/v1/* surface of each service is a public surface under this contract — the routes named below are illustrative examples, not an exhaustive allow-list, so a new /api/v1/* route is covered the moment it ships. Fleet serves deployed agents (the ingestion endpoints POST /api/v1/enroll, /api/v1/report, /api/v1/heartbeat) and dashboard/automation clients (/api/v1/organizations/*, /api/v1/licenses/*). The vuln-proxy serves clients POST /api/v1/query (plus the opt-in /api/v1/typosquat contribution path). The license-server serves /api/v1/licenses/*, /api/v1/machines/*, /api/v1/seats/*, /api/v1/entitlements, and /api/v1/audit-events.

Breaking (MAJOR):

  • Removing or renaming a route, or changing its HTTP method.
  • Removing, renaming, or retyping a request or response JSON field.
  • Tightening validation to reject a payload that previously succeeded.
  • Changing auth header names or semantics (Authorization, X-Packageprobe-Fleet-Key, X-Packageprobe-Timestamp, X-Packageprobe-Request-ID).
  • Changing an error status code that clients branch on.

Non-breaking (MINOR/PATCH):

  • Adding a new route.
  • Adding an optional request field or a new response field.
  • Accepting an additional auth scheme alongside the existing ones.

Path-versioned endpoints (/api/v1/*): the major version lives in the path. A breaking wire change ships under a new /api/v2/ prefix while /api/v1/ continues to be served for ≥ 1 MAJOR release or 180 days, whichever is longer — the server runs both during the window. Additive changes stay on v1. Fleet’s ingestion endpoints get the longest support, because agents in the field upgrade on their own schedule. Database-schema evolution behind these APIs follows the expand/contract discipline and one-version-forward compatibility contract in the upgrade & rollback runbook.

Unversioned public endpoints: a few public endpoints predate — or sit outside — the /api/v1/ scheme, most notably the license-server’s POST /trials (called by the desktop trial flow) and the vuln-proxy’s legacy GET /search and GET /vuln/{id} (public advisory links). These are still public surfaces and obey the same breaking-change definitions above, but because they carry no version segment a breaking change is handled in place — the old endpoint is kept working for ≥ 1 MAJOR release / 180 days (optionally redirecting to a new path) rather than served under a parallel /api/vN/ prefix. New public endpoints should be added under /api/v1/.

Excluded from this contract: provider webhook receivers (POST /webhook/lemonsqueezy, POST /webhook/appstore) — their request shape is dictated by the external provider, not a packageprobe-defined contract — and the operational GET /health / GET /metrics utility endpoints.

The user config at ~/.packageprobe/config.yaml (and the admin policy file) is a public surface — existing configs must keep loading across upgrades.

Breaking (MAJOR):

  • Removing or renaming a config key.
  • Changing a key’s type or default so an existing valid config behaves differently or fails to load.

Non-breaking (MINOR/PATCH):

  • Adding an optional key with a safe default.
  • Adding an accepted enum value to an existing key.

Policy: a removed or renamed key is still accepted (mapped to its replacement, with a deprecation warning) for ≥ 1 MINOR release before removal on a MAJOR — mirroring how discovery_backend: maps to backend: today. Config loading must never panic; an unknown key is ignored, and the partial-merge write path preserves keys a given client doesn’t model.

SurfaceMinimum window before removalRemoval allowed inRuntime warning
CLI flag / subcommand≥ 1 MINOR or 90 days (longer)MAJORstderr line
FFI symbol≥ 2 MINOR releasesMAJORheader comment + docs
GUI AID≥ 1 MINOR (alias where feasible)MAJORn/a (doc + release notes)
Fleet / vuln-proxy / license API≥ 1 MAJOR or 180 days (path-versioned)new /api/vN+1/old path served in parallel
Config key≥ 1 MINOR releaseMAJORconfig-load warning

Every removal is listed in the release notes’ Breaking changes section, keyed to the surface it affects.

These have no SemVer guarantee and may change in any release:

  • Internal packageprobe-core Rust APIs — the crate is not published to crates.io; only the FFI and CLI are public.
  • On-disk cache formats (~/.packageprobe/vuln-cache/, the scan cache) — internal, regenerated on demand, cleared with mise run clear-cache.
  • Human-readable CLI text, GUI copy, and visual layout.
  • Audit/app log line formatting. (Audit event tags are separately locked by a regression test, but their textual rendering is not a versioned surface.)
  • Undocumented internal endpoints on any service not listed above.