Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
abdf43a
feat(mint)!: key a receipt to the branch's change, so a code review c…
wenzowski Sep 6, 2026
b5b9c6f
chore(schema): regenerate for the mint columns and base-delta's patch id
wenzowski Sep 6, 2026
2b60797
fix(gate): repair the four defects the gate's own code review found
wenzowski Sep 6, 2026
26e19e7
test(gate): a map naming only another id is could-not-look, not a ref…
wenzowski Sep 6, 2026
01ee91c
style(mint): wrap the selector binding as rustfmt wants it
wenzowski Sep 6, 2026
c2a8251
fix(gate): repair the five defects the gate's second review found
wenzowski Sep 6, 2026
d9cf79d
style(mint): let the test module use expect, as its siblings do
wenzowski Sep 6, 2026
c10a2b3
fix(mint): repair the four correctness defects the third review found
wenzowski Sep 6, 2026
be51f5f
fix(mint): the null-result guard made the gate unclearable, and the t…
wenzowski Sep 6, 2026
b0e7aea
fix(mint): the guard added last round was unreachable, so it deleted …
wenzowski Sep 6, 2026
1a3b882
fix(policy): an unguarded base-delta faulted the whole bundle on a fr…
wenzowski Sep 6, 2026
677e101
feat(receipt): key a receipt to the branch's change, so a review can …
wenzowski Sep 6, 2026
8ac16c9
feat(policy): price the code review at the ready transition, not per …
wenzowski Sep 6, 2026
2e1d7ec
test(receipt): build the delta-keying fixture through the builder, no…
wenzowski Sep 6, 2026
506edc9
chore(waiver): suppress filed-here, whose class has no walkable route…
wenzowski Sep 6, 2026
87a9a11
fix(lint): a waiver may name a predicate id, which is the spelling th…
wenzowski Sep 6, 2026
14a3fc0
fix(receipt): resolve the delta identity from the repo root, not the cwd
wenzowski Sep 6, 2026
de6236f
perf(receipt): resolve the delta identity once per check, not three t…
wenzowski Sep 6, 2026
6b58475
docs(receipt): the delta arm's comment described the code before the …
wenzowski Sep 6, 2026
b3c2cc6
refactor(lint): extract the waiver diagnostics, which put smells over…
wenzowski Sep 6, 2026
6eb08e1
style(trust): the outcome census rationale carried collapsed-line whi…
wenzowski Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/rules/policy-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ path: a linked worktree may live anywhere on the machine, so its base is read to
decide `present` and dropped at the boundary — rule 4 held in the fact's TYPE, the
same way `commit-meta` has no body field.

**`base-delta`'s `patch-id` is the one member of that fact NOT computed the way
its siblings are, so it is worth the sentence they do not need** (CLOUD-1484). The
three path lists are a tip diff over the WORKING TREE; this is a merge-base diff
over COMMITTED bytes. The two therefore disagree on a stale branch and on a dirty
one, deliberately: a module asking which files this branch touched wants the
lists, and one asking whether a change was ATTESTED wants the identity, because an
attestation must not move when somebody saves a file and must not be restated by a
rebase — `land` rebases every lap, and a per-lap re-attestation is the cost that
gets a gate switched off. Its `null` is could-not-look AND the empty diff: a branch
that changed nothing has no identity, `cumulative_patch_id` refuses to mint one so
two empty changes cannot compare equal, and a predicate reading that absence as
_unattested_ refuses a checkout with nothing to review. Do not re-derive an
identity from the path lists — that is a second notion of _the same change_, free
to disagree with `landing`'s about exactly the rebase the key turns on.

**`records-blocked` is the recorder surface's could-not-look, and its EMPTY value
is an answer** (CLOUD-1126). `records` already distinguishes a record that could
not be read (absent from the map) from one that was read and is short; neither
Expand All @@ -190,7 +205,8 @@ and grouping them is worth a sentence because each answers a question no walk
can: `input.tree["base-delta"]` is how the declared globs' paths differ from a
declared base rev — `added`, `edited`, `deleted`, `code-changed` and the base side
of every EDITED path's lines, which is what lets a module decide a CHANGE rather
than a state (CLOUD-1059); `input.tree.symbols` is where a delegated analyser
than a state (CLOUD-1059), plus `patch-id`, the identity of the whole change;
`input.tree.symbols` is where a delegated analyser
resolved a named type, by NAME rather than by spelling, and carries the
`provenance` of the tool that produced it — the first `Cost::Effect` fact
(CLOUD-760); `input.tree.external` is a file outside the repository root, resolved
Expand Down
271 changes: 271 additions & 0 deletions batten.toml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion completions/batten.bash
Original file line number Diff line number Diff line change
Expand Up @@ -8394,7 +8394,7 @@ _batten() {
fi
case "${prev}" in
--key)
COMPREPLY=($(compgen -W "head branch named" -- "${cur}"))
COMPREPLY=($(compgen -W "head branch named delta" -- "${cur}"))
return 0
;;
--strictness)
Expand Down
3 changes: 2 additions & 1 deletion completions/batten.fish
Original file line number Diff line number Diff line change
Expand Up @@ -2557,7 +2557,8 @@ complete -c batten -n "__fish_batten_using_subcommand receipt; and __fish_seen_s
complete -c batten -n "__fish_batten_using_subcommand receipt; and __fish_seen_subcommand_from record" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c batten -n "__fish_batten_using_subcommand receipt; and __fish_seen_subcommand_from status" -l key -d 'Which git fact the receipt is judged against: the exact commit, or the branch' -r -f -a "head\t'Keyed to the exact commit; an amend, a rebase, or a moved trunk expires it'
branch\t'Keyed to the branch; every commit on it continues to serve the claim'
named\t'Keyed to a value the CALL names, read through [`Rule::key_from`] (CLOUD-987)'"
named\t'Keyed to a value the CALL names, read through [`Rule::key_from`] (CLOUD-987)'
delta\t'Keyed to the identity of the branch\'s whole CHANGE against [`Rule::key_base`] (CLOUD-1547) — the read side of [`crate::mint::MintKey::Delta`]'"
complete -c batten -n "__fish_batten_using_subcommand receipt; and __fish_seen_subcommand_from status" -l strictness -d 'Raise how strictly gates apply (an override may only tighten policy)' -r -f -a "permissive\t'Advisory: findings are reported without failing the run'
standard\t'The default: a finding is a violation'
strict\t'Everything `Standard` fails on, plus anything advisory'"
Expand Down
3 changes: 2 additions & 1 deletion completions/batten.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -4280,7 +4280,8 @@ trace\:"Add everything"))' \
_arguments "${_arguments_options[@]}" : \
'--key=[Which git fact the receipt is judged against\: the exact commit, or the branch]: :((head\:"Keyed to the exact commit; an amend, a rebase, or a moved trunk expires it"
branch\:"Keyed to the branch; every commit on it continues to serve the claim"
named\:"Keyed to a value the CALL names, read through \[\`Rule\:\:key_from\`\] (CLOUD-987)"))' \
named\:"Keyed to a value the CALL names, read through \[\`Rule\:\:key_from\`\] (CLOUD-987)"
delta\:"Keyed to the identity of the branch'\''s whole CHANGE against \[\`Rule\:\:key_base\`\] (CLOUD-1547) — the read side of \[\`crate\:\:mint\:\:MintKey\:\:Delta\`\]"))' \
'--strictness=[Raise how strictly gates apply (an override may only tighten policy)]: :((permissive\:"Advisory\: findings are reported without failing the run"
standard\:"The default\: a finding is a violation"
strict\:"Everything \`Standard\` fails on, plus anything advisory"))' \
Expand Down
1 change: 1 addition & 0 deletions crates/batten/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2980,6 +2980,7 @@ fn default_rules() -> Vec<Rule> {
when_present: None,
when_value: None,
key_from: None,
key_base: None,
key_shape: None,
max_age: None,
requires_field: None,
Expand Down
13 changes: 13 additions & 0 deletions crates/batten/src/facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1902,6 +1902,19 @@ impl Fact {
"type": "object",
"additionalProperties": {"type": "array", "items": {"type": "string"}},
},
// CLOUD-1484. The identity of the branch's whole CHANGE
// against the base, so a module can ask whether THIS change
// was attested without re-deriving one from the path lists —
// which would be a second notion of `the same change`, free
// to disagree with `landing`'s about a rebase. A MERGE-BASE
// diff over COMMITTED bytes, where the lists beside it are a
// tip diff over the working tree: the two disagree on a stale
// or dirty branch, deliberately, because an attestation must
// not move when somebody saves a file. `null` is
// could-not-look AND covers the empty diff — a branch that
// changed nothing has no identity, so a predicate must not
// read it as `unattested`.
"patch-id": {"type": ["string", "null"]},
},
"additionalProperties": false,
}),
Expand Down
97 changes: 96 additions & 1 deletion crates/batten/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2606,6 +2606,33 @@ pub struct BaseDelta {
/// must not collapse.
#[serde(rename = "base-lines")]
pub base_lines: BTreeMap<String, Vec<String>>,
/// The identity of the branch's whole CHANGE against the base
/// (CLOUD-1484) — [`branch_patch_id`], as a bare hex string.
///
/// # Why a receipt-shaped value belongs on this fact
///
/// A module asking *was this change reviewed* has to name the change, and a
/// digest is the only spelling of it that fits non-negotiable rule 4: the
/// three path lists above already say WHICH files moved, and a module that
/// re-derived an identity from them would be a second notion of *the same
/// change* — free to disagree with [`landing`]'s about a rebase, which is
/// the one property such a receipt turns on.
///
/// # It answers a DIFFERENT question from the lists beside it, deliberately
///
/// The path lists are a tip diff over the WORKING TREE. This is a merge-base
/// diff over COMMITTED bytes. They disagree on a stale branch and on a dirty
/// one, and that is not a defect in either: a module asking which files this
/// branch touched wants the first, and one asking which change was attested
/// wants the second, because an attestation must not move when somebody
/// saves a file.
///
/// `None` is could-not-look and covers the empty diff too — a branch that
/// changed nothing has no identity, and [`cumulative_patch_id`] refuses to
/// mint one so two empty changes cannot compare equal. A predicate must not
/// read that as *unreviewed*; there is nothing to review.
#[serde(rename = "patch-id")]
pub patch_id: Option<String>,
}

/// A file's content with its comment and blank lines removed.
Expand Down Expand Up @@ -2745,7 +2772,12 @@ pub fn materialize_rev(dir: &Path, rev: &str, dest: &Path) -> Result<()> {
/// # Errors
///
/// Raises only when the repository cannot be opened at all.
pub fn base_delta(dir: &Path, base: &str, globs: &[String]) -> Result<Option<BaseDelta>> {
pub fn base_delta(
dir: &Path,
base: &str,
globs: &[String],
wants_patch_id: bool,
) -> Result<Option<BaseDelta>> {
let repository = open(dir)?;
let hash = repository.object_hash();

Expand Down Expand Up @@ -2875,6 +2907,22 @@ pub fn base_delta(dir: &Path, base: &str, globs: &[String]) -> Result<Option<Bas
delta.edited.sort();
delta.deleted.sort();
delta.code_changed.sort();
// THE CHANGE'S OWN IDENTITY (CLOUD-1484), resolved here because this is the
// function that already holds the base. Failure leaves `None` — the field is
// could-not-look and the three path lists above are still answered, which is
// `base_date`'s posture one field down and for its reason: collapsing the
// whole fact over one unreadable field is the opposite error.
//
// PAID ONLY WHERE A ROW ASKED, which is the economy every other git fact in
// `resolve_git_facts` already keeps and which `mint_receipts` states one
// module over: a caller must not pay a git invocation for a question it
// never asks. This one is not cheap — a second repository open, a merge-base
// walk and a tree diff — and `base_delta` runs on every `check` that declares
// any `delta_sources`, which is most of them. Ungated it charged the whole
// tree for a field one module reads.
if wants_patch_id {
delta.patch_id = branch_patch_id(dir, base).ok().flatten();
}
// THE BASE'S OWN TIMESTAMP, resolved here because this is where the base rev
// has already been resolved. Every failure leaves `None` rather than a
// fabricated instant: a consumer comparing against could-not-look must skip
Expand Down Expand Up @@ -3248,6 +3296,53 @@ pub fn merge_base(dir: &Path, base_ref: &str) -> Result<Option<String>> {
.map(|found| found.detach().to_string()))
}

/// The patch identity of the branch's whole change against a declared base REF
/// (CLOUD-1484), as a bare hex string.
///
/// **A wrapper over [`cumulative_patch_id`] and deliberately not a second
/// computation.** What a review-dispatch receipt is keyed by has to be *the same
/// change* in exactly the sense the rest of this module already means it: line
/// numbers excluded, so a rebase onto a moved base still matches, and the merge
/// base used for RANGE SELECTION rather than as a merged-ness answer
/// (`no_ancestry_decides_merged_ness`). A digest computed here over a diff of
/// this function's own devising would be a second notion of *the same change*,
/// free to disagree with [`landing`]'s about a rebase — which is the one property
/// the receipt turns on.
///
/// `None` for every could-not-look: a base ref that does not resolve, a HEAD that
/// does not, two histories that share none, and — the one worth naming — an
/// **empty diff**. A branch that changed nothing has no identity, and
/// [`cumulative_patch_id`] refuses to mint one precisely so two empty changes
/// cannot compare equal. A caller must not read that `None` as *not reviewed*;
/// there is nothing to review.
///
/// The value is over COMMITTED bytes — `HEAD` against the merge base — so an
/// uncommitted edit cannot move it. That is what makes the key stable, and it is
/// also why a caller that cares whether the reviewed bytes are the ones that will
/// land must ask [`uncommitted`] separately: this function cannot see the working
/// tree at all.
///
/// # Errors
///
/// When the repository cannot be opened.
pub fn branch_patch_id(dir: &Path, base_ref: &str) -> Result<Option<String>> {
let Some(base) = resolve_ref(dir, base_ref)? else {
return Ok(None);
};
// `None` RATHER THAN `?`, so the doc above is true of the body. `head_commit`
// raises on an unresolvable HEAD — an empty repository, a broken ref — and
// propagating that would make this function raise where it promises
// could-not-look. Latent while both callers swallow the error, and a trap for
// the next one that follows the doc instead of reading the code.
let Ok(head) = head_commit(dir) else {
return Ok(None);
};
Ok(cumulative_patch_id(dir, &base, &head)
.ok()
.flatten()
.map(|id| id.as_str().to_owned()))
}

/// The patch identity of the branch's whole change: the diff from where the two
/// histories diverged to `head`.
///
Expand Down
48 changes: 48 additions & 0 deletions crates/batten/src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3451,6 +3451,47 @@ impl Policy {
bounds
}

/// The declared base ref, per check, for the `delta`-keyed rows this call
/// selects (CLOUD-1547).
///
/// [`Policy::field_bound_for`]'s twin, empty for the same reason: a
/// repository declaring no `delta`-keyed row resolves no patch identity, and
/// resolving one costs a merge-base and a diff on the hottest path in the
/// binary.
///
/// **First declaration wins where two rows disagree**, the same tie-break
/// `field_bound_for` takes and for its reason rather than `max_age`'s. Two
/// bases over one check are not two constraints that both hold — they are two
/// different identities for one change, so at most one receipt can exist and
/// combining them would make the check unsatisfiable. That is the config
/// error wearing a strict-policy costume, and declaration order is what every
/// other alternative on this surface breaks a tie by.
///
/// **`receipt_` IS IN THE NAME BECAUSE [`Policy::key_base_for`] ALREADY MEANS
/// SOMETHING ELSE**, and the collision is worth the prefix rather than a
/// shorter name. That one answers `requires_key`'s question — since which
/// commit should this call's evidence be looked for — and returns one ref for
/// the whole call. This one answers which ref a `delta` receipt's identity is
/// diffed against, per check. Two refs, two questions, and a reader who
/// reached the wrong one would get a plausible value and a wrong receipt.
#[must_use]
pub fn receipt_key_base_for(
&self,
envelope: &Envelope,
) -> std::collections::BTreeMap<String, String> {
let mut bases: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for rule in matching_receipt_rows(self, envelope) {
let Some(base) = rule.key_base.as_ref() else {
continue;
};
for check in rule.receipt_names() {
bases.entry(check.clone()).or_insert_with(|| base.clone());
}
}
bases
}

/// Whether any row on this call could read the pinned-program fact
/// (CLOUD-1028).
///
Expand Down Expand Up @@ -5185,6 +5226,12 @@ fn receipt_refusal(
ReceiptKey::Branch => "branch",
ReceiptKey::Named => "row",
ReceiptKey::Head => "commit",
// NOT "commit", which is the pointer this word exists to avoid sending.
// A delta receipt is keyed on the branch's whole change, so it survives a
// rebase and expires on a content edit — a reader told "commit" would go
// looking for a per-commit step and conclude the loop is re-buying one
// every lap, which is the opposite of what this keying does.
ReceiptKey::Delta => "change",
};
// THE BOUND TRAVELS TOO, and only on the class it is the measure for. A
// reader acting on an expiry needs to know what the age was measured
Expand Down Expand Up @@ -9703,6 +9750,7 @@ mod tests {
when_present: None,
when_value: None,
key_from: None,
key_base: None,
key_shape: None,
max_age: None,
requires_field: None,
Expand Down
Loading
Loading