diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index 3f8d54c..d1b1489 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -51,6 +51,7 @@ finding.plausible-blocker-preexisting-kinds eureka-state:finding:cut-3a.s2.F4 ep finding.severity-medium eureka-state:finding:cut-3a.s2.F4 epiphany.pipeline.finding.v2 EpiphanyPipelineFindingDocument 918da863616d706169676eac657572656b612d7374617465a776657264696374be657572656b612d73746174653a766572646963743a6375742d33612e7332a56c6162656ca24634a572616e676582a462617365a83566393832323864a468656164a83566393832323864aa636f6e666964656e6365a9436f6e6669726d6564a87365766572697479a64d656469756da5636c61696dd9214120646f74746564206c6162656c20636f6d706f7365732074776f206b6579732eaa696e76617269616e747391ab6d696e642d61646d697473a96c6f636174696f6e739183a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509b06661696c7572655f7363656e6172696fbc54776f20646f63756d656e747320636c61696d206f6e65206b65792ea865766964656e63659183a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6baa707265636564656e747390a66f726967696eaa496e74726f6475636564 finding.severity-low eureka-state:finding:cut-3a.s2.F4 epiphany.pipeline.finding.v2 EpiphanyPipelineFindingDocument 918da863616d706169676eac657572656b612d7374617465a776657264696374be657572656b612d73746174653a766572646963743a6375742d33612e7332a56c6162656ca24634a572616e676582a462617365a83566393832323864a468656164a83566393832323864aa636f6e666964656e6365a9436f6e6669726d6564a87365766572697479a34c6f77a5636c61696dd9214120646f74746564206c6162656c20636f6d706f7365732074776f206b6579732eaa696e76617269616e747391ab6d696e642d61646d697473a96c6f636174696f6e739183a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509b06661696c7572655f7363656e6172696fbc54776f20646f63756d656e747320636c61696d206f6e65206b65792ea865766964656e63659183a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6baa707265636564656e747390a66f726967696eaa496e74726f6475636564 ruling.authority-mind eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273c0a663686f696365c0a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465c0a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e74739185a47265706fb147616d6543756c742f4165746865726961a6636f6d6d6974d92861616161616161616161616161616161616161616161616161616161616161616161616161616161a46b696e64a652756c696e67a26964b363756c7463616368653a72756c696e673a5231ae7061796c6f61645f736861323536d94062626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262a9617574686f72697479a44d696e64 +ruling.authority-steward eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273c0a663686f696365c0a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465c0a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e74739185a47265706fb147616d6543756c742f4165746865726961a6636f6d6d6974d92861616161616161616161616161616161616161616161616161616161616161616161616161616161a46b696e64a652756c696e67a26964b363756c7463616368653a72756c696e673a5231ae7061796c6f61645f736861323536d94062626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262a9617574686f7269747981a75374657761726484a773746577617264ae406865696d64616c6c3a63686174a47265706fb047616d6543756c742f426966726f7374a5776f726473b4726566757365206c656164696e67207a65726f73a56576656e74a424657631 cut_spec.reads-and-symbol eureka-state:cut_spec:cut-3a.r1 epiphany.pipeline.cut_spec.v2 EpiphanyPipelineCutSpecDocument 91de0013a863616d706169676eac657572656b612d7374617465a3637574a23361a87265766973696f6e01a57469746c65b2506970656c696e6520646f63756d656e7473a47265706fb147616d6543756c742f4570697068616e79a66272616e6368bb636f6465782f657572656b612d706970656c696e652d7374617465a462617365a83566393832323864aa646570656e64735f6f6e91a132a5666972737491ae526561642074686520737065632ea764656c657465739183a470617468a66f6c642e7273a56c696e657303a46e6f7465a464656164ab6b656570735f6d6f76657391ac636f6d6d6974206f776e6572a46164647391b5706970656c696e655f646f63756d656e74732e7273ac66696c655f6368616e6765739182a86c6f636174696f6e84a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509a673796d626f6cab76616c75655f7479706573a66368616e6765a3616464a572656164739282a86c6f636174696f6e84a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e650aa8656e645f6c696e6514a673796d626f6caf506970656c696e6543757453706563a3776879b274686520646f63756d656e7420736861706582a86c6f636174696f6e83a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e651ea8656e645f6c696e65c0a3776879a86f6e65206c696e65ad617574686f726974795f6d617087a56f776e6572a4636f7265a6696e7075747391af747970656420646f63756d656e7473a76f75747075747391a9656e76656c6f706573ad646572697665645f737461746591ac64657269766564206b657973b1666f7262696464656e5f7772697465727391a34d4350ac7368617265645f706174687391a961646d697373696f6ead64656c6574696f6e5f6c696e65a36e2f61ac766572696669636174696f6e84a66275696c647391ab636172676f20636865636ba574657374739182a46e616d65a46b657973a470696e73af6f6e652064657269766564206b6579a86e656761746976659182a77061747465726ea75665633c75383ea573636f7065a9646f63756d656e7473a86f70657261746f7291a46e6f6e65a8657374696d61746588ab6c696e65735f6164646564cd0384ad6c696e65735f72656d6f76656400b2646570656e64656e636965735f616464656491a8736368656d617273b4646570656e64656e636965735f72656d6f76656490ad666f726d6174735f616464656491b66570697068616e792e706970656c696e652e2a2e7631af666f726d6174735f72656d6f76656490ad746172676574735f616464656490af746172676574735f72656d6f76656490a772756c696e677391b6657572656b612d73746174653a72756c696e673a5238a97175657374696f6e7391b8657572656b612d73746174653a7175657374696f6e3a5131 finding.location-symbol eureka-state:finding:cut-3a.s2.F4 epiphany.pipeline.finding.v2 EpiphanyPipelineFindingDocument 918da863616d706169676eac657572656b612d7374617465a776657264696374be657572656b612d73746174653a766572646963743a6375742d33612e7332a56c6162656ca24634a572616e676582a462617365a83566393832323864a468656164a83566393832323864aa636f6e666964656e6365a9436f6e6669726d6564a87365766572697479a448696768a5636c61696dd9214120646f74746564206c6162656c20636f6d706f7365732074776f206b6579732eaa696e76617269616e747391ab6d696e642d61646d697473a96c6f636174696f6e739184a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509a673796d626f6cab2631313533303930343938b06661696c7572655f7363656e6172696fbc54776f20646f63756d656e747320636c61696d206f6e65206b65792ea865766964656e63659183a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6baa707265636564656e747390a66f726967696eaa496e74726f6475636564 target.repos eureka-state:target:r2 epiphany.pipeline.target.v2 EpiphanyPipelineTargetDocument 9187a863616d706169676eac657572656b612d7374617465a87265766973696f6e02aa696e76617269616e74739182a56c6162656cab6d696e642d61646d697473a973746174656d656e74b64f6e6c792061646d697373696f6e207772697465732eac6e6f745f696e5f73636f706591ac4576652062726f7773696e67b963616e6f6e6963616c5f696d706c656d656e746174696f6e7391a743756c744c6962a57265706f7392b147616d6543756c742f4570697068616e79af47616d6543756c742f487567696e6ea3646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 8dda40c..4541ae7 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -759,7 +759,6 @@ unit_enums! { ClaimOutcome { Holds, Falsified, Unproven } FindingConfidence { Confirmed, Plausible } FindingSeverity { Blocker, High, Medium, Low } - RulingAuthority { Operator, Standing, Defaulted, Mind } FindingOrigin { Introduced, PreExisting } RunOperator { Operator, Mind } RunTurn { PersonaTurn, SelfRun, Sleep } @@ -1127,6 +1126,55 @@ impl Bounded for ResearchSource { } } +/// Who decided a ruling, and under what. `Mind` is this mind's own decision, admitted by her Self. `Steward` is +/// a named peer's decision inside the peer's stewardship, as this mind heard it: `steward` is the deciding Mind's +/// Matrix id, `repo` the stewardship she decided under, `words` her own words copied from the line this mind heard, +/// `event` that line's Matrix event id when it was a delivery. The four unit variants stay bare strings on the wire. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)] +pub enum RulingAuthority { + Operator, + Standing, + Defaulted, + Mind, + Steward { steward: Short, repo: OrgRepo, words: Para, event: Option }, +} + +impl RulingAuthority { + /// The variants' spellings, in declaration order: the names the wire and listings use. + pub const NAMES: &'static [&'static str] = &["Operator", "Standing", "Defaulted", "Mind", "Steward"]; + + /// The variant's spelling, for rows, headers and listings. + pub fn name(&self) -> &'static str { + match self { + Self::Operator => "Operator", + Self::Standing => "Standing", + Self::Defaulted => "Defaulted", + Self::Mind => "Mind", + Self::Steward { .. } => "Steward", + } + } +} + +impl Bounded for RulingAuthority { + fn validate(&self, field: &str) -> Result<(), PipelineRefusal> { + match self { + Self::Steward { steward, repo, words, event } => { + steward.validate(&format!("{field}.steward"))?; + if steward.0.trim().is_empty() { + return Err(format_error(&format!("{field}.steward"), "a steward naming no Mind")); + } + repo.validate(&format!("{field}.repo"))?; + words.validate(&format!("{field}.words"))?; + if words.0.trim().is_empty() { + return Err(format_error(&format!("{field}.words"), "a steward ruling with no words")); + } + event.validate(&format!("{field}.event")) + } + _ => Ok(()), + } + } +} + /// How a subject was resolved. Every referent is a parsed `PipelineRef`, so a /// resolution names its records by ids of the kinds they declare, validated /// where every other referent is; a `Fixed` commit is a `Sha` whose referent @@ -1923,6 +1971,49 @@ mod tests { ruling } + fn steward_ruling() -> PipelineRuling { + let mut ruling = mind_ruling(); + ruling.authority = RulingAuthority::Steward { + steward: "@heimdall:chat".into(), + repo: OrgRepo("GameCult/Bifrost".into()), + words: "refuse leading zeros".into(), + event: Some("$ev1".into()), + }; + ruling + } + + #[test] + fn a_steward_ruling_round_trips_and_leaves_the_unit_authorities_bare_strings() -> Result<()> { + let cache = schema_cache()?; + let document = PipelineDocument::Ruling(steward_ruling()); + document.validate()?; + assert_eq!(PipelineDocument::decode(&document.prepare(&cache)?)?, document); + let json = serde_json::to_value(&document)?; + assert_eq!(json["value"]["authority"]["Steward"]["steward"], "@heimdall:chat"); + let mut unit = ruling_sample(); + unit.authority = RulingAuthority::Defaulted; + assert_eq!(serde_json::to_value(PipelineDocument::Ruling(unit))?["value"]["authority"], "Defaulted"); + Ok(()) + } + + #[test] + fn a_steward_ruling_refuses_blank_words_blank_steward_and_a_bad_repo() { + let refused = |edit: fn(&mut RulingAuthority)| { + let mut ruling = steward_ruling(); + edit(&mut ruling.authority); + PipelineDocument::Ruling(ruling).validate().expect_err("refused").to_string() + }; + let fields = [ + refused(|a| if let RulingAuthority::Steward { words, .. } = a { words.0 = " ".into() }), + refused(|a| if let RulingAuthority::Steward { steward, .. } = a { steward.0 = String::new() }), + refused(|a| if let RulingAuthority::Steward { repo, .. } = a { repo.0 = "nope".into() }), + refused(|a| if let RulingAuthority::Steward { words, .. } = a { words.0 = "w".repeat(4001) }), + ]; + for (fault, field) in fields.iter().zip(["authority.words", "authority.steward", "authority.repo", "authority.words"]) { + assert!(fault.contains(field), "{fault}"); + } + } + /// A withdrawal is a pulse that says only `tempo_vote_withdrawn`: it must survive /// encoding to an envelope, JSON and named MessagePack as a withdrawal, and an /// unwithdrawn pulse must not write the key. @@ -1967,7 +2058,7 @@ mod tests { #[test] fn unit_enum_names_are_the_variants_in_declaration_order() { - assert_eq!(RulingAuthority::NAMES, ["Operator", "Standing", "Defaulted", "Mind"]); + assert_eq!(RulingAuthority::NAMES, ["Operator", "Standing", "Defaulted", "Mind", "Steward"]); assert_eq!(FindingOrigin::NAMES, ["Introduced", "PreExisting"]); } @@ -2044,6 +2135,7 @@ mod tests { out.push((format!("finding.severity-{name}"), D::Finding(f))); } out.push(("ruling.authority-mind".into(), D::Ruling(mind_ruling()))); + out.push(("ruling.authority-steward".into(), D::Ruling(steward_ruling()))); let PipelineDocument::CutSpec(mut spec) = samples().remove(4).0 else { unreachable!() }; spec.file_changes[0].location.symbol = Some("value_types".into()); spec.reads = vec![ diff --git a/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json b/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json index f555a4e..2a816e5 100644 --- a/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json @@ -80,13 +80,56 @@ "type": "string" }, "RulingAuthority": { - "enum": [ - "Operator", - "Standing", - "Defaulted", - "Mind" - ], - "type": "string" + "description": "Who decided a ruling, and under what. `Mind` is this mind's own decision, admitted by her Self. `Steward` is\na named peer's decision inside the peer's stewardship, as this mind heard it: `steward` is the deciding Mind's\nMatrix id, `repo` the stewardship she decided under, `words` her own words copied from the line this mind heard,\n`event` that line's Matrix event id when it was a delivery. The four unit variants stay bare strings on the wire.", + "oneOf": [ + { + "enum": [ + "Operator", + "Standing", + "Defaulted", + "Mind" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "Steward": { + "properties": { + "event": { + "anyOf": [ + { + "$ref": "#/$defs/Short" + }, + { + "type": "null" + } + ] + }, + "repo": { + "$ref": "#/$defs/OrgRepo" + }, + "steward": { + "$ref": "#/$defs/Short" + }, + "words": { + "$ref": "#/$defs/Para" + } + }, + "required": [ + "steward", + "repo", + "words" + ], + "type": "object" + } + }, + "required": [ + "Steward" + ], + "type": "object" + } + ] }, "Sha256Hex": { "description": "A SHA-256 digest: exactly 64 lowercase hex characters.",