Skip to content

Batch acceptance invocations over enumerated, manifest-pinned selections - #42

Merged
Hafeok merged 1 commit into
mainfrom
claude/ledger-accept-group-njrtpz
Aug 13, 2026
Merged

Batch acceptance invocations over enumerated, manifest-pinned selections#42
Hafeok merged 1 commit into
mainfrom
claude/ledger-accept-group-njrtpz

Conversation

@Hafeok

@Hafeok Hafeok commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Implement ledger accept --group|--set to batch the invocation over a selection while preserving the binding semantics of acceptance. Each decision in the selection receives its own acceptance record signing its own version hash, exactly as a single accept would produce.

Summary

This change adds batched acceptance as an ergonomic affordance for signing multiple decisions in one pass. The key insight is that batching the act (filing multiple acceptances together) is different from batching the signature (one record covering multiple decisions). This implementation preserves the latter's semantics: every acceptance is scoped to its own version hash, and the selection is pinned by a manifest digest that must match between the read (dry run) and the write (confirmation).

Key Changes

  • New batch module (ledger-core/src/batch.rs): Core abstraction for enumerated, pinned selections

    • Plan struct: enumerated members with their standing (signable, held, already-signed, forked) and a manifest digest
    • Outcome struct: result of an invocation (dry run or signed, with optional refusal)
    • Member struct: one decision with its version hash, state, and standing
    • manifest() function: domain-separated digest over (selector, actor, members) triples including standing
  • New batch_drift module (ledger-core/src/batch_drift.rs): Drift detection and recovery

    • locate(): recovers the selection a manifest was taken over by replaying log prefixes
    • changes(): names what moved between two selections (entries added/removed/revised/restated)
    • Enables refusals to say which decision changed, not just "the selection moved"
  • New batch_render module (ledger-core/src/batch_render.rs): Terminal rendering

    • render(): formats the enumeration, tally, and verdict as a pasteable block
    • Separates layout from the Outcome struct so --json and text cannot drift
  • New hold module (ledger-core/src/hold.rs): Held entries that block batches

    • Hold struct: a basis pointer in the indeterminate: scheme and the reason it blocks
    • hold(): derives holds from based_on pointers, not hand-listed
    • A held member stops the whole batch by name, so the judgment stays recorded
  • New group_accept module (ledger-core/src/author/group_accept.rs): Batched acceptance logic

    • accept_group(): enumerates the selection, pins it with a manifest, and optionally signs it
    • Default posture: dry run (enumerate, print, write nothing)
    • Confirmation posture: --confirm <manifest> signs only if the store still enumerates to that manifest
    • Per-member gating: each acceptance is gated individually, so a member that would fail as a single accept fails the batch with the same class and message
    • Three refusals: held/forked members, manifest drift, and gate violations
  • CLI adapter (ledger-cli/src/commands/sign.rs): Two shapes of accept over one meaning

    • AcceptFlags struct: decision (single) vs. set/group (selection) vs. confirm (manifest)
    • Naming a decision signs it immediately (existing behavior)
    • Naming a selection enumerates it, prints it, writes nothing (dry run)
    • --confirm <manifest> signs the selection if it still matches that manifest
    • --json output for both shapes
  • Selector canonicalization (ledger-core/src/show.rs): Selector::to_string() for manifest digests

    • Canonical spelling so selectors can go inside manifests without a second serialization
  • Comprehensive test suite (ledger-cli/tests/accept_group.rs): 380 lines of end-to-end scenarios

    • Batching files one acceptance record per decision, each on its own hash
    • Dry run is the default posture and writes nothing
    • Confirmation without a

https://claude.ai/code/session_01FwsbsqP5nqVPjkjZFzmH7b

`ledger accept --group|--set` takes a selection `ledger show` already
resolves and signs it in one invocation. It files N acceptance records,
one per decision, each on its own version hash under `scope: version` —
never one record covering N decisions. What is batched is the
invocation; the binding is untouched.

The plan is built from `show`'s screens rather than a second traversal,
so `show --group X` and `accept --group X` cannot resolve to different
sets — asserted by test, but structural first. The selection is
enumerated once into explicit `(decision, hash, standing)` triples and
pinned by a manifest digest under its own domain-separation prefix; a
dry run is the default posture and prints it; signing requires
`--confirm <manifest>`, a value only a dry run can produce, so
confirm-without-a-prior-read has no spelling.

Standing is inside the digest, not just the pairs: an acceptance filed
between the read and the run changes what the run would *do* without
changing which decisions it covers, and a signature must not absorb
that silently. A mismatching run refuses and names what moved —
recovered by replaying the append-only log's own prefixes until one
hashes to the confirmed manifest, so nothing is cached between the two
invocations and nothing can go stale.

Held is derived from the record, never hand-listed: a basis pointer in
the `indeterminate:` scheme says the ground is unsettled, and such a
member stops the whole run by name rather than being signed around. On
this repo's store that is `dec/ddd/interceptor-not-extension`, caught
without the worksheet having to say so. Forked chains block the same
way; an entry the actor already signed is enumerated and labelled, not
dropped.

No batch-only leniency: each acceptance runs the same delta-gate a
single accept runs — once alone so a failure names its decision, once
combined so the write is checked as the act it is. `refusal_check`
gained a baseline-supplied variant so the N+1 passes reuse one
baseline; the rule and the comparison stay in one copy. Identity and
committer semantics are unchanged: git config, no `--as`, L009 on
every record. No format change, no new gate class, no `--all-pending`,
no auto-confirm, and the L1 precommitment tension is left exactly where
it stands — this verb signs present members, enumerated at run time.

The ruling is filed as a `ledger-design` entry
(`dec:hafeok.ledger/01KZXJX693301CZSY4XNP643XY`), allocated criterion at
pr stage against the tests that check it, awaiting the principal's
acceptance. No acceptance is filed by this session. The PRD and the
acceptance worksheet record the verb as the second interim affordance
standing in for L5's workbench, next to the PR-review surface, so it
does not quietly become the answer.

Gates green: cargo t (46 binaries), the documented clippy gate, ddd
validate (240 entries), ledger verify, cargo xtask check (0 errors),
ddd diff-contracts over HEAD (12 events, 0 undischarged).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwsbsqP5nqVPjkjZFzmH7b
@Hafeok
Hafeok merged commit d506ac9 into main Aug 13, 2026
5 of 6 checks passed
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.

2 participants