Skip to content

3b: export the nine per-verb apt_<verb>() commit API - #9

Merged
TroyHernandez merged 10 commits into
masterfrom
commit-api
Aug 20, 2026
Merged

TroyHernandez merged 10 commits into
masterfrom
commit-api

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

The public API that completes the commit lifecycle -- the increment that makes pkgops mutation-capable. Each `apt_(preview, ...)` commits the `pkgops_preview` its `apt__preview()` twin produced.

What lands

  • Nine exported entrypoints: `apt_install`, `apt_remove`, `apt_purge`, `apt_hold`, `apt_unhold`, `apt_update`, `apt_upgrade`, `apt_dist_upgrade`, `apt_configure` (`R/commit_api.R`).
  • `.commit_verb(expected_verb, preview, ...)` (`R/commit.R`) adds the two checks the verb-agnostic `.commit_session` cannot make, before anything is opened:
    1. the argument is a `pkgops_preview`;
    2. its verb is the one this function commits (`apt_install()` refuses an `apt.remove` preview -- a verb/preview mismatch).
      Then it delegates to the already-reviewed `.commit_session` (committability gate, capability, polkit, open, commit, verify, write-outcome, signal).

Design points for review

  • Integrity authority stays the plan_hash. A commit binds only the preview it is handed; the privileged helper re-validates that `plan_hash` under the `dpkg` lock, so a drifted plan is refused there, never applied. pkgops does not re-derive the hash at the R layer (the preview/commit split is the whole point).
  • `interactive` defaults to `interactive()`. An R console commits through the `pkexec` prompt; a script / CI run commits in machine mode (a non-interactive `pkcheck`, whose denial or approval challenge is a durably-audited refusal, never a prompt). This is "mode detection at the CLI layer" from the plan; the default uses `base::interactive()` to avoid the self-reference footgun. If you would rather the default be an explicit `FALSE`, that is a one-line change.
  • DESCRIPTION updated: it no longer says mutation is out of scope.

Scope boundary

Deferred to the VM-gated increment: the durable outcome-record grammar (the `observed`/`changed` post-state fields), the plain-intent refusal record grammar, and the exact pkcheck rc->outcome split, all pinned against a real broker/polkit. The combined `apt__run()` (plan+commit) convenience is optional and not built here. Then slice 4 (`rctl` apt.*).

Verification

  • 642 tests pass (89 new in `test_commit_api.R`: the nine-verb commit table, verb/preview mismatch refusals with nothing opened, non-preview + non-ok refusals, parameter pass-through, both authorization modes, and an exported-surface assertion).
  • R/ ASCII-clean; `R CMD check` 0 errors, 0 notes, 1 expected New-submission / non-CRAN-deps WARNING.
  • Version bumped to 0.0.1.8 (separate commit); NEWS + CLAUDE.md updated.

Held as a draft for review.

The exported public API that completes the commit lifecycle. Each
apt_<verb>(preview, ...) takes the pkgops_preview its apt_<verb>_preview()
twin produced and drives .commit_session for it, returning a
pkgops_outcome or signaling a typed condition. This is the increment that
makes pkgops mutation-capable.

.commit_verb() adds the two checks the verb-agnostic .commit_session
cannot make -- the argument is a pkgops_preview, and its verb is the one
this function commits (apt_install() refuses an apt.remove preview: a
verb/preview mismatch) -- both before any capability call or intent, then
delegates. The plan_hash the preview carries stays the integrity
authority (the helper re-validates it under the dpkg lock); pkgops does
not re-derive it at the R layer.

interactive defaults to interactive(): an R console commits through the
pkexec prompt, a script or CI run commits in machine mode (pkcheck).
lock_timeout/deadline_ms/socket_path pass through to the session.

Updates the DESCRIPTION: mutation is no longer out of scope. Hermetic
tests cover the verb match, the nine-verb surface, the non-ok/non-preview
refusals, parameter pass-through, and both authorization modes.
# Conflicts:
#	CLAUDE.md
#	NEWS.md
A left-open intent exists precisely so it can be reconciled later, which needs its
correlation_id. But a commit that RAISED (a mid-flight kill -- the G-INT case) was
re-signaled as the raw runix condition with NO cid: the cid lived only on the
outcome object .commit_and_classify discards. A lost-result effect_unknown whose
delivered frame dropped its correlation_id had the same gap.

.ensure_cid(cond, cid) attaches the session correlation_id to a condition that
reaches the caller on a left-open / effect-unknown path, PRESERVING its class and
every structured field and ADDING the cid only when the condition does not already
carry a usable one (never overwriting a cid a lower layer set). Applied in
.commit_and_classify to both the raised-commit condition and any classified
condition, so every open intent the caller sees is reconcilable.

The classified helper-refusal conditions already carried commit$correlation_id, so
this is a no-op for them; it fills the two gaps (raised commit, lost cid). 756
tinytest.
…und)

.ensure_cid now stamps the session correlation_id whenever the condition's
existing cid is not ALREADY a well-formed broker cid (reusing .valid_broker_cid),
so an empty, NA, malformed, or non-scalar value is replaced -- not just a missing
or NA one. A valid cid a lower layer set correctly is still kept, and a
non-well-formed session cid is never stamped (no replacing one bad cid with
another). This keeps a left-open / effect-unknown intent (G-INT) reconcilable
even when the raw condition carried a garbage cid.

Regression tests: a direct .ensure_cid unit test (empty/malformed/non-scalar
replaced, valid kept, class+message preserved) and a G-INT end-to-end case
(a raised commit whose condition carries "", a malformed cid, or NA still leaves
a reconcilable open intent). 778 tests pass.

CLAUDE.md: correct the stale "34 gates via pkgops" Part B wording to describe
membership (every functional gate via pkgops; G12-G14 + G15 via rab-exercise;
G11a/G11b via direct pkexec; G9/G-OWN are pkgops preview-side refusals).
@TroyHernandez

Copy link
Copy Markdown
Contributor Author

Part B authoritative run 4: 68/68. Proven via an ephemeral integration branch = this branch + #11's coarse-outcome fix cherry-picked (788 tinytest green, VM 68/68). To land after #11: rebase onto master, bump 0.0.1.9 -> 0.0.1.10, then squash-merge.

Brings the merged coarse-`outcome` conformance fix (0.0.1.9) under the exported
apt_<verb>() commit API. R/commit.R + test_commit.R auto-merged (the outcome-record
and mutation-API changes are disjoint); DESCRIPTION/NEWS/CLAUDE.md reconciled by
hand. Bump to 0.0.1.10. 788 tinytest green.
@TroyHernandez
TroyHernandez marked this pull request as ready for review August 20, 2026 18:30
@TroyHernandez
TroyHernandez merged commit 8fca2ab into master Aug 20, 2026
@TroyHernandez
TroyHernandez deleted the commit-api branch August 20, 2026 18:30
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