git-ents.gitmain
⌘K
foforge
commit de2c1c6
effect: write results as a ResultRecord naming effect and commit

write_result now serializes a ResultRecord carrying the effect name and judged commit oid (model.result-identity), from which the gate recomputes the results refname; a signed pass can no longer be replayed for a different effect or commit. The query evaluator reads the status out of that record.

feat: write_result builds a ResultRecord (model.result-identity) refactor: read ResultRecord.status in the query evaluator Assisted-by: Claude:claude-opus-4-8

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/kernel/ents-effect/src/definition.rs @@ -33,6 +33,7 @@ /// use ents_model::Effect; /// /// let good = Effect { +/// name: "unit".into(), /// trigger: "rev(refs/heads/main)".into(), /// toolchains: vec!["rust-stable".into()], /// run: "cargo test".into(), @@ -65,6 +66,7 @@ fn effect(trigger: &str, toolchains: &[&str]) -> Effect { Effect { + name: "unit".to_owned(), trigger: trigger.to_owned(), toolchains: toolchains.iter().map(|s| (*s).to_owned()).collect(), run: "true".to_owned(),
crates/kernel/ents-effect/src/lib.rs @@ -99,6 +99,7 @@ //! // is the composition root's job (via `ents-kiln`), not this crate's — //! // `run_effect` only ever receives an already-materialized slice. //! let effect = Effect { +//! name: "unit".into(), //! trigger: "rev(refs/heads/main)".into(), //! toolchains: vec![], //! run: "true".into(),
crates/kernel/ents-effect/src/results.rs @@ -2,26 +2,33 @@ //! `effect.identity`): an ordinary [`ents_receive::receive`] client, never a //! privileged write outside the gate. //! -//! [`write_result`] builds the [`ents_model::Status`] typed tree, seals it -//! into a signed commit exactly the way [`ents_sync::resolve::merge_heads`] -//! seals a merge tip (`sign` is a caller-injected closure, so this crate -//! never holds key material — the composition root injects the worker's own -//! member key, `effect.identity`: "its result commit MUST be signed with -//! its own member key"), and hands the result to `receive` like any other -//! frontend. +//! [`write_result`] builds the [`ents_model::ResultRecord`] typed tree, +//! seals it into a signed commit exactly the way +//! [`ents_sync::resolve::merge_heads`] seals a merge tip (`sign` is a +//! caller-injected closure, so this crate never holds key material — the +//! composition root injects the worker's own member key, `effect.identity`: +//! "its result commit MUST be signed with its own member key"), and hands +//! the result to `receive` like any other frontend. -use ents_model::Status; -use ents_model::trailer::Trailers; +use ents_model::{ResultRecord, Status}; use ents_receive::{EventSink, Mode, Outcome, Proposal, RefTransition}; use gix::refs::FullName; +use gix_hash::ObjectId; use gix_object::{Commit, Find, Kind, Write, WriteTo as _}; use gix_ref_store::RefStore; use crate::error::{Error, Result}; -/// Build a signed commit recording `status` on `results_ref`, and push it -/// through [`ents_receive::receive`] — the sole path an effect's outcome -/// may re-enter the repository (`effect.results-writeback`). +/// Build a signed commit recording `status` for `effect` on the commit +/// `target` at `results_ref`, and push it through +/// [`ents_receive::receive`] — the sole path an effect's outcome may +/// re-enter the repository (`effect.results-writeback`). +/// +/// The tree is a [`ResultRecord`] carrying `effect` and `target` +/// (`model.result-identity`), from which the gate recomputes +/// `results_ref`'s `<effect>` and `<short-oid>` segments +/// (`gate.identity-binding`): a signed `pass` cannot be replayed as the +/// result of a different effect or commit. /// /// `results_ref` is the caller's choice: the canonical /// `refs/meta/results/<effect>/<short-oid>` for a designated worker, or the @@ -54,15 +61,16 @@ /// time: gix::date::Time { seconds: 1_000, offset: 0 }, /// }; /// +/// let target = gix_hash::ObjectId::from_hex(b"abc123456789000000000000000000000000abcd").expect("hex"); /// let name: gix::refs::FullName = /// "refs/meta/results/unit/abc123456789".try_into().expect("valid"); /// let outcome = write_result( -/// &refs, &objects, &NullEventSink, name, Status::Pass, &author, +/// &refs, &objects, &NullEventSink, name, "unit", target, Status::Pass, &author, /// |payload| key.sign(payload), Mode::Advisory, /// ).expect("evaluates"); /// assert_eq!(outcome.result, ents_receive::TxResult::Applied); /// ``` -// @relation(effect.results-writeback, effect.identity, effect.result-taxonomy, effect.self-run, scope=function) +// @relation(effect.results-writeback, effect.identity, effect.result-taxonomy, effect.self-run, model.result-identity, scope=function) #[expect( clippy::too_many_arguments, reason = "one input per commit-building step, mirrors ents_sync::resolve::merge_heads's shape" @@ -72,25 +80,24 @@ objects: &(impl Find + Write), events: &dyn EventSink, results_ref: FullName, + effect: &str, + target: ObjectId, status: Status, author: &gix::actor::Signature, sign: impl FnOnce(&[u8]) -> String, mode: Mode, ) -> Result<Outcome> { - let tree = facet_git_tree::serialize_into(&status, objects)?; + let record = ResultRecord::new(effect, target, status); + let tree = facet_git_tree::serialize_into(&record, objects)?; let old = refs.get(results_ref.as_ref())?; let parents: Vec<_> = old.into_iter().collect(); - let trailers = Trailers { - ents_ref: Some(results_ref.clone()), - schema_version: None, - }; let summary = match status { Status::Pass => "Record pass", Status::Fail => "Record fail", Status::Error => "Record error", }; - let message = format!("{summary}\n\n{}", trailers.render()); + let message = summary.to_owned(); let mut commit = Commit { tree, parents: parents.clone().into(), @@ -179,11 +186,15 @@ ); let name = namespace::result_ref("unit", "deadbeefcafe").expect("valid"); + let target = + ObjectId::from_hex(b"deadbeefcafe0000000000000000000000000000").expect("valid hex"); let outcome = write_result( &refs, &objects, &NullEventSink, name.clone(), + "unit", + target, status, &author(), |payload| worker.sign(payload), @@ -203,9 +214,11 @@ .expect("readable") .expect("present"); let commit = gix_object::CommitRef::from_bytes(data.data, tip.kind()).expect("decodes"); - let landed: Status = + let landed: ResultRecord = facet_git_tree::deserialize(&commit.tree(), &objects).expect("deserializes"); - assert_eq!(landed, status); + assert_eq!(landed.status, status); + assert_eq!(landed.effect, "unit"); + assert_eq!(landed.target(), target); } #[rstest] @@ -225,11 +238,15 @@ let name = namespace::self_result_ref(&ents_model::MemberId::new("bob"), "unit", "abc") .expect("valid"); + let target = + ObjectId::from_hex(b"abc0000000000000000000000000000000000000").expect("valid hex"); let outcome = write_result( &refs, &objects, &NullEventSink, name.clone(), + "unit", + target, Status::Fail, &author(), |payload| member.sign(payload),
crates/kernel/ents-effect/src/run.rs @@ -116,7 +116,7 @@ /// enroll_member(&refs, &objects, "worker", &worker, Provenance::AdminRegistered, 100); /// let commits = advance_ref(&refs, &objects, "refs/heads/main", 1, 200); /// -/// let effect = Effect { trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into() }; +/// let effect = Effect { name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into() }; /// let results_ref = namespace::result_ref("unit", "abcabcabcabc").expect("valid"); /// let author = gix::actor::Signature { /// name: "worker".into(), email: "worker@ents.test".into(), @@ -182,6 +182,8 @@ objects, events, results_ref, + &effect.name, + oid, status, author, sign, @@ -351,6 +353,7 @@ let commits = advance_ref(&refs, &objects, "refs/heads/main", 2, 200); let effect = Effect { + name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into(), @@ -405,6 +408,7 @@ let commits = advance_ref(&refs, &objects, "refs/heads/main", 3, 200); let effect = Effect { + name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into(), @@ -451,6 +455,7 @@ let commits = advance_ref(&refs, &objects, "refs/heads/main", 1, 200); let effect = Effect { + name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into(), @@ -498,6 +503,7 @@ advance_ref(&refs, &objects, "refs/heads/main", 2, 200); let effect = Effect { + name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into(), @@ -577,6 +583,7 @@ let first = *commits.first().expect("advance_ref produced a commit"); let effect = Effect { + name: "unit".into(), trigger: "rev(refs/heads/main)".into(), toolchains: vec![], run: "true".into(),
crates/kernel/ents-query/src/eval.rs @@ -34,7 +34,7 @@ use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet}; use std::rc::Rc; -use ents_model::Status; +use ents_model::{ResultRecord, Status}; use gix::refs::FullName; use gix_hash::ObjectId; use gix_object::{CommitRef, Find, Kind}; @@ -370,21 +370,24 @@ } /// The recorded status behind one results ref tip, cached: the tip - /// commit's tree deserialized as the [`Status`] typed tree. + /// commit's tree deserialized as a [`ResultRecord`], of which the + /// status is one field (`model.result-identity`: the tree also carries + /// the effect and judged commit, so a signed status means something + /// with the refname stripped away). fn result_status(&self, name: &str, tip: ObjectId) -> EvalResult<Status> { if let Some(status) = self.status.borrow().get(&tip) { return Ok(*status); } let tree = self.commit_tree(tip)?; - let status: Status = + let record: ResultRecord = facet_git_tree::deserialize(&tree, self.objects).map_err(|source| { EvalError::Status { name: name.to_owned(), source, } })?; - self.status.borrow_mut().insert(tip, status); - Ok(status) + self.status.borrow_mut().insert(tip, record.status); + Ok(record.status) } /// The tip commits of every author-written meta-ref matching the