Design Goal

The main purpose of this format is to let inventory tools reliably remind system administrators about packages they explicitly installed.

Current package-manager databases and logs are not enough for this. Some package managers do not retain useful transaction history; some rotate or delete logs; some expose install timestamps but not a durable selection class such as "this package was requested by an administrator" versus "this package was pulled in as a dependency". The transaction log described here is intended to be small, append-only, durable, and implementable by all package managers.

Terminology

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.

Core concepts

The important distinctions are:

  • base: the package was seeded by an operating-system installer, image builder, package-manager bootstrap, global profile, or comparable initial system state.

  • manual: the package was explicitly named by an administrator, manifest, profile, or global-install command.

  • auto: the package was installed because the solver needed it to satisfy some other request.

An inventory program can then answer this question directly: which currently installed packages did an administrator explicitly request, excluding base packages unless they were later explicitly manual?

Everything else in this specification is in support of that operation.

Storage

The log MUST be JSON Lines: UTF-8 text, one complete JSON object per line. This keeps the format append-friendly, streamable, recoverable after partial writes, and easy to inspect with ordinary tools.

A package manager may keep one or more journals under persistent state. System-scope journals MUST live at:

/var/lib/pkg-transactions/*.jsonl

Recommended user-scope journals SHOULD live at:

${XDG_STATE_HOME:-~/.local/state}/pkg-transactions/*.jsonl

The directory is deliberately under persistent state, not an ordinary log directory such as /var/log, because these records are package database provenance, not disposable diagnostics.

Best practice is to have only a single file representing the entire timeline of transactions recorded by possibly multiple package managers. In that case package managers MUST write-lock appends for atomicity. The preferred name for the single timeline is "journal.jsonl".

When permissions issues make a shared journal impossible, package managers MAY create multiple journal files names in any convenient way. Filenames SHOULD NOT be semantically loaded; records should remain self-describing when journals are concatenated, exported, moved, or analyzed outside their original path.

Record Shape

Each package-state line MUST describe a committed package-manager state change. Managers MUST NOT write records for planned actions that did not change package state, failed package operations that left package state unchanged, or ordinary diagnostic events. If a transaction partially succeeds, the manager MUST write records only for packages whose state actually changed. If a rollback changes package state, the manager MUST write records describing the resulting state changes, not a failure event.

Each package-state line should be independently useful even if transaction begin or end records are missing. This section specifies package-state records; adding other record types later would be a backward-incompatible shape change requiring a new required field.

Example package record:

{
  "time": "2026-05-18T15:04:05Z",
  "txid": "01JVR6K2ZCFAT3RZ3Z2W0V6S1G",
  "scope": "apt:/",
  "operation": "install",
  "selection": "manual",
  "reason": "manual"
  "package": {
    "name": "vim",
    "version": "2:9.1.001-1",
    "id": "vim:amd64",
    "repository": "debian-stable"
  },
}

Compatibility And Evolution

There is no explicit schema-version field. Consumers recognize records by their shape: the required fields present, the types of values needed to interpret them, and the required-field values they understand.

Consumers MUST ignore records whose shapes they do not recognize. This includes records with missing required fields, unexpected value types, or required-field values the consumer does not understand. Consumers MUST NOT reject a journal merely because it contains unknown record shapes or extra fields. Consumers SHOULD ignore unknown optional fields in otherwise recognized records.

Each backward-incompatible change MUST be marked by either adding a new required field or introducing a new value for an existing required field, such as operation, selection, or reason. Adding a required record field to distinguish package records from other record types would be one example of such a shape change. This makes old consumers skip records they cannot safely interpret rather than misreading them.

Required Fields

time

UTC RFC 3339 timestamp, for example 2026-05-18T15:04:05Z.

scope

String identifying the package namespace and installation universe being modified. It MUST distinguish package namespaces that might otherwise collide in a shared journal. Consumers MUST compare scope strings byte-for-byte and SHOULD NOT infer semantics from them except for display or grouping. Examples include apt:/, pacman:/, flatpak:system, flatpak:user, homebrew:/opt/homebrew, nix:profile:/nix/var/nix/profiles/system, and npm:/usr/local.

operation

Committed operation that affected the package. Common values are bootstrap, install, remove, purge, upgrade, downgrade, reinstall, mark, autoremove, refresh, and gc.

package

Object identifying the package. It MUST include name and SHOULD include id when the package manager can provide a stable native package identity. Consumers SHOULD use package.id as the replay identity within the scope when present, falling back to package.name when it is absent. If architecture, ABI, flavor, or another native qualifier is needed to distinguish co-installable packages, the manager SHOULD include that qualifier in package.id.

selection

String containing the current selection class after the operation. The value MUST be one of the values in the selection vocabulary below.

reason

Shared reason code explaining why this package was touched.

Optional fields

txid

Transaction identifier shared by all records produced by one package-manager operation. Not required in a singleton record.

held

Boolean indicating that the package is under a persistent package-manager policy that prevents ordinary solver actions from upgrading, removing, or replacing it. This field is independent of selection. It should be omitted when the package is not held, when the manager has no equivalent concept, or when hold state is unknown.

package.repository

Optional manager-defined repository, channel, tap, registry, or comparable source label. This field is descriptive package metadata. Consumers MUST NOT use it to identify a package unless the package manager also includes it in package.id.

Selection Vocabulary

The selection field describes current install intent after the operation. It is a state classification, not the reason this transaction touched the package.

manual

The package is installed and was explicitly selected by an administrator, manifest, profile, or global-install command.

auto

The package is installed because the package manager selected it to satisfy some other package or profile.

base

The package is installed as part of the base system, initial image, package-manager bootstrap, or first profile, and has not later been explicitly selected.

absent

The package is not installed after this operation. This is normally used in remove, purge, autoremove, and gc records.

unknown

The package is installed, but the manager cannot classify it as manual, auto, or base.

Reason Vocabulary

The reason field should use a small shared vocabulary:

manual

The package was explicitly named by an administrator, manifest, profile, or global-install command.

dependency

The package was pulled in by dependency solving.

weak-dependency

The package was installed as a recommendation, supplement, optional runtime, suggested package, or equivalent weak dependency.

build-dependency

The package was installed in order to build another package.

base

The package was seeded by an OS installer, image builder, package-manager bootstrap, system profile, or equivalent initial state.

replacement

The package was installed as part of a rename, transition, obsoletion, or replacement.

cleanup

The package was removed because it was orphaned, obsolete, garbage-collected, or autoremoved.

unknown

The manager cannot classify the reason.

Base-System Handling

The format needs a first-class baseline transaction. During OS installation, image creation, package-manager bootstrap, or first profile creation, the manager SHOULD emit package records with operation set to bootstrap, reason set to base, and selection set to base.

Example baseline package record:

{
  "time": "2026-05-18T00:00:00Z",
  "txid": "baseline-20260518",
  "scope": "pacman:/",
  "operation": "bootstrap",
  "selection": "base",
  "reason": "base"
  "package": {
    "name": "filesystem",
    "version": "2025.05-1"
  },
}

If an administrator later explicitly requests a base package, the manager SHOULD emit a mark record or a no-op request record with selection set to manual. This allows inventory tools to include the package as an intentional administrator selection even though it originated in the base system.

Dependency Example

The first record below is an explicit administrator request. The second is a dependency installed to satisfy that request.

{
  "time": "2026-05-18T15:04:05Z",
  "txid": "tx123",
  "scope": "apt:/",
  "operation": "install",
  "package": {
    "name": "vim",
    "version": "2:9.1"
  },
  "selection": "manual",
  "reason": "manual"
}
{
  "time": "2026-05-18T15:04:06Z",
  "txid": "tx123",
  "scope": "apt:/",
  "operation": "install",
  "selection": "auto",
  "reason": "dependency"
  "package": {
    "name": "vim-runtime",
    "version": "2:9.1"
  },
}

Replay Rules For Inventory

An inventory tool should replay records independently for each tuple of scope and package identity.

  1. Consume only recognized package-state records. The presence of a recognized record means the described state change was committed.

  2. Treat install, bootstrap, upgrade, downgrade, reinstall, refresh, and mark as operations that update current package state.

  3. Treat remove, purge, autoremove, and gc as operations that mark the package not currently installed.

  4. Consider a package an administrator reminder candidate if it is currently installed and selection is manual.

  5. Hide a package by default if it is currently installed and selection is base.

  6. Treat a package as dependency noise if it is currently installed and selection is auto.

These rules intentionally make the current selection value authoritative. A package can move from auto to manual, manual to auto, base to manual, or manual to removed without requiring consumers to infer intent from old command lines.

Package-Manager Mappings

apt

Explicit command targets are manual. Solver-installed dependencies are auto with reason dependency. Debian installer manifests or comparable bootstrap state are base. apt-mark manual and apt-mark auto should emit mark records.

pacman

Explicit install targets are manual. Packages installed with dependency semantics are auto. --asexplicit and --asdeps should emit mark records. Bootstrap packages or the installed base closure can be logged as base.

dnf, zypper, and rpm

High-level solvers should log manual packages and dependency reasons from their transaction plans. Low-level rpm -i can log packages as manual or unknown if no solver context exists.

apk

Entries explicitly added to the world file are manual. Solver-installed dependencies are auto. Initial world or bootstrap packages are base.

emerge

The world set is manual. Packages installed only by dependency solving are auto. The system set is base.

xbps and pkg

Use native auto/manual flags where available. Package-manager bootstrap or initial image packages should be base.

homebrew

Map installed_on_request to manual and installed_as_dependency to auto. Prefix creation or managed bootstrap packages may be base.

flatpak and snap

Application installs are usually manual. Runtimes, bases, content snaps, and similar support packages should usually have selection auto or base, with reason dependency or base as appropriate.

pipx and cargo

Top-level globally installed tools are normally manual.

gem, npm, and yarn

Globally manual top-level packages are manual. Visible installed dependencies should be auto with reason dependency.

nix

Profile elements are manual. Store closure paths are dependencies if they are logged at all. System profile or image entries may be base.