3b increment 5a: pkgstate verification predicates - #7
Conversation
Add .verify(preview, reader) (R/verify.R): check every RESOLVED RECORD of
a committed preview against native dpkg/apt ground truth, per verb (4.7 /
6.3). The next increment wires this into .commit_session step 6.
transaction verbs (install/remove/purge/upgrade/dist_upgrade): each
record checked by ITS OWN action via pkgstate::dpkg_installed() --
install/upgrade/downgrade must be `installed` at to_version; remove
leaves config-files/not-installed/an absent row; purge must be absent
or not-installed (a surviving config-files is a FAILED purge, since
dpkg has no purged status word -- purge is the absence of a DB entry).
configure: each record's package must be fully `installed`.
hold/unhold: the pkgstate::dpkg_selections() want reads back (install
vs hold), matched by package name (hold records carry no arch).
update: no observable post-state -> verified NA.
Verification is INDEPENDENT of the helper's self-report (4.7): .verify
reads only the plan (preview) and the ground truth (reader), so a clean
status with a disagreeing post-state is a failure. It returns (verified
TRUE/FALSE/NA, detail) and NEVER raises.
The record grammar the 3a preview slice carried advisory-only is now
load-bearing, pinned to shipped pkgexec 0.0.3 (the five transaction
actions install/remove/purge/upgrade/downgrade; the configure/hold state
words) and pkgstate 0.0.1.9 (dpkg_installed `status` = the state word;
dpkg_selections `selection` = the want word).
The pkgstate reads go through an injectable reader seam
(pkgstate_reader()/set_pkgstate_reader), so the 50 new tests run against
canned data frames and never query dpkg. CI installs pkgstate (pure R,
public sibling).
Still deferred: wiring .verify() into .commit_session (capturing the
verdict into the outcome, never raising), then the exported apt_<verb>().
Two 5a review blockers.
1. Multi-arch hold now preserves target identity. .split_pkg_arch() splits
a hold record's `pkg:arch` package (hold records carry no separate
architecture field) and matches BOTH package and architecture against
dpkg_selections; an unqualified name still matches across all its rows
(every row must agree, conservative). Tests: qualified success/failure,
wrong-arch no-match, and two qualified targets on distinct arches.
2. .verify() is now genuinely non-raising on malformed reader/record data:
- .rec_str() returns NA for a non-list record instead of erroring on
`rec[[field]]`;
- the hold selection comparison is NA-safe (an NA selection is a
mismatch, not `if (NA)`);
- .require_cols() rejects a reader frame missing an expected column, and
an outer tryCatch in .verify() normalizes ANY residual error (a
throwing reader, a malformed frame) into verified = FALSE with a
detail -- a post-state pkgops cannot read is fail-closed, never a pass.
Regression tests: NA selection, non-list records, a column-short frame,
a throwing reader, and a hostile-input batch asserting .verify always
returns a well-formed (verified, detail) list.
|
Both blockers fixed in the latest commit.
|
…ildcard The pinned pkgexec 0.0.3 record grammar makes architecture a required field on transaction and configure records (digest.h). The verifier previously treated a missing architecture as a wildcard (is.na(arch) | inst$architecture == arch), which matched every arch row and inspected the first -- a missing/malformed arch could pass against an unrelated arch's post-state. Require architecture in both .verify_txn and .verify_configure (a missing or malformed arch is now a malformed record), and filter the installed frame by an exact package+architecture match with no wildcard fallback. Adds a multi-arch regression test: the same package on two arches is matched to its own arch row (not the first), and a record with no architecture fails as malformed.
|
Third 5a blocker fixed (2caeea0): architecture is required for transaction/configure records, never a wildcard. The pinned pkgexec 0.0.3 record grammar (digest.h) makes `architecture` a required field on txn and configure records. `.verify_txn` / `.verify_configure` previously matched a missing arch as `is.na(arch) | inst$architecture == arch` -- that wildcarded across every arch row and inspected the first, so a missing/malformed architecture could pass against an unrelated arch's post-state. Now:
Regression test added: the same package on two arches matches each record to its own arch row (i386 at the wrong version fails while amd64 passes), and a record with no `architecture` field fails as malformed for both txn and configure. 528 tests pass; `R CMD check` clean (only the expected New-submission / non-CRAN-deps WARNING). 5a is ready for review. |
The pkgstate-verification predicates (contract §4.7 / §6.3) — pure and hermetic,
held before they are wired into
.commit_session. Same cadence as the classifier→ orchestration and the polkit decision → wiring: build the checker first, wire it
next.
pkgstatebecomes anImportshere (the default reader uses it).What this adds
.verify(preview, reader)checks every resolved record of a committed previewagainst native dpkg/apt ground truth, per verb:
dpkg_installed()): install/upgrade/downgrade →installedatto_version; remove →config-files/not-installed/ absent; purge → absent /not-installed; a survivingconfig-filesis a failed purgeinstalled(dpkg_installed())dpkg_selections()want reads back (installvshold)verified = NAIndependent of the helper's self-report (§4.7):
.verifyreads only the plan(the preview) and the ground truth (the reader), so a clean status with a
disagreeing post-state is a verification failure. It returns
(verified TRUE/FALSE/NA, detail)and never raises.Record grammar is now load-bearing
The record grammar the 3a preview slice carried advisory-only is pinned here to
shipped pkgexec 0.0.3 (the five transaction actions
install/remove/purge/upgrade/downgrade— noconfigure/reinstallin txnrecords; the configure/hold state words) and pkgstate 0.0.1.9 (
dpkg_installedstatus= dpkg's state word verbatim;dpkg_selectionsselection= the wantword). Key semantic pinned from the source: dpkg has no "removed"/"purged"
status word — removed-with-configs is
config-files, purged is an absent row, sopurge-vs-remove is distinguished by row-presence +
config-files.Hermetic
The pkgstate reads go through an injectable reader seam
(
pkgstate_reader()/set_pkgstate_reader), so the 50 new tests (498 total) runagainst canned data frames and never query dpkg. CI installs
pkgstate(pure R,public sibling; the fake reader means dpkg is never touched on the runner).
Point for review
Multi-arch hold: hold records carry no
architecture, so hold/unhold is verifiedby package name only across all
dpkg_selectionsrows for that package (allmust agree — fail closed). Flagging the name-only match.
Deferred to later increments
.verify()into.commit_sessionstep 6: for a success status,verify and capture the verdict into the outcome's
verified/verify_detail— never raise; a verification failure still writes the outcome (§4.3 step 6).
This is also where the durable outcome record gains the
observed/changedpost-state fields.
apt_<verb>()API.CI hermetic;
R CMD checkclean (0 notes —pkgstateis used, no unused-ImportNOTE) but for the expected "New submission" WARNING.