Refuse a ref that is no ref, rather than answering over it
The pin moves to Epiphany `5cda0886`, where `PipelineRef::validate_ref` opens the leaf's grammar to a reader. `view` and `history`'s subject scope ask it before they look, so a ref whose kind and id disagree is refused instead of reporting the nothing a lookup finds. `query`'s `campaign`, `cut` and `repo` filters take a `Slug`, a `Label` and an `OrgRepo`, not refs, and are unchanged. The refusal is `MindRefusal::Document`, the leaf's `InvalidFormat` wrapped where the organ already wraps the leaf's four. A new variant would be a second name for one rule, and the grammar it names lives in the leaf. `a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors` pins both doors on a disagreeing kind and on a local no writer composes, and pins that absence is still absence: a well-formed id of a document this mind does not hold answers `None` and an empty history as it always did. The history test's malformed scope asserts the refusal where it asserted an empty answer. `V24` and `V24L` in `tools/eureka-cut9-mutations.psd1` are the validation dropped and the validation applied at `view` alone. `V10L` leaves the suite with its reason in the header: a subject compared by `id` alone can no longer differ from one compared whole, since both sides' kind is now a function of the id, so the weakening is unreachable and a mutation that cannot fail is not a proof. `cargo test -p huginn-mind --lib` 57 passed, 0 warnings; `cargo check --workspace` clean; Cut 8 and Cut 9 suites green-and-killed through the harness. The lock moves the one pin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
333e1c4314
commit
5ba7b0c6e5
4 changed files with 124 additions and 26 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -146,7 +146,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
|||
[[package]]
|
||||
name = "epiphany-pipeline"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/GameCult/Epiphany.git?rev=d5a36c2a291b910539072717d42d61a9aa7485d1#d5a36c2a291b910539072717d42d61a9aa7485d1"
|
||||
source = "git+https://github.com/GameCult/Epiphany.git?rev=5cda0886bd937b9df18309de7f505029d0e41f4d#5cda0886bd937b9df18309de7f505029d0e41f4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ publish.workspace = true
|
|||
anyhow = "1"
|
||||
chrono = "0.4.44"
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
|
||||
epiphany-pipeline = { git = "https://github.com/GameCult/Epiphany.git", rev = "d5a36c2a291b910539072717d42d61a9aa7485d1", package = "epiphany-pipeline" }
|
||||
epiphany-pipeline = { git = "https://github.com/GameCult/Epiphany.git", rev = "5cda0886bd937b9df18309de7f505029d0e41f4d", package = "epiphany-pipeline" }
|
||||
rmp-serde = "1"
|
||||
schemars = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -313,7 +313,15 @@ fn ordered(views: &mut [PipelineDocumentView]) {
|
|||
impl<S: MindStore> Mind<S> {
|
||||
/// One document with its admission facts and its derived status, or
|
||||
/// `None` when the mind does not hold it.
|
||||
///
|
||||
/// The ref is validated first, through the leaf's own door: a kind and an
|
||||
/// id that disagree name no document any writer could have composed, and
|
||||
/// answering `None` over one would report a malformed reference as an
|
||||
/// absent document. The refusal is the leaf's, wrapped: this crate owns no
|
||||
/// grammar, and a second name for `InvalidFormat` would be a second
|
||||
/// vocabulary for one rule.
|
||||
pub fn view(&self, id: &PipelineRef) -> Result<Option<PipelineDocumentView>, MindRefusal> {
|
||||
id.validate_ref()?;
|
||||
let reader = Reader::new(self)?;
|
||||
reader.held(id.kind, &id.id.0).map(|held| reader.view_of(held)).transpose()
|
||||
}
|
||||
|
|
@ -385,7 +393,15 @@ impl<S: MindStore> Mind<S> {
|
|||
/// Every record of one scope, withdrawn ones included with their status
|
||||
/// and their reasons, in the scope's own sequence order. Uncapped: a
|
||||
/// scope's history is bounded by the scope.
|
||||
///
|
||||
/// A subject scope is validated as `view`'s id is, and for the same
|
||||
/// reason: an empty history is the answer for a subject with no records,
|
||||
/// not for a ref that is no ref. A repo scope is an `OrgRepo`, which the
|
||||
/// type already holds to its own format.
|
||||
pub fn history(&self, scope: &HistoryScope) -> Result<Vec<PipelineDocumentView>, MindRefusal> {
|
||||
if let HistoryScope::Subject(subject) = scope {
|
||||
subject.validate_ref()?;
|
||||
}
|
||||
let reader = Reader::new(self)?;
|
||||
let (kind, mut records) = match scope {
|
||||
HistoryScope::Subject(subject) => (
|
||||
|
|
@ -429,7 +445,8 @@ mod tests {
|
|||
use crate::store::test_stores::MemoryStore;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use epiphany_pipeline::{
|
||||
ClaimOutcome, Date, FindingConfidence, PipelineDocument as D, PipelineKind as K, ResolutionOutcome,
|
||||
ClaimOutcome, Date, FindingConfidence, PipelineDocument as D, PipelineKind as K, PipelineRefusal,
|
||||
ResolutionOutcome,
|
||||
};
|
||||
|
||||
fn view(mind: &Mind<MemoryStore>, kind: K, key: &str) -> PipelineDocumentView {
|
||||
|
|
@ -440,6 +457,12 @@ mod tests {
|
|||
views.iter().map(|view| view.id.id.0.clone()).collect()
|
||||
}
|
||||
|
||||
/// The refusal a door answers for a ref whose kind is not its id's: the
|
||||
/// leaf's own `InvalidFormat`, at the field the leaf names it, wrapped.
|
||||
fn malformed(id: &str) -> MindRefusal {
|
||||
MindRefusal::Document(PipelineRefusal::InvalidFormat { field: "ref.id".into(), value: id.into() })
|
||||
}
|
||||
|
||||
/// A ruling that answers a question, which is what derives its
|
||||
/// `Answered` resolution.
|
||||
fn answering(label: &str, question: &str) -> D {
|
||||
|
|
@ -510,6 +533,38 @@ mod tests {
|
|||
assert_eq!(view(&mind, K::Verdict, &id("verdict", "cut-1.s1")).status, PipelineStatus::InForce);
|
||||
}
|
||||
|
||||
/// Ruling 1's read half: the grammar lives in the leaf, and both doors
|
||||
/// that take a ref ask it before they look. A ref that is no ref is
|
||||
/// refused; a ref that is well formed and names nothing is still the
|
||||
/// empty answer, so the door refuses malformation and not absence.
|
||||
#[test]
|
||||
fn a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors() {
|
||||
let mut mind = seeded();
|
||||
let q1 = id("question", "Q1");
|
||||
committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]));
|
||||
committed(admit(&mut mind, vec![answering("R1", &q1)]));
|
||||
|
||||
// The id names a question this mind holds and a resolution it derived;
|
||||
// read as a ruling it names neither, and is not a reference at all.
|
||||
let disagreeing = r(K::Ruling, &q1);
|
||||
assert_eq!(mind.view(&disagreeing), Err(malformed(&q1)));
|
||||
assert_eq!(mind.history(&HistoryScope::Subject(disagreeing)), Err(malformed(&q1)));
|
||||
|
||||
// A local no writer composes is the same refusal, so the door is the
|
||||
// whole grammar and not the kind segment alone. The refusal's `value`
|
||||
// is the failing part, here the empty part after the trailing dot,
|
||||
// and not the whole id.
|
||||
let dotted = r(K::Question, &format!("{q1}."));
|
||||
assert_eq!(mind.view(&dotted), Err(malformed("")));
|
||||
assert_eq!(mind.history(&HistoryScope::Subject(dotted)), Err(malformed("")));
|
||||
|
||||
// Absence is not malformation: a well-formed id of a kind this mind
|
||||
// does not hold answers as it always did.
|
||||
let absent = id("question", "Q9");
|
||||
assert_eq!(mind.view(&r(K::Question, &absent)), Ok(None));
|
||||
assert_eq!(mind.history(&HistoryScope::Subject(r(K::Question, &absent))), Ok(Vec::new()));
|
||||
}
|
||||
|
||||
/// R-B and D7: a subject's resolutions are a first-class view, withdrawn
|
||||
/// ones included with their status and their reasons, in sequence order.
|
||||
#[test]
|
||||
|
|
@ -552,13 +607,15 @@ mod tests {
|
|||
id("resolution", "resolution.question.Q1.n1.n1")
|
||||
]);
|
||||
|
||||
// The scope is the whole ref, kind included: a subject whose kind
|
||||
// disagrees with its id is a subject this mind has no resolution of,
|
||||
// even though the id names one with ten. The read side does not
|
||||
// validate the ref it is handed -- the leaf's validator is not public
|
||||
// at the pinned rev -- so a malformed scope is an empty history rather
|
||||
// than a typed refusal.
|
||||
assert!(mind.history(&HistoryScope::Subject(r(K::Ruling, &q1))).unwrap().is_empty());
|
||||
// A subject whose kind disagrees with its id is no subject at all, and
|
||||
// the door says so: the id names ten resolutions, and a mind that
|
||||
// answered an empty history over it would report a malformed reference
|
||||
// as a subject with no records.
|
||||
assert_eq!(
|
||||
mind.history(&HistoryScope::Subject(r(K::Ruling, &q1))),
|
||||
Err(malformed(&q1)),
|
||||
"a malformed scope is refused, not answered over"
|
||||
);
|
||||
|
||||
// History lands one record per batch (Cut 12's import constraint), so
|
||||
// ten records carry ten receipts.
|
||||
|
|
|
|||
|
|
@ -15,23 +15,26 @@
|
|||
# breaks it for the views. V1L is H41's own edit, listed here as well as in
|
||||
# `eureka-cut8-mutations.psd1` -- one mutant, two suites, one owner.
|
||||
#
|
||||
# One weakening the spec names has no entry because it changes no behaviour
|
||||
# Two weakenings the spec names have no entry because they change no behaviour
|
||||
# this Body can reach, and a mutation that cannot fail is not a proof:
|
||||
#
|
||||
# - `assignments_of` comparing the repo and ignoring the instance. A5 refuses
|
||||
# a stewardship naming another instance, so every stewardship a mind holds
|
||||
# already names that mind, and every other caller is a sequence this mind
|
||||
# derives over its own scope.
|
||||
# - `resolutions_of` comparing a subject by `id` alone, which was `V10L` until
|
||||
# the leaf opened `PipelineRef::validate_ref` and `view` and `history` began
|
||||
# asking it. A ref that reaches the comparison now has a kind its id's kind
|
||||
# segment agrees with, and every stored subject was held to the same grammar
|
||||
# at admission, so on both sides the kind is a function of the id and
|
||||
# comparing the whole ref and comparing the id alone cannot differ. The rule
|
||||
# is still the rule; it is the reachable weakening that went, and what
|
||||
# defends it is `V24`/`V24L` at the doors instead.
|
||||
#
|
||||
# The two listed beside it in the spec are failable and have entries. The
|
||||
# The one listed beside them in the spec is failable and has an entry: the
|
||||
# `semantic` check below `Reader::new` changes the answer for a store the
|
||||
# reader refuses -- a doubled receipt, a row that does not decode -- from the
|
||||
# semantic refusal to the integrity one, and `V8L` is that move. And
|
||||
# `resolutions_of` comparing a subject by `id` alone is reachable because the
|
||||
# read side never validates the `PipelineRef` it is handed: the leaf's
|
||||
# validator is not public at the pinned rev, so `history(Subject(ref))` and
|
||||
# `view(ref)` take a ref whose kind and id disagree and answer over it. `V10L`
|
||||
# is that comparison.
|
||||
# semantic refusal to the integrity one, and `V8L` is that move.
|
||||
@{
|
||||
Mutations = @(
|
||||
@{
|
||||
|
|
@ -229,14 +232,6 @@
|
|||
.map(|(key, resolution)| { let _ = subject; (resolution.sequence, key.to_string()) })
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'V10L'
|
||||
Rule = 'A subject is selected by the whole ref, kind included: the read side validates no ref it is handed, so a kind that disagrees with its id selects nothing rather than that id''s records.'
|
||||
Test = 'query::tests::a_subjects_history_lists_every_resolution_with_its_status_and_receipt'
|
||||
File = 'crates/huginn-mind/src/docs.rs'
|
||||
Old = ' self.resolutions().filter(move |(_, resolution)| resolution.subject == *subject)'
|
||||
New = ' self.resolutions().filter(move |(_, resolution)| resolution.subject.id == subject.id)'
|
||||
}
|
||||
@{
|
||||
Id = 'V11'
|
||||
Rule = 'A repo''s history is selected by (this mind, this repo), so another repo''s assignments are another history.'
|
||||
|
|
@ -480,5 +475,51 @@
|
|||
Old = ' D::Campaign(campaign) => campaign.repos.contains(repo),'
|
||||
New = ' D::Campaign(_) => false,'
|
||||
}
|
||||
@{
|
||||
Id = 'V24'
|
||||
Rule = 'Every door that takes a ref asks the leaf''s grammar first: a ref whose kind and id disagree is refused, never answered over as an absent document or an empty history.'
|
||||
Test = 'query::tests::a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors'
|
||||
Edits = @(
|
||||
@{
|
||||
File = 'crates/huginn-mind/src/query.rs'
|
||||
Old = @'
|
||||
id.validate_ref()?;
|
||||
let reader = Reader::new(self)?;
|
||||
'@
|
||||
New = @'
|
||||
let reader = Reader::new(self)?;
|
||||
'@
|
||||
}
|
||||
@{
|
||||
File = 'crates/huginn-mind/src/query.rs'
|
||||
Old = @'
|
||||
if let HistoryScope::Subject(subject) = scope {
|
||||
subject.validate_ref()?;
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
if let HistoryScope::Subject(subject) = scope {
|
||||
let _ = subject;
|
||||
}
|
||||
'@
|
||||
}
|
||||
)
|
||||
}
|
||||
@{
|
||||
Id = 'V24L'
|
||||
Rule = 'Both doors ask, not one: a mind that validates the ref it views and not the subject it takes a history of still answers an empty history over a ref that is no ref.'
|
||||
Test = 'query::tests::a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors'
|
||||
File = 'crates/huginn-mind/src/query.rs'
|
||||
Old = @'
|
||||
if let HistoryScope::Subject(subject) = scope {
|
||||
subject.validate_ref()?;
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
if let HistoryScope::Subject(subject) = scope {
|
||||
let _ = subject;
|
||||
}
|
||||
'@
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue