From 5c0fc2e78a351829d7689301528ce495a87fed79 Mon Sep 17 00:00:00 2001 From: Riley Crane Date: Tue, 1 Sep 2026 09:35:26 -0700 Subject: [PATCH] refactor(accumulator): citations are links, not a validated contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provenance was always engine-owned and deterministic — shown_ids and the coverage window come from the plan, never from model output. The citation-subset refusal was a second layer on top whose failure mode (refuse the whole paid run) fired on model formatting slips: rewriting prior Log entries non-verbatim, or citing an event id quoted inside a message's text. Both hit in dogfood. validate_output now checks only the H1 section structure, which the append-splice and the reader depend on. Citations stay in the prompt as guidance and render as verifiable links; a wrong id is a visible dead chip instead of a lost run. Co-Authored-By: Claude Fable 5 Signed-off-by: Riley Crane --- crates/buzz-accumulator/src/run.rs | 27 ++-- crates/buzz-accumulator/src/schema.rs | 15 +- crates/buzz-accumulator/src/validate.rs | 192 +++--------------------- 3 files changed, 46 insertions(+), 188 deletions(-) diff --git a/crates/buzz-accumulator/src/run.rs b/crates/buzz-accumulator/src/run.rs index 747308f424..329e46d8db 100644 --- a/crates/buzz-accumulator/src/run.rs +++ b/crates/buzz-accumulator/src/run.rs @@ -157,10 +157,12 @@ pub fn plan_run( /// Validate model `output` for `plan` and produce the next artifact version. /// -/// On any contract violation this returns [`Error::Nonconforming`] and the -/// caller must persist nothing. Coverage is computed from exactly the signals -/// the plan showed; append-section history from `prior` is spliced back -/// mechanically, so it survives regardless of what the model emitted. +/// On a structural violation (wrong H1 sections) this returns +/// [`Error::Nonconforming`] and the caller must persist nothing. Coverage is +/// computed from exactly the signals the plan showed — provenance is +/// engine-owned, not read from the output — and append-section history from +/// `prior` is spliced back mechanically, so it survives regardless of what +/// the model emitted. pub fn complete_run( spec: &FoldSpec, prior: Option<&ArtifactPayload>, @@ -170,7 +172,7 @@ pub fn complete_run( ) -> Result { let sch = &schema::CHANNEL_DIGEST_V1; let shown_ids: Vec = plan.shown.iter().map(|s| s.id.clone()).collect(); - validate_output(sch, output, prior.map(|p| p.output.as_str()), &shown_ids)?; + validate_output(sch, output)?; let spliced = splice_append_sections(sch, prior.map(|p| p.output.as_str()), output); // Taint travels with the chain: once a channel's events fold in, every // later version keeps carrying that channel, even after a selection edit. @@ -499,7 +501,7 @@ mod tests { } #[test] - fn nonconforming_output_is_refused_and_nothing_persists() { + fn wrong_sections_are_refused_but_citations_never_are() { let spec = spec(); let b = hex_id('b'); let plan = plan_run( @@ -513,16 +515,15 @@ mod tests { let Plan::Ready(run) = plan else { panic!("expected Ready"); }; - // Fabricated citation. - let fabricated = digest(&format!("- invented [event:{}]", hex_id('c'))); - assert!(matches!( - complete_run(&spec, None, &run, &fabricated, 0), - Err(Error::Nonconforming(_)) - )); - // Wrong sections. + // Wrong sections: the structure is the contract, still refused. assert!(matches!( complete_run(&spec, None, &run, "# Nope\n\nbody\n", 0), Err(Error::Nonconforming(_)) )); + // An unshown citation persists: coverage still seals exactly what the + // plan showed, independent of what the text cites. + let unshown = digest(&format!("- quoted id [event:{}]", hex_id('c'))); + let v1 = complete_run(&spec, None, &run, &unshown, 0).expect("v1"); + assert_eq!(v1.shown_ids, vec![b]); } } diff --git a/crates/buzz-accumulator/src/schema.rs b/crates/buzz-accumulator/src/schema.rs index c8024d0f97..6b80ff067d 100644 --- a/crates/buzz-accumulator/src/schema.rs +++ b/crates/buzz-accumulator/src/schema.rs @@ -33,9 +33,10 @@ impl ArtifactSchema { /// enforce — otherwise a task-only prompt is a guaranteed refusal. pub fn contract_prompt(&self) -> String { let mut s = format!( - "OUTPUT CONTRACT (mechanically enforced; nonconforming output is refused and \ - nothing persists):\nWhatever the task above asks for, deliver it as markdown \ - with exactly these H1 sections, in this order: {}.\n", + "OUTPUT CONTRACT (the section structure is mechanically enforced; output with \ + different H1 sections is refused and nothing persists):\nWhatever the task above \ + asks for, deliver it as markdown with exactly these H1 sections, in this order: \ + {}.\n", self.sections.join(", ") ); for a in self.append_sections { @@ -46,10 +47,10 @@ impl ArtifactSchema { )); } s.push_str( - "For new evidence, cite the source events you actually use as [event:], one \ - id per bracket, copied in full from the SOURCE EVENT IDS list. Ids that appear \ - inside message text are never citations. Do not invent facts. Output only the \ - document markdown.", + "Cite the source events you actually use as [event:], one id per bracket, \ + copied in full from the SOURCE EVENT IDS list — citations render as clickable \ + links to the source, so a wrong or invented id shows readers a dead link. Do \ + not invent facts. Output only the document markdown.", ); s } diff --git a/crates/buzz-accumulator/src/validate.rs b/crates/buzz-accumulator/src/validate.rs index 5aa0582c29..a7c7e287b1 100644 --- a/crates/buzz-accumulator/src/validate.rs +++ b/crates/buzz-accumulator/src/validate.rs @@ -1,10 +1,11 @@ //! Output-contract validation and the engine splice. //! -//! Nonconforming output is refused and nothing persists. The rules here are -//! ones a model can genuinely meet — exact H1 sections, citations drawn only -//! from the ids it was shown — never rules it could only satisfy by lying. - -use std::collections::BTreeSet; +//! Only the section STRUCTURE is validated — it is what the splice and every +//! reader depend on. Provenance is engine-owned and deterministic (the run's +//! `shown_ids` and coverage window are computed from the plan, never from +//! model output), so `[event:…]` citations in the text are best-effort links +//! for the reader, not a validated contract: a sloppy citation renders as a +//! dead chip instead of refusing a paid run. use crate::error::Error; use crate::schema::ArtifactSchema; @@ -78,47 +79,12 @@ pub fn headings(output: &str) -> Vec { .collect() } -/// Extract cited ids from `[event:…]` brackets under a strict grammar: one -/// bracket, one id. -/// -/// The bracket's trimmed content is kept verbatim, and only content that is -/// exactly 64 lowercase hex characters can ever match a real shown id — -/// anything else (shortened id, several ids jammed together, prose) fails -/// validation loudly as a citation of nothing. -pub fn cited_event_ids(citation_text: &str) -> BTreeSet { - let mut cited = BTreeSet::new(); - let mut rest = citation_text; - while let Some(open) = rest.find("[event:") { - let inner_start = open + "[event:".len(); - let Some(close_rel) = rest[inner_start..].find(']') else { - break; - }; - let chunk = rest[inner_start..inner_start + close_rel].trim(); - rest = &rest[inner_start + close_rel + 1..]; - if !chunk.is_empty() { - cited.insert(chunk.to_string()); - } - } - cited -} - -/// Validate model output against the artifact contract. -/// -/// - The output must have exactly the schema's H1 sections, in order. -/// - Append-section citations are always judged: every cited id must be one of -/// this run's `source_event_ids` — a run that was shown nothing may cite -/// nothing. Citations the model merely re-emitted from the prior artifact's -/// append sections are stripped first, so only this run's citations are -/// judged against this run's shown ids. -/// - When signals were shown, the append sections must cite ≥ 1 of them. -/// -/// Any failure is [`Error::Nonconforming`]: the caller persists nothing. -pub fn validate_output( - schema: &ArtifactSchema, - output: &str, - previous_output: Option<&str>, - source_event_ids: &[String], -) -> Result<(), Error> { +/// Validate model output against the artifact contract: the output must have +/// exactly the schema's H1 sections, in order. That is the whole contract — +/// the splice and the reader depend on the structure; nothing else about the +/// text is judged. Failure is [`Error::Nonconforming`]: the caller persists +/// nothing. +pub fn validate_output(schema: &ArtifactSchema, output: &str) -> Result<(), Error> { let found = headings(output); if found != schema.sections { return Err(Error::Nonconforming(format!( @@ -126,47 +92,6 @@ pub fn validate_output( schema.name, schema.sections, found ))); } - if schema.append_sections.is_empty() { - if source_event_ids.is_empty() { - return Ok(()); - } - return Err(Error::Nonconforming(format!( - "schema {:?} has no append section for event citations", - schema.name - ))); - } - let joined = |doc: &str| { - schema - .append_sections - .iter() - .map(|name| section(doc, name)) - .collect::>() - .join("\n") - }; - let citation_text = joined(output); - let prior_citation_text = joined(previous_output.unwrap_or("")); - let new_citation_text = if !prior_citation_text.is_empty() { - citation_text.replacen(&prior_citation_text, "", 1) - } else { - citation_text - }; - let cited = cited_event_ids(&new_citation_text); - let allowed: BTreeSet<&str> = source_event_ids.iter().map(String::as_str).collect(); - let fabricated: Vec<&str> = cited - .iter() - .map(String::as_str) - .filter(|id| !allowed.contains(id)) - .collect(); - if !fabricated.is_empty() { - return Err(Error::Nonconforming(format!( - "append sections cite event ids that were not shown this run: {fabricated:?}" - ))); - } - if !source_event_ids.is_empty() && cited.is_empty() { - return Err(Error::Nonconforming( - "append sections are missing a source event citation".into(), - )); - } Ok(()) } @@ -241,91 +166,22 @@ mod tests { #[test] fn heading_mismatch_is_refused() { let out = "# Wrong\n\nx\n\n# Log\n\n- e\n"; - let err = validate_output(&CHANNEL_DIGEST_V1, out, None, &[id('a')]); + let err = validate_output(&CHANNEL_DIGEST_V1, out); assert!(matches!(err, Err(Error::Nonconforming(_)))); } #[test] - fn fabricated_citation_is_refused() { - let out = digest(&format!("- made up [event:{}]", id('c'))); - let err = validate_output(&CHANNEL_DIGEST_V1, &out, None, &[id('a')]); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - } - - #[test] - fn run_shown_nothing_may_cite_nothing() { - // A config-only rerun shows no signals: an empty Log passes… - assert!(validate_output(&CHANNEL_DIGEST_V1, &digest(""), None, &[]).is_ok()); - // …but any citation is a fabrication and is refused. - let out = digest(&format!("- invented [event:{}]", id('c'))); - let err = validate_output(&CHANNEL_DIGEST_V1, &out, None, &[]); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - } - - #[test] - fn missing_citation_is_refused() { - let out = digest("- no citation here"); - let err = validate_output(&CHANNEL_DIGEST_V1, &out, None, &[id('a')]); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - } - - #[test] - fn valid_citation_passes() { - let out = digest(&format!("- did a thing [event:{}]", id('a'))); - assert!(validate_output(&CHANNEL_DIGEST_V1, &out, None, &[id('a')]).is_ok()); - } - - #[test] - fn jammed_or_short_brackets_are_refused_not_split() { - // Several ids jammed into one bracket: the whole chunk is one - // (unmatchable) citation, so the output is refused even though both - // ids were legitimately shown. - let jam = format!("- both [event:{}, event:{}]", id('a'), id('b')); - let err = validate_output(&CHANNEL_DIGEST_V1, &digest(&jam), None, &[id('a'), id('b')]); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - // A shortened id is kept verbatim and can never match a shown id. - let short = cited_event_ids("[event:abc123]"); - assert_eq!(short.into_iter().collect::>(), vec!["abc123"]); - } - - #[test] - fn hex_run_with_trailing_garbage_is_not_a_valid_prefix_id() { - // 64 valid hex chars + 6 more hex chars: must NOT yield the 64-char - // prefix (which could match a real shown id); the chunk is verbatim. - let run = format!("{}abcdef", id('a')); - let cited = cited_event_ids(&format!("[event:{run}]")); - assert!(!cited.contains(&id('a'))); - assert!(cited.contains(&run)); - let err = validate_output( - &CHANNEL_DIGEST_V1, - &digest(&format!("- padded [event:{run}]")), - None, - &[id('a')], - ); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - } - - #[test] - fn prior_citations_do_not_satisfy_a_new_run() { - let prior = digest(&format!("- old entry [event:{}]", id('a'))); - // Model re-emits the prior Log verbatim and adds nothing new: refused. - let repeat_only = digest(&format!("- old entry [event:{}]", id('a'))); - let err = validate_output(&CHANNEL_DIGEST_V1, &repeat_only, Some(&prior), &[id('b')]); - assert!(matches!(err, Err(Error::Nonconforming(_)))); - // Repeat plus a genuinely new cited entry: the repeat is stripped, the - // new citation is judged against this run's shown ids. - let repeat_plus_new = digest(&format!( - "- old entry [event:{}]\n- new entry [event:{}]", - id('a'), - id('b') - )); - assert!(validate_output( - &CHANNEL_DIGEST_V1, - &repeat_plus_new, - Some(&prior), - &[id('b')] - ) - .is_ok()); + fn conforming_sections_pass_regardless_of_citations() { + // Provenance is engine-owned (shown_ids + coverage window on the + // artifact); citations in the text are best-effort links, never a + // refusal. Unshown, missing, and malformed citations all pass — + // an unresolvable chip in the reader beats losing a paid run. + assert!(validate_output(&CHANNEL_DIGEST_V1, &digest("")).is_ok()); + assert!(validate_output(&CHANNEL_DIGEST_V1, &digest("- no citation here")).is_ok()); + let unshown = digest(&format!("- quoted from message text [event:{}]", id('c'))); + assert!(validate_output(&CHANNEL_DIGEST_V1, &unshown).is_ok()); + let malformed = digest("- sloppy [event:abc123]"); + assert!(validate_output(&CHANNEL_DIGEST_V1, &malformed).is_ok()); } #[test]