Skip to content

feat(packages): installed-packages fact (source-namespaced record arrays)#29

Merged
ncode merged 6 commits into
mainfrom
juliano/packages-fact-impl
Jul 2, 2026
Merged

feat(packages): installed-packages fact (source-namespaced record arrays)#29
ncode merged 6 commits into
mainfrom
juliano/packages-fact-impl

Conversation

@ncode

@ncode ncode commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the packages fact (ADR-0014): the installed packages on the host, namespaced by the package database they come from. packages.<source> is an array of {name, version, ...} records with per-source identity fields. Sources are never merged, a source is omitted when its database is absent, and the whole probe is a gated single-output resolver--disable packages skips collection, and packages is a named fact group in --list-block-groups (a deliberate Facts-native parity divergence).

Sources (one cheap read/query each — no spawn-per-package, no network)

Platform Sources
Linux dpkg, rpm, pacman, apk, snap, flatpak, nix
FreeBSD / DragonFly pkg (+ pkgsrc secondary on DragonFly)
OpenBSD / NetBSD / illumos openbsd_pkg / pkgsrc / ips (+ pkgsrc SmartOS secondary)
macOS receipts (primary), apps (secondary, never merged), homebrew, nix
Windows registry (both HKLM hives), appx

Readers live in packages_{bsd,mac,win,extra}.go with no GOOS suffixes or build constraints (ADR-0010); tools outside the engine's trusted PATH use absolute paths.

Validation — every source against its real database

Source Count = native
dpkg / rpm / pacman / apk 324 & 740 / 386 / 183 / 200
pkg (freebsd / dragonfly) 2 / 48
openbsd_pkg / pkgsrc / ips 18 / 19 / 418
snap / flatpak (installed real software on ubuntu2404) 3 / 4 — flatpak branch identity proven by live same-version siblings
nix (NixOS system profile + real daemon install on ubuntu) 70 / 3 — both enumeration modes
receipts / apps / homebrew 10 / 80 (incl. Utilities) / 88 (with prefix)
registry (installed 7-Zip x64+x86 on the Windows guest) 2 — both hives, derived arch, MSI GUIDs

Plan 9 emits nothing. Remaining gap: appx is format-validated with live-verified DISM numeric shapes + unit-tested (populating it needs a signed MSIX — disproportionate for a secondary source).

Deep adversarial review (two multi-agent rounds, 10 lenses, per-finding verification)

Round 1 — 13 findings fixed: dpkg trigger-state packages kept; pkgsrc illumos/DragonFly secondaries and nix default-profile support (ADR compliance); apps Utilities globs (61→80 live); plutil corrupt-chunk bisection, multiline-string tracking, array roots; homebrew prefix identity (dual-brew duplicates); packages fact group; 6 schema description drifts.

Round 2 — 11 confirmed findings fixed (Windows ones verified live on the guest):

  • appx architecture: provisioned packages render DISM UInt32 (x64=9) not enum names — mapped, dedup restored.
  • OEM-codepage mojibake: redirected PowerShell stdout corrupted every non-ASCII DisplayName (CP437) — [Console]::OutputEncoding=UTF8.
  • exit-1 output loss: a failing final statement discarded all valid output (bare registry on 32-bit Windows; SYSTEM-context appx) — ;exit 0.
  • Windows-on-ARM: native-hive arch derived from PROCESSOR_ARCHITECTURE instead of hardcoded x64.
  • Registry delimiter: unit separator, immune to | in free-text values.
  • nix custom outputs (bind-…-dnsutils) collapse onto their base record; Determinate Nix (nix.enable=false) system set read via the default profile's nix-store.
  • YAML corruption: plain scalars Psych retypes are now quoted — 43_1→Integer 431, 0755→493, 2026-05-14→Date (which raises under safe_load); verified against Ruby Psych, full round-trip now string-clean.
  • plutil: scalar-root blocks; block-count-validated invocations with bisection contain any parser desync to the offending file.
  • CI hygiene: gating test no longer runs ~9 redundant full package probes.

Also fixed along the way: dpkg held packages, homebrew cask-only, apps/registry empty-version invariants, a plutil multi-line-value bug, nix digit-leading names, plutil ARG_MAX chunking, total-order record sort, resolver-gating test, and a path-vs-filepath Windows CI catch.

Deferred (documented): homebrew tap, nix store_path.

Implements OpenSpec change add-packages-fact.

ncode added 6 commits July 1, 2026 13:27
Adds the `packages` fact (ADR-0014): packages.<source> = an array of
{name, version, ...} records, one namespace per package database. Sources are
never merged, a source is omitted when its database is absent, and the whole
probe is a gated single-output resolver so `--disable packages` skips it.

Readers (one cheap read/query each, no spawn-per-package, no network):
- Linux: dpkg (install/hold state), rpm (epoch query, gpg-pubkey filtered,
  DB-presence-gated), pacman, apk, snap, flatpak, nix (NixOS system profile).
- BSD/illumos: pkg (FreeBSD+DragonFly, absolute /usr/local/sbin/pkg),
  openbsd_pkg, pkgsrc (PKG_DBDIR discovery), ips.
- macOS: receipts (primary), apps (secondary, never merged; plutil -p batch),
  homebrew (auto-detected).
- Windows: registry (both HKLM hives, product_code+architecture), appx.

Per-platform readers live in packages_{bsd,mac,win,extra}.go with no GOOS
suffixes or build constraints (ADR-0010), so the pure parse functions stay
cross-platform testable. Commands outside the engine's trusted PATH use
absolute paths (pkgng, nix-store).

Schema, supported-facts pages, README, man page, and CHANGELOG updated.

Validated on the nlab fleet / local darwin, counts vs native tools: dpkg 324,
rpm 386, pacman 183, apk 200, pkg freebsd 2 / dragonfly 48, openbsd_pkg 18,
pkgsrc 19, ips 418, nix 70, receipts 10 / apps 61 / homebrew 88 — all exact.
Windows registry/appx are format- and empty-case-validated (bare guest);
snap/flatpak are format-only (no populated guest).

Reviewed with OCR + Codex + self-adversarial; all findings fixed (dpkg held
packages, homebrew cask-only, apps/registry empty-version invariants, plutil
multi-line value bug, nix digit-leading names, plutil argv chunking, total-order
sort, resolver-gating test).
The macOS and BSD readers build always-unix paths (/Applications,
/var/db/pkg) but used path/filepath, whose separator is OS-specific. Because
these pure parse functions are tested on every platform (ADR-0010, no GOOS
suffix), the Windows CI runner produced backslash paths and failed
TestAppsPackages / TestOpenbsdPackages. Switch to the path package (always
'/'). Validated on the nlab Windows guest: the packages tests now PASS.
…idated

Populated the nlab ubuntu2404 guest (snap hello-world; flatpak org.vim.Vim
from flathub) and validated both readers end-to-end. Real data showed the
deferred flatpak `branch` field is load-bearing, not decorative: the same
application id (org.freedesktop.Platform.GL.default) installs twice with an
identical version and arch, distinguishable only by branch (25.08 vs
25.08-extra) — without it the reader emitted two indistinguishable duplicate
records. The reader now requests --columns=application,version,arch,branch
and emits branch as an identity field; versionless extensions (codecs-extra)
stay dropped by the name+version invariant.

Guest validation: facts.snap = 3 = snap list; facts.flatpak = 4 = versioned
flatpak rows; dpkg (740) coexists on the same host. Fixtures replaced with
verbatim guest output.
…rouping

Deep multi-lens review of the changeset surfaced 9 code defects and 4 doc
drifts (each independently triaged against the code and design docs):

- dpkg: keep trigger-state packages (install ok triggers-awaited/pending) —
  fully unpacked and configured; drop half-installed. Held already kept.
- groups: add the Facts-native "packages" fact group (ADR-0014's deliberate
  parity divergence) so --list-block-groups names it as one disable unit.
- pkgsrc: honor ADR-0014's "illumos/DragonFly secondary" — wired into both
  dispatches with the SmartOS dbdir candidates (/opt/local/pkgdb,
  /opt/local/pkg); inert on hosts without a pkgsrc db (omnios ips 418 and
  dragonfly pkg 48 unchanged, pkgsrc omitted).
- nix: honor ADR-0014's "default profile AND NixOS system profile" — fall back
  to nix-env -q --profile /nix/var/nix/profiles/default on non-NixOS hosts
  (validated against a real daemon install on ubuntu2404: 3 records incl. the
  nix-manual-2.34.7-man output-suffix case); gate opens on either profile;
  also wired on darwin per the design. NixOS still prefers the system profile
  without a fallback spawn.
- apps: scan the Utilities subfolders and match only *.app bundles — live
  darwin count 61 -> 80, exactly matching ls.
- plutil pipeline: bisect failed chunks so one corrupt plist no longer drops
  the whole receipts/apps source (O(log n) recovery, pairing per-invocation by
  construction); track multi-line string values so brace-looking lines inside
  them cannot desync block boundaries; handle array-rooted plists so pairing
  survives them.
- homebrew: add the prefix identity field — dual-prefix installs
  (/opt/homebrew + Rosetta /usr/local) previously yielded byte-identical
  duplicate records.
- schema: correct six descriptions to match behavior (dpkg states, appx
  provisioned+collector union, flatpak system+collector-user, openbsd_pkg
  architecture, homebrew prefix, nix profiles) and pkgsrc platforms.

ubuntu2404 now validates four coexisting sources end-to-end: dpkg 740,
snap 3, flatpak 4, nix 3.
…verified)

Windows (validated live on the lab guest before and after):
- Map Get-AppxProvisionedPackage's raw DISM UInt32 architecture (0/5/9/11/12)
  to x86/arm/x64/neutral/arm64 — provisioned lines rendered numbers, producing
  wrong arch values and defeating cross-view dedup on every populated host.
- Force [Console]::OutputEncoding=UTF8 in both scripts: redirected PowerShell
  stdout used the OEM codepage (CP437 turned o-umlaut into invalid UTF-8 and
  best-fit-mapped (R) to "r"), corrupting every non-ASCII DisplayName.
- Terminate both scripts with ;exit 0: an error in the final statement exits 1
  even under SilentlyContinue, and run() then discards all valid output (bare
  registry source on 32-bit Windows; lost provisioned lines under SYSTEM).
- Derive the native-hive architecture from PROCESSOR_ARCHITECTURE instead of
  hardcoding x64 (wrong for the whole native hive on Windows-on-ARM).
- Switch the registry delimiter to the unit separator so a '|' in free-text
  DisplayVersion/subkey values cannot shift the record columns.

nix:
- Collapse custom-output store paths (bind's dnsutils/host) onto their base
  record; a digitless version tail with no base sibling stays verbatim, so
  genuine 1.0-beta versions are never touched.
- Read the system set through the default profile's nix-store when
  nix.enable=false (Determinate) keeps nix-store out of the system env.

YAML formatter:
- Quote plain scalars a YAML 1.1 resolver retypes — verified against Ruby 3.4
  Psych: "43_1" parsed as Integer 431, "0x1F"/"0755" as hex/octal ints, and
  "2026-05-14" as Date (raising under safe_load). Package versions exercise
  every shape (homebrew revision/date versions, apps "36"); the whole packages
  tree now round-trips through Psych as strings.

plutil pipeline:
- Scalar-rooted plists emit an empty block so positional pairing advances.
- An invocation is committed only when its block count matches its path count;
  any mismatch (parser desync from content plutil prints ambiguously) is
  distrusted and bisected, containing damage to the offending file.

CI hygiene: the per-category gating test disables packages alongside each
other category, keeping ~9 redundant full package probes out of every run.
@ncode ncode merged commit 6d069ae into main Jul 2, 2026
37 checks passed
@ncode ncode deleted the juliano/packages-fact-impl branch July 2, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant