refactor(accumulator): the response IS the artifact — free-form output, transparent runs

The artifact model now matches the mental model: instructions + context →
model → response, persisted verbatim. Any shape the task asks for.

- Delete schema.rs and validate.rs entirely: no output contract, no splice,
  no refusal path. complete_run is infallible; Error::Nonconforming and
  RunOutcome::Refused are gone. Provenance (shown_ids, coverage, truncated)
  stays engine-computed from the plan, never parsed from output.
- spec: schema becomes a provenance label (freeform@v1 default; old chains
  keep channel-digest@v1, no migration). New DEFAULT_INSTRUCTIONS.
- run: model_input = TASK (user instructions) + short engine guidance
  (context explained, citations render as links) + CONTEXT; prior version
  rides as '--- PRIOR VERSION (name vN) ---'.
- preflight gains include_input: the Ready plan can carry the exact string
  the model would receive — the behind-the-curtain seam.
- UI: fold cards always show their instructions; the plan box explains
  incremental coverage and offers the exact model input behind a
  disclosure; artifact header explains what 'covers' means (this run's new
  events) and that versions build on the prior; refused branch removed.

Gates: cargo test -p buzz-accumulator 67/67, clippy -D warnings, fmt;
ui tsc --noEmit + vite build clean. Live-verified on scratch daemon :4641
(preflight include_input round-trip; legacy channel-digest@v1 artifact
still loads).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
This commit is contained in:
Riley Crane
2026-09-01 12:14:41 -07:00
committed by Sol
parent 5c0fc2e78a
commit 0722066d73
11 changed files with 202 additions and 475 deletions

View File

@@ -28,14 +28,6 @@ pub enum RunOutcome {
/// New (uncovered) signals waiting in the window.
pending: usize,
},
/// Model output broke the artifact contract; nothing persisted.
/// The paid-for output is salvaged here rather than discarded.
Refused {
/// The validator's refusal.
reason: String,
/// Raw model output for inspection.
model_output: String,
},
/// A concurrent run appended this version first; nothing persisted.
Unpublished {
/// What happened.
@@ -83,6 +75,11 @@ pub enum PreflightOutcome {
estimate: crate::estimate::Estimate,
/// Window actually queried, `[since, until_exclusive)`.
window: (i64, i64),
/// The exact string the model would receive — the transparency seam
/// ("what's happening behind the curtain"). Present only when the
/// caller asked for it.
#[serde(skip_serializing_if = "Option::is_none")]
model_input: Option<String>,
},
}
@@ -164,12 +161,14 @@ async fn plan(
}
/// Zero-spend preflight: what would a run over this window do, and at what
/// estimated cost?
/// estimated cost? With `include_input`, the Ready outcome carries the exact
/// string the model would receive.
pub async fn preflight(
store: &Store,
name: &str,
since: i64,
until_exclusive: i64,
include_input: bool,
) -> Result<PreflightOutcome, FoldError> {
let (_, _, plan) = plan(store, name, since, until_exclusive).await?;
Ok(match plan {
@@ -181,6 +180,7 @@ pub async fn preflight(
truncated: rp.truncated,
estimate: rp.estimate,
window: (since, until_exclusive),
model_input: include_input.then_some(rp.model_input),
},
})
}
@@ -209,18 +209,10 @@ pub async fn run_fold(
.await
.map_err(|e| crate::Error::Runner(format!("runner task panicked: {e}")))??;
// Post-spend: every failure from here on salvages the paid-for output.
// Post-spend: the response IS the artifact; only the version fence can
// still reject it, and that path salvages the paid-for output.
let now = chrono::Utc::now().timestamp();
let artifact = match complete_run(&spec, chain.last(), &run_plan, &output, now) {
Ok(a) => a,
Err(crate::Error::Nonconforming(reason)) => {
return Ok(RunOutcome::Refused {
reason,
model_output: output,
});
}
Err(e) => return Err(e.into()),
};
let artifact = complete_run(&spec, chain.last(), &run_plan, &output, now);
if let Err(e) = store.insert_artifact(&artifact).await {
// The (fold, version) primary key is the fence; a violation means a
// concurrent writer won. Nothing forked, nothing persisted here.
@@ -291,26 +283,49 @@ mod tests {
#[tokio::test]
async fn preflight_prices_before_any_spend() {
let store = store_with_fold_and_signal().await;
let out = preflight(&store, "weekly", 0, 1_000)
let out = preflight(&store, "weekly", 0, 1_000, false)
.await
.expect("preflight");
match out {
PreflightOutcome::Ready { shown, pending, .. } => {
PreflightOutcome::Ready {
shown,
pending,
model_input,
..
} => {
assert_eq!(shown, 1);
// Engine semantics: pending counts every new signal in the
// window, including the ones that would be shown.
assert_eq!(pending, 1);
assert!(model_input.is_none(), "input only rides when asked for");
}
other => panic!("expected ready, got {other:?}"),
}
}
#[tokio::test]
async fn run_persists_conforming_output_and_caches_next() {
async fn preflight_can_expose_the_exact_model_input() {
let store = store_with_fold_and_signal().await;
let out = preflight(&store, "weekly", 0, 1_000, true)
.await
.expect("preflight");
match out {
PreflightOutcome::Ready { model_input, .. } => {
let input = model_input.expect("requested input");
assert!(input.contains("digest the channel"), "task rides in input");
assert!(input.contains("hello world"), "events ride in input");
}
other => panic!("expected ready, got {other:?}"),
}
}
#[tokio::test]
async fn run_persists_output_verbatim_and_caches_next() {
let store = store_with_fold_and_signal().await;
let id = "e".repeat(64);
let output = format!("# Working Context\nsummary\n\n# Log\n- hello [event:{id}]\n");
let runner = Arc::new(FakeRunner(output));
// Free-form output — any shape the task asked for persists verbatim.
let output = format!("Just a paragraph citing [event:{id}], no headings.");
let runner = Arc::new(FakeRunner(output.clone()));
let guard = RunGuard::default();
let out = run_fold(&store, runner.clone(), &guard, "weekly", 0, 1_000)
.await
@@ -322,6 +337,7 @@ mod tests {
assert_eq!(artifact.version, 1);
assert_eq!(shown, 1);
assert_eq!(artifact.shown_ids, vec![id.clone()]);
assert_eq!(artifact.output, output, "response persists verbatim");
}
other => panic!("expected folded, got {other:?}"),
}
@@ -333,26 +349,10 @@ mod tests {
assert_eq!(store.artifacts("weekly").await.expect("chain").len(), 1);
}
#[tokio::test]
async fn nonconforming_output_is_refused_and_salvaged() {
let store = store_with_fold_and_signal().await;
let runner = Arc::new(FakeRunner("no headings at all".into()));
let out = run_fold(&store, runner, &RunGuard::default(), "weekly", 0, 1_000)
.await
.expect("run");
match out {
RunOutcome::Refused { model_output, .. } => {
assert_eq!(model_output, "no headings at all");
}
other => panic!("expected refused, got {other:?}"),
}
assert!(store.artifacts("weekly").await.expect("chain").is_empty());
}
#[tokio::test]
async fn unknown_fold_is_not_found() {
let store = Store::open(":memory:").await.expect("open");
let err = preflight(&store, "nope", 0, 10)
let err = preflight(&store, "nope", 0, 10, false)
.await
.expect_err("missing fold");
assert!(matches!(err, FoldError::NotFound(_)));

View File

@@ -15,7 +15,7 @@ use axum::{Json, Router};
use serde_json::{json, Value};
use crate::runner::FoldRunner;
use crate::{schema, FoldSpec, Selection};
use crate::{FoldSpec, Selection};
use super::folds::{self, FoldError, RunGuard};
use super::status::StatusRegistry;
@@ -279,9 +279,9 @@ async fn get_fold(
struct PutFoldBody {
selection: Selection,
model: String,
/// Defaults to the built-in channel digest prompt.
/// Defaults to the built-in running-digest task.
instructions: Option<String>,
/// Defaults to `channel-digest@v1` (the only schema).
/// Provenance label recorded on artifacts; defaults to `freeform@v1`.
schema: Option<String>,
/// Free-form client-owned JSON, stored verbatim and never read by the
/// engine (see [`FoldSpec::meta`]).
@@ -298,12 +298,12 @@ async fn put_fold(
selection: body.selection,
schema: body
.schema
.unwrap_or_else(|| schema::CHANNEL_DIGEST_V1.name.to_string()),
.unwrap_or_else(|| crate::spec::FREEFORM_SCHEMA.to_string()),
model: body.model,
instructions: body
.instructions
.filter(|i| !i.trim().is_empty())
.unwrap_or_else(|| schema::CHANNEL_DIGEST_PROMPT.to_string()),
.unwrap_or_else(|| crate::spec::DEFAULT_INSTRUCTIONS.to_string()),
meta: body.meta,
};
spec.validate()?;
@@ -327,15 +327,24 @@ async fn delete_fold(
Ok(Json(json!({ "deleted": name })))
}
#[derive(Debug, Default, serde::Deserialize)]
struct PreflightBody {
#[serde(flatten)]
window: WindowBody,
/// When true, the Ready plan carries the exact string the model would
/// receive — the "show me what's behind the curtain" seam.
#[serde(default)]
include_input: bool,
}
async fn preflight_fold(
State(s): State<AppState>,
Path(name): Path<String>,
body: Option<Json<WindowBody>>,
body: Option<Json<PreflightBody>>,
) -> Result<Json<Value>, ApiError> {
let (since, until) = body
.map(|Json(w)| w.resolve())
.unwrap_or_else(|| WindowBody::default().resolve());
let out = folds::preflight(&s.store, &name, since, until).await?;
let Json(body) = body.unwrap_or_default();
let (since, until) = body.window.resolve();
let out = folds::preflight(&s.store, &name, since, until, body.include_input).await?;
Ok(Json(
serde_json::to_value(out).unwrap_or_else(|_| json!({})),
))
@@ -454,7 +463,7 @@ mod tests {
let resp = app.clone().oneshot(put).await.expect("resp");
assert_eq!(resp.status(), StatusCode::OK);
let saved = body_json(resp).await;
assert_eq!(saved["saved"]["schema"], "channel-digest@v1");
assert_eq!(saved["saved"]["schema"], "freeform@v1");
assert!(!saved["saved"]["instructions"]
.as_str()
.unwrap_or("")

View File

@@ -2,21 +2,15 @@
/// Errors surfaced by the fold engine.
///
/// A [`Error::Nonconforming`] means the model's output broke the artifact
/// contract — the run is refused and nothing may be persisted. This is a
/// deliberate design rule: never build a validator that a model can only
/// satisfy by lying; every rule enforced here is one the model can genuinely
/// meet.
/// There is deliberately no "model output rejected" variant: the paid-for
/// response persists verbatim as the artifact, and provenance is
/// engine-computed from the plan, never parsed out of the output.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The fold spec is invalid (bad name, empty selection, unknown schema, …).
/// The fold spec is invalid (bad name, empty selection, blank field, …).
#[error("invalid fold spec: {0}")]
InvalidSpec(String),
/// Model output does not conform to the artifact contract; nothing persists.
#[error("nonconforming model output: {0}")]
Nonconforming(String),
/// The model runner failed (missing binary, timeout, crash, auth sentinel).
#[error("fold runner failed: {0}")]
Runner(String),

View File

@@ -10,17 +10,16 @@
//! - **signal** — a raw relay event, never rewritten; the source of truth.
//! - **selection** — a saveable query over signals (channels, authors, kinds).
//! - **fold** — `artifact' = fold(artifact, new_signals, spec)`.
//! - **artifact** — compact sectioned output where every claim cites the exact
//! signals it came from; persisted locally as an immutable, append-only
//! version chain.
//! - **artifact** — the model's response, verbatim and free-form, persisted
//! locally as an immutable, append-only version chain whose provenance
//! (exactly which signals went in, and when) is engine-computed.
//! - (a **lens** renders artifacts into UI; rendering is out of scope here.)
//!
//! The engine modules are deliberately **pure**: they plan runs, price them,
//! render transcripts, validate model output, and splice history — but perform
//! no relay I/O and hold no storage. Callers (the `buzz` CLI, the [`daemon`])
//! fetch signals and prior artifacts, hand them to [`run::plan_run`], invoke a
//! [`runner::FoldRunner`] if the plan is ready, and finish with
//! [`run::complete_run`].
//! and render transcripts — but perform no relay I/O and hold no storage.
//! Callers (the `buzz` CLI, the [`daemon`]) fetch signals and prior
//! artifacts, hand them to [`run::plan_run`], invoke a [`runner::FoldRunner`]
//! if the plan is ready, and finish with [`run::complete_run`].
//!
//! The optional `daemon` feature (default; engine consumers opt out with
//! `default-features = false`) adds the standalone headless app: it borrows a
@@ -36,11 +35,10 @@
//! - Coverage records exactly the signals the model was shown — an oversized
//! window stalls or truncates honestly; unread signals are never sealed
//! ([`run`]).
//! - Output that breaks the artifact contract — wrong sections, a citation of
//! a signal the model was not shown, or no citation at all — is refused and
//! nothing persists ([`validate`]).
//! - Prior append-section history is retained by the engine, not the model:
//! history cannot be rewritten regardless of model output ([`validate::splice_append_sections`]).
//! - Provenance is engine-owned, never read from model output: `shown_ids`
//! and the coverage window come from the plan. The output itself is
//! free-form — `[event:<id>]` citations in it are reader-verifiable links,
//! not a validated contract ([`artifact`]).
pub mod artifact;
#[cfg(feature = "daemon")]
@@ -49,12 +47,10 @@ pub mod error;
pub mod estimate;
pub mod run;
pub mod runner;
pub mod schema;
pub mod selection;
pub mod signal;
pub mod spec;
pub mod transcript;
pub mod validate;
pub use artifact::ArtifactPayload;
pub use error::Error;

View File

@@ -7,21 +7,20 @@
//! call, no I/O.
//! 2. The caller shows the estimate (priced before spend), invokes a
//! [`crate::runner::FoldRunner`] with `RunPlan::model_input`, then
//! 3. [`complete_run`] — validate the output against the artifact contract,
//! splice history, and produce the next [`ArtifactPayload`]. Nonconforming
//! output returns an error and nothing persists.
//! 3. [`complete_run`] — record the output verbatim with engine-computed
//! provenance (exactly the shown signals and their window) as the next
//! [`ArtifactPayload`]. The output's shape is the task's business, not the
//! engine's.
use std::collections::{BTreeMap, BTreeSet};
use crate::artifact::ArtifactPayload;
use crate::error::Error;
use crate::estimate::{self, Estimate};
use crate::schema;
use crate::selection::materialize;
use crate::signal::Signal;
use crate::spec::FoldSpec;
use crate::transcript::{render_transcript, ShownSignal};
use crate::validate::{splice_append_sections, validate_output};
/// Hard character budget for one run's model input (prior digest + transcript).
pub const MAX_CONTEXT_CHARS: usize = 120_000;
@@ -37,6 +36,17 @@ pub const MAX_SHOWN_PER_RUN: usize = 250;
/// that failure priced-before-spend instead of a wasted model call.
pub const MAX_PRIOR_OUTPUT_BYTES: usize = 40_000;
/// Engine note composed into every model input between the task and the
/// context: what the context is, and how citations work. The output shape is
/// deliberately unconstrained — provenance is engine-owned, so citations are
/// reader-verifiable links, not a contract.
const ENGINE_GUIDANCE: &str = "The context below is your evidence: the prior version of this \
document (when one exists) followed by new time-ordered events. Produce the next version of \
the document — whatever shape the task asks for. Where useful, cite the events behind a claim \
as [event:<id>], 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.";
/// The outcome of planning a run.
#[derive(Debug, Clone, PartialEq)]
pub enum Plan {
@@ -110,7 +120,7 @@ pub fn plan_run(
}
let parent = match prior {
Some(p) => format!(
"--- PRIOR DIGEST ({} v{}) ---\n{}",
"--- PRIOR VERSION ({} v{}) ---\n{}",
spec.name, p.version, p.output
),
None => String::new(),
@@ -136,14 +146,11 @@ pub fn plan_run(
transcript.push_str("\n\n--- SOURCE EVENT IDS ---\n");
transcript.push_str(&ids.join("\n"));
}
// Instructions are the task; the schema contract is non-negotiable and
// travels with every run — otherwise custom instructions silently drop
// the structural rules that complete_run() will refuse on.
// Instructions are the task and the output is free-form; the engine only
// explains what the context is and how citations render.
let model_input = format!(
"--- TASK ---\n{}\n\n{}\n\n--- CONTEXT (time-ordered events) ---\n{}\n",
spec.instructions,
schema::CHANNEL_DIGEST_V1.contract_prompt(),
transcript
spec.instructions, ENGINE_GUIDANCE, transcript
);
let est = estimate::estimate(&spec.model, model_input.chars().count());
Ok(Plan::Ready(RunPlan {
@@ -155,25 +162,19 @@ pub fn plan_run(
}))
}
/// Validate model `output` for `plan` and produce the next artifact version.
/// Record model `output` for `plan` verbatim as the next artifact version.
///
/// 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.
/// The output's shape is the task's business — nothing about the text is
/// judged. Provenance is engine-owned: coverage is computed from exactly the
/// signals the plan showed, never from what the output claims.
pub fn complete_run(
spec: &FoldSpec,
prior: Option<&ArtifactPayload>,
plan: &RunPlan,
output: &str,
created_at: i64,
) -> Result<ArtifactPayload, Error> {
let sch = &schema::CHANNEL_DIGEST_V1;
) -> ArtifactPayload {
let shown_ids: Vec<String> = plan.shown.iter().map(|s| s.id.clone()).collect();
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.
let channels: BTreeSet<String> = prior
@@ -182,10 +183,10 @@ pub fn complete_run(
.union(&spec.selection.channels.iter().cloned().collect())
.cloned()
.collect();
Ok(ArtifactPayload {
ArtifactPayload {
fold: spec.name.clone(),
version: prior.map_or(1, |p| p.version + 1),
output: spliced,
output: output.to_string(),
coverage_since: plan.shown.iter().map(|s| s.created_at).min(),
coverage_until: plan.shown.iter().map(|s| s.created_at).max().map(|t| t + 1),
selection: spec.selection.clone(),
@@ -196,7 +197,7 @@ pub fn complete_run(
prompt_sha256: spec.prompt_sha256(),
truncated: plan.truncated,
created_at,
})
}
}
#[cfg(test)]
@@ -295,15 +296,21 @@ mod tests {
assert!(!run.model_input.contains("--- SOURCE EVENT IDS ---"));
assert!(run
.model_input
.contains("--- PRIOR DIGEST (team-digest v1) ---"));
// No shown signals → citation contract not demanded, coverage stays empty.
let v2 = complete_run(&spec, Some(&prior), &run, &digest(""), 1_700_000_200).expect("v2");
.contains("--- PRIOR VERSION (team-digest v1) ---"));
// No shown signals → nothing new to cover, coverage stays empty.
let v2 = complete_run(
&spec,
Some(&prior),
&run,
"rewritten under new task",
1_700_000_200,
);
assert_eq!(v2.version, 2);
assert!(v2.shown_ids.is_empty());
assert_eq!(v2.coverage_since, None);
assert_eq!(v2.coverage_until, None);
// The engine splice still carried the prior Log forward.
assert!(v2.output.contains("- prior entry"));
// The response persists verbatim — retention is the model's job now.
assert_eq!(v2.output, "rewritten under new task");
}
#[test]
@@ -371,7 +378,7 @@ mod tests {
panic!("expected Ready");
};
let out = digest(&format!("- new entry [event:{b}]"));
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_500).expect("v2");
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_500);
assert_eq!(v2.channels, vec!["ch1".to_string(), "ch2".to_string()]);
assert_eq!(v2.selection, spec.selection);
}
@@ -403,9 +410,9 @@ mod tests {
}
#[test]
fn custom_instructions_cannot_drop_the_output_contract() {
// A task-only prompt ("what is this project about?") must still carry
// the schema's structural rules, or validation is a guaranteed refusal.
fn every_run_carries_the_task_and_the_engine_guidance() {
// The engine explains what the context is and how citations render;
// the task is the caller's, verbatim.
let mut spec = spec();
spec.instructions = "what is this project trying to accomplish".to_string();
let plan = plan_run(
@@ -422,9 +429,8 @@ mod tests {
assert!(run
.model_input
.contains("what is this project trying to accomplish"));
assert!(run.model_input.contains("OUTPUT CONTRACT"));
assert!(run.model_input.contains("Working Context, Log"));
assert!(run.model_input.contains("Log is append-only"));
assert!(run.model_input.contains("whatever shape the task asks for"));
assert!(run.model_input.contains("SOURCE EVENT IDS"));
}
#[test]
@@ -445,7 +451,7 @@ mod tests {
// Coverage seals only what was shown; the rest stays pending.
let cited = run.shown[0].id.clone();
let out = digest(&format!("- chunk one [event:{cited}]"));
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300).expect("v1");
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300);
assert_eq!(v1.shown_ids.len(), run.shown.len());
assert!(v1.truncated);
}
@@ -466,7 +472,7 @@ mod tests {
panic!("expected Ready");
};
let out = digest(&format!("- said hello [event:{b}]"));
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300).expect("v1");
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300);
assert_eq!(v1.version, 1);
assert_eq!(v1.shown_ids, vec![b]);
assert_eq!(v1.coverage_since, Some(200));
@@ -475,7 +481,7 @@ mod tests {
}
#[test]
fn complete_run_splices_prior_history_and_increments_version() {
fn output_is_verbatim_and_the_prior_rides_in_the_input() {
let spec = spec();
let a = hex_id('a');
let b = hex_id('b');
@@ -492,38 +498,16 @@ mod tests {
let Plan::Ready(run) = plan else {
panic!("expected Ready");
};
// Model returns ONLY the new entry; engine must carry the prior Log.
let out = digest(&format!("- new entry [event:{b}]"));
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_400).expect("v2");
// The prior version is the model's context, labeled as such…
assert!(run
.model_input
.contains("--- PRIOR VERSION (team-digest v1) ---"));
// …and the model's response IS the artifact: any shape, verbatim, no
// splice. Provenance (version, coverage) is engine bookkeeping.
let out = "A totally free-form rewrite that dropped the prior entry.";
let v2 = complete_run(&spec, Some(&prior), &run, out, 1_700_000_400);
assert_eq!(v2.version, 2);
assert!(v2.output.contains("- prior entry"));
assert!(v2.output.contains("- new entry"));
}
#[test]
fn wrong_sections_are_refused_but_citations_never_are() {
let spec = spec();
let b = hex_id('b');
let plan = plan_run(
&spec,
None,
&BTreeSet::new(),
vec![sig(&b, 200, "hello")],
&BTreeMap::new(),
)
.expect("plan");
let Plan::Ready(run) = plan else {
panic!("expected Ready");
};
// 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]);
assert_eq!(v2.output, out);
assert_eq!(v2.shown_ids, vec![b]);
}
}

View File

@@ -1,64 +0,0 @@
//! Artifact schemas: the structural contract a fold's output must satisfy.
/// A schema names the exact H1 sections an artifact must have, and which of
/// them are *append* sections — sections whose history the engine retains
/// mechanically ([`crate::validate::splice_append_sections`]) so the model
/// only ever contributes new entries.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArtifactSchema {
/// Versioned schema name, e.g. `channel-digest@v1`.
pub name: &'static str,
/// Required H1 sections, in order.
pub sections: &'static [&'static str],
/// Sections that accumulate: prior content is preserved by construction
/// and new entries must carry signal citations.
pub append_sections: &'static [&'static str],
}
/// A bounded digest of a selection: a rewritten standing summary plus an
/// append-only, citation-carrying log. The only schema; the `schema` string
/// on specs and artifacts stays as the versioning seam for a future second
/// one.
pub const CHANNEL_DIGEST_V1: ArtifactSchema = ArtifactSchema {
name: "channel-digest@v1",
sections: &["Working Context", "Log"],
append_sections: &["Log"],
};
impl ArtifactSchema {
/// The output contract, rendered as prompt text. [`crate::plan_run`]
/// composes this into every model input *alongside* the spec's
/// instructions, so custom instructions focus the task but can never drop
/// the structural rules that [`crate::validate::validate_output`] will
/// enforce — otherwise a task-only prompt is a guaranteed refusal.
pub fn contract_prompt(&self) -> String {
let mut s = format!(
"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 {
s.push_str(&format!(
"{a} is append-only: output ONLY new dated entries for this run's evidence \
— the engine keeps the standing {a} and appends your new entries to it; \
never repeat prior entries.\n"
));
}
s.push_str(
"Cite the source events you actually use as [event:<id>], 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
}
}
/// Default fold *task* instructions for [`CHANNEL_DIGEST_V1`]. The structural
/// rules live in [`ArtifactSchema::contract_prompt`], which every run gets
/// regardless of what the instructions say.
pub const CHANNEL_DIGEST_PROMPT: &str = "Maintain a bounded digest of the anchored selection. \
Working Context is a concise standing summary rewritten in light of all evidence; the Log \
records dated evidence entries.";

View File

@@ -9,12 +9,21 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::Error;
use crate::schema;
use crate::selection::Selection;
/// Maximum fold-name length (also the `d` tag value).
pub const MAX_NAME_LEN: usize = 64;
/// Default schema label for new folds. The output is free-form — the label is
/// provenance on artifacts (which prompt regime produced them), not a
/// validated contract; chains created under `channel-digest@v1` keep their
/// historical label.
pub const FREEFORM_SCHEMA: &str = "freeform@v1";
/// Default fold *task* instructions when the caller supplies none.
pub const DEFAULT_INSTRUCTIONS: &str = "Maintain a running digest of the selection: rewrite the \
prior version in light of the new events. Keep it concise, concrete, and factual.";
/// One fold definition.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FoldSpec {
@@ -22,7 +31,8 @@ pub struct FoldSpec {
pub name: String,
/// What to read.
pub selection: Selection,
/// Schema name; the only one is [`crate::schema::CHANNEL_DIGEST_V1`].
/// Schema label recorded on artifacts as provenance ([`FREEFORM_SCHEMA`]
/// by default). Output is free-form; the label is not a validated shape.
pub schema: String,
/// Model alias passed to the runner and priced by [`crate::estimate`].
pub model: String,
@@ -55,12 +65,10 @@ impl FoldSpec {
self.name
)));
}
if self.schema != schema::CHANNEL_DIGEST_V1.name {
return Err(Error::InvalidSpec(format!(
"unknown schema {:?}; the only schema is {:?}",
self.schema,
schema::CHANNEL_DIGEST_V1.name
)));
if self.schema.trim().is_empty() {
return Err(Error::InvalidSpec(
"schema label must be non-empty (it is recorded on artifacts as provenance)".into(),
));
}
if self.model.trim().is_empty() {
return Err(Error::InvalidSpec(
@@ -116,9 +124,13 @@ mod tests {
}
#[test]
fn unknown_schema_and_empty_fields_are_rejected() {
fn empty_fields_are_rejected_but_any_schema_label_passes() {
// Schema is a provenance label, not a validated shape.
let mut s = valid();
s.schema = "nope@v1".to_string();
s.schema = "my-custom-regime@v3".to_string();
assert!(s.validate().is_ok());
let mut s = valid();
s.schema = " ".to_string();
assert!(s.validate().is_err());
let mut s = valid();
s.model = " ".to_string();

View File

@@ -1,227 +0,0 @@
//! Output-contract validation and the engine splice.
//!
//! 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;
/// True for a line the heading grammar recognizes as an H1: `# ` followed by
/// at least one non-`#` character.
fn is_h1(line: &str) -> bool {
match line.strip_prefix("# ") {
Some(rest) => rest.chars().next().is_some_and(|c| c != '#'),
None => false,
}
}
/// Byte offsets of the section body for H1 `name`: (start just after the
/// heading's newline, end at the next H1 line or end of input). `None` when
/// the heading is absent (a heading with no trailing newline has no body).
fn section_span(output: &str, name: &str) -> Option<(usize, usize)> {
let mut pos = 0;
let mut found: Option<usize> = None;
for line in output.split_inclusive('\n') {
let bare = line.strip_suffix('\n');
let text = bare.unwrap_or(line);
match found {
None => {
// Heading line: `# {name}` plus optional trailing whitespace,
// and it must be newline-terminated to have a body.
let matches = text
.strip_prefix("# ")
.is_some_and(|rest| rest.trim_end() == name && rest.trim_end() == rest.trim());
if matches && bare.is_some() {
found = Some(pos + line.len());
}
}
Some(start) => {
if is_h1(text) {
return Some((start, pos));
}
}
}
pos += line.len();
}
found.map(|start| (start, output.len()))
}
/// The trimmed body of H1 section `name`, or `""` when absent.
pub fn section(output: &str, name: &str) -> String {
match section_span(output, name) {
Some((start, end)) => output[start..end].trim().to_string(),
None => String::new(),
}
}
/// Replace the body of H1 section `name` with `content`, leaving the rest of
/// the document byte-identical. A missing heading leaves the document
/// unchanged.
pub fn replace_section(output: &str, name: &str, content: &str) -> String {
match section_span(output, name) {
Some((start, end)) => {
format!("{}\n{}\n\n{}", &output[..start], content, &output[end..])
}
None => output.to_string(),
}
}
/// All H1 headings in document order (text after `# `, verbatim to line end).
pub fn headings(output: &str) -> Vec<String> {
output
.lines()
.filter(|line| is_h1(line))
.map(|line| line[2..].to_string())
.collect()
}
/// 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!(
"output does not conform to {}: expected H1 sections {:?}, got {:?}",
schema.name, schema.sections, found
)));
}
Ok(())
}
/// Rebuild append sections so prior content is retained by construction.
///
/// The model contributes only this run's NEW entries; the engine prepends the
/// prior section verbatim. A model that repeats the prior section anyway is
/// deduplicated (exact-substring strip). History cannot be rewritten or
/// dropped no matter what the model outputs — and the model never has to
/// re-emit an ever-growing log, which kept run latency flat along the chain.
pub fn splice_append_sections(
schema: &ArtifactSchema,
previous_output: Option<&str>,
output: &str,
) -> String {
let Some(previous) = previous_output.filter(|p| !p.is_empty()) else {
return output.to_string();
};
let mut doc = output.to_string();
for name in schema.append_sections {
let prior = section(previous, name);
if prior.is_empty() {
continue;
}
let model_part = section(&doc, name);
let new_part = if model_part.contains(&prior) {
model_part.replacen(&prior, "", 1)
} else {
model_part
};
let new_part = new_part.trim();
let combined = if new_part.is_empty() {
prior
} else {
format!("{prior}\n{new_part}")
};
doc = replace_section(&doc, name, &combined);
}
doc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::CHANNEL_DIGEST_V1;
fn id(c: char) -> String {
std::iter::repeat_n(c, 64).collect()
}
fn digest(log_body: &str) -> String {
format!("# Working Context\n\nSummary.\n\n# Log\n\n{log_body}\n")
}
#[test]
fn section_and_replace_roundtrip() {
let doc = digest("- entry one");
assert_eq!(section(&doc, "Log"), "- entry one");
assert_eq!(section(&doc, "Working Context"), "Summary.");
assert_eq!(section(&doc, "Missing"), "");
let replaced = replace_section(&doc, "Log", "- swapped");
assert_eq!(section(&replaced, "Log"), "- swapped");
assert_eq!(section(&replaced, "Working Context"), "Summary.");
}
#[test]
fn headings_ignore_h2_and_capture_in_order() {
let doc = "# One\n\n## nested\n\n# Two\nbody\n";
assert_eq!(headings(doc), vec!["One", "Two"]);
}
#[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);
assert!(matches!(err, Err(Error::Nonconforming(_))));
}
#[test]
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]
fn splice_retains_prior_history_when_model_omits_it() {
let prior = digest(&format!("- old entry [event:{}]", id('a')));
let fresh = digest(&format!("- new entry [event:{}]", id('b')));
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &fresh);
let log = section(&doc, "Log");
assert_eq!(
log,
format!(
"- old entry [event:{}]\n- new entry [event:{}]",
id('a'),
id('b')
)
);
}
#[test]
fn splice_dedupes_a_repeated_prior_section() {
let prior = digest(&format!("- old entry [event:{}]", id('a')));
let repeat_plus_new = digest(&format!(
"- old entry [event:{}]\n- new entry [event:{}]",
id('a'),
id('b')
));
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &repeat_plus_new);
let log = section(&doc, "Log");
assert_eq!(log.matches("- old entry").count(), 1);
assert!(log.contains("- new entry"));
}
#[test]
fn splice_with_no_new_entries_keeps_prior_exactly() {
let prior = digest(&format!("- old entry [event:{}]", id('a')));
let empty_log = digest("");
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &empty_log);
assert_eq!(
section(&doc, "Log"),
format!("- old entry [event:{}]", id('a'))
);
}
}

View File

@@ -105,6 +105,9 @@ export type Preflight =
truncated: boolean;
estimate: Estimate;
window: [number, number];
/** The exact string the model would receive; present only when the
* preflight asked for it (`include_input`). */
model_input?: string;
};
export type Artifact = {
@@ -138,7 +141,6 @@ export type ArtifactSummary = {
export type RunOutcome =
| { status: "cached" }
| { status: "stalled"; reason: string; pending: number }
| { status: "refused"; reason: string; model_output: string }
| { status: "unpublished"; reason: string; model_output: string }
| {
status: "folded";
@@ -226,8 +228,11 @@ export const api = {
) => request<{ saved: FoldSpec }>(`/folds/${name}`, { method: "PUT", ...json(body) }),
deleteFold: (name: string) =>
request<{ deleted: string }>(`/folds/${name}`, { method: "DELETE" }),
preflight: (name: string, window: TimeWindow) =>
request<Preflight>(`/folds/${name}/preflight`, post(window)),
preflight: (name: string, window: TimeWindow, includeInput = false) =>
request<Preflight>(
`/folds/${name}/preflight`,
post({ ...window, include_input: includeInput }),
),
run: (name: string, window: TimeWindow) =>
request<RunOutcome>(`/folds/${name}/run`, post(window)),
artifacts: (name: string) =>

View File

@@ -10,8 +10,8 @@ type Props = {
onVersion: (v: number) => void;
};
/** Renders one artifact version: Working Context + Log with citation chips,
* a version scrubber, and a pinned-evidence rail. */
/** Renders one artifact version: the model's free-form response with citation
* chips, a version scrubber, and a pinned-evidence rail. */
export function ArtifactReader({ fold, version, onVersion }: Props) {
const [pinned, setPinned] = useState<string[]>([]);
@@ -50,12 +50,21 @@ export function ArtifactReader({ fold, version, onVersion }: Props) {
</div>
{a && (
<div className="muted" style={{ fontSize: 12, marginBottom: 10 }}>
{fmtTime(a.created_at)} · model {a.model} · {a.shown_ids.length} shown
{a.truncated ? " · chunked window" : ""}
{fmtTime(a.created_at)} · model {a.model} · folded{" "}
{a.shown_ids.length} new event(s)
{a.truncated ? " · chunked (more remained pending)" : ""}
{a.coverage_since !== null && a.coverage_until !== null && (
<>
{" "}
· covers {fmtTime(a.coverage_since)} → {fmtTime(a.coverage_until)}
· this run's new events span {fmtTime(a.coverage_since)} →{" "}
{fmtTime(a.coverage_until)}
</>
)}
{a.version > 1 && (
<>
{" "}
· built on v{a.version - 1} (earlier versions' events aren't
re-read — they ride along in the prior version)
</>
)}
</div>

View File

@@ -84,7 +84,7 @@ function CreateFold({ selection }: { selection: Selection }) {
<label>Instructions</label>
<textarea
rows={2}
placeholder="task focus, e.g. 'track decisions and blockers' — the Working Context / Log output contract always applies"
placeholder="the task — instructions + context go to the model; its response IS the artifact, any shape you ask for"
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
/>
@@ -120,7 +120,9 @@ function FoldCard({
const [outcome, setOutcome] = useState<RunOutcome | null>(null);
const preflight = useMutation({
mutationFn: () => api.preflight(fold.name, {}),
// Always ask for the exact model input: the plan box offers it behind a
// disclosure so a run is never a mystery.
mutationFn: () => api.preflight(fold.name, {}, true),
onSuccess: (p) => {
setPlan(p);
setOutcome(null);
@@ -163,6 +165,9 @@ function FoldCard({
? `v${fold.latest_version} · updated ${fmtTime(fold.updated_at)}`
: "never run"}
</div>
<div className="desc" style={{ whiteSpace: "pre-wrap" }}>
<span className="faint">task:</span> {fold.spec.instructions}
</div>
<div className="actions">
<button
onClick={() => preflight.mutate()}
@@ -210,14 +215,28 @@ function PlanBox({ plan }: { plan: Preflight }) {
const fits = est.window_fit.fits;
return (
<div className="plan-box ok">
Ready: {plan.shown} shown / {plan.pending} pending
{plan.truncated ? " (chunked)" : ""} · ~
Ready: {plan.shown} new event(s) to fold / {plan.pending} not yet covered
{plan.truncated ? " (chunked — the rest stays pending)" : ""} · ~
{fmtTokens(est.est_input_tokens)} input tokens
{fits === false ? " · OVER the model window" : ""}
<br />
<span className="faint">
Run is pinned to this priced window; press run to spend.
Each run folds only events no earlier version covered; the prior
version rides along as context. Run is pinned to this priced window.
</span>
{plan.model_input !== undefined && (
<details style={{ marginTop: 6 }}>
<summary className="faint" style={{ cursor: "pointer" }}>
show the exact model input ({plan.model_input.length.toLocaleString()} chars)
</summary>
<pre
className="mono"
style={{ whiteSpace: "pre-wrap", maxHeight: 320, overflow: "auto" }}
>
{plan.model_input}
</pre>
</details>
)}
</div>
);
}
@@ -232,16 +251,6 @@ function OutcomeBox({ outcome }: { outcome: RunOutcome }) {
Stalled: {outcome.reason} ({outcome.pending} pending)
</div>
);
case "refused":
return (
<div className="plan-box err">
Model output refused ({outcome.reason}). Nothing persisted; raw
output:
<pre className="mono" style={{ whiteSpace: "pre-wrap" }}>
{outcome.model_output.slice(0, 2000)}
</pre>
</div>
);
case "unpublished":
return (
<div className="plan-box err">