use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::Engine;
use crate::anchor::{Anchor, AnchorState, ObservedArtifactHash};
use crate::binding::{
Binding, DEFAULT_ADJUDICATION_CAP, DEFAULT_FULL_RESYNC_EVERY, hash_binding, medium_capabilities,
};
use crate::workspace_store::{StoreError, WORKSPACE_STORE_DIR};
use super::advance::is_single_component;
use super::cursor::{compute_source_cursor, enumerate_facet_files};
use super::refinement::{
ROTATION_ANCHOR_ADJUDICATION, bump_verify_runs, next_batch, next_rotation_batch,
};
use super::resolve::{ResolvedIngest, ResolvedSource};
const STATE_DIR: &str = "state";
const FINDINGS_DIR: &str = "findings";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FindingKey {
pub binding_hash: String,
pub source_head: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FindingClass {
Drifted,
Wrong,
Uncovered,
UnresolvableAnchor,
QueuedForAdjudication,
}
impl FindingClass {
pub const WIRE_VALUES: &'static [&'static str] = &[
"drifted",
"wrong",
"uncovered",
"unresolvable-anchor",
"queued-for-adjudication",
];
pub fn as_wire(&self) -> &'static str {
match self {
FindingClass::Drifted => "drifted",
FindingClass::Wrong => "wrong",
FindingClass::Uncovered => "uncovered",
FindingClass::UnresolvableAnchor => "unresolvable-anchor",
FindingClass::QueuedForAdjudication => "queued-for-adjudication",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
match s {
"drifted" => Some(FindingClass::Drifted),
"wrong" => Some(FindingClass::Wrong),
"uncovered" => Some(FindingClass::Uncovered),
"unresolvable-anchor" => Some(FindingClass::UnresolvableAnchor),
"queued-for-adjudication" => Some(FindingClass::QueuedForAdjudication),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum FindingTarget {
Anchor {
entity: String,
artifact: String,
},
Artifact {
artifact: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
pub key: FindingKey,
pub facet: String,
pub target: FindingTarget,
pub class: FindingClass,
pub detail: String,
pub created_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FindingsBatch {
pub key: FindingKey,
pub recorded_at: String,
pub findings: Vec<Finding>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FindingsStore {
pub binding: String,
#[serde(default)]
pub batches: Vec<FindingsBatch>,
}
impl FindingsStore {
fn current_batch_index(&self, binding_hash: &str) -> Option<usize> {
self.batches
.iter()
.enumerate()
.filter(|(_, b)| b.key.binding_hash == binding_hash)
.max_by_key(|(i, b)| (b.recorded_at.parse::<u64>().unwrap_or(0), *i))
.map(|(i, _)| i)
}
pub fn record(&mut self, key: FindingKey, recorded_at: String, findings: Vec<Finding>) {
self.batches
.retain(|b| b.key.binding_hash != key.binding_hash);
self.batches.push(FindingsBatch {
key,
recorded_at,
findings,
});
}
pub fn current(&self, key: &FindingKey) -> &[Finding] {
self.current_batch_index(&key.binding_hash)
.map(|i| self.batches[i].findings.as_slice())
.unwrap_or(&[])
}
pub fn superseded(&self, key: &FindingKey) -> Vec<&Finding> {
let current = self.current_batch_index(&key.binding_hash);
self.batches
.iter()
.enumerate()
.filter(|(i, _)| Some(*i) != current)
.flat_map(|(_, b)| b.findings.iter())
.collect()
}
}
pub fn findings_store_path(workspace_root: &Path, mem: &str, name: &str) -> PathBuf {
workspace_root
.join(WORKSPACE_STORE_DIR)
.join(STATE_DIR)
.join(FINDINGS_DIR)
.join(mem)
.join(format!("{name}.json"))
}
pub const STANDALONE_KEY: &str = "standalone";
#[derive(Debug, Clone, Serialize)]
pub struct AnnotatedStandaloneFinding {
#[serde(flatten)]
pub finding: Finding,
pub already_seen: bool,
}
pub fn record_standalone_findings(
workspace_root: &Path,
report: &crate::engine::query::MemAnchorVerification,
) -> Result<Vec<AnnotatedStandaloneFinding>, StoreError> {
let mem = &report.mem;
let key = FindingKey {
binding_hash: STANDALONE_KEY.to_string(),
source_head: String::new(),
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
.to_string();
let findings: Vec<Finding> = report
.anchors
.iter()
.filter_map(|a| {
let class = match a.state.as_str() {
"drifted" => FindingClass::Drifted,
"unresolvable" => FindingClass::UnresolvableAnchor,
_ => return None,
};
Some(Finding {
key: key.clone(),
facet: STANDALONE_KEY.to_string(),
target: FindingTarget::Anchor {
entity: a.entity_id.clone(),
artifact: a.artifact.clone(),
},
class,
detail: format!("{} ({} {})", a.state, a.class, a.grain),
created_at: now.clone(),
})
})
.collect();
let mut store =
read_findings_store(workspace_root, mem, STANDALONE_KEY)?.unwrap_or_else(|| {
FindingsStore {
binding: format!("{mem}/{STANDALONE_KEY}"),
..Default::default()
}
});
let prior: BTreeSet<(String, String)> = store
.current(&key)
.iter()
.map(|f| {
(
serde_json::to_string(&f.target).unwrap_or_default(),
f.class.as_wire().to_string(),
)
})
.collect();
let annotated: Vec<AnnotatedStandaloneFinding> = findings
.iter()
.map(|f| AnnotatedStandaloneFinding {
finding: f.clone(),
already_seen: prior.contains(&(
serde_json::to_string(&f.target).unwrap_or_default(),
f.class.as_wire().to_string(),
)),
})
.collect();
store.record(key, now, findings);
write_findings_store(workspace_root, mem, STANDALONE_KEY, &store)?;
Ok(annotated)
}
pub fn read_findings_store(
workspace_root: &Path,
mem: &str,
name: &str,
) -> Result<Option<FindingsStore>, StoreError> {
let path = findings_store_path(workspace_root, mem, name);
match std::fs::read(&path) {
Ok(bytes) => serde_json::from_slice(&bytes)
.map(Some)
.map_err(|e| StoreError::Parse {
path,
message: e.to_string(),
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(StoreError::Io { path, source: e }),
}
}
pub(crate) fn ensure_selfignoring_store_dir(subtree_root: &Path) -> Result<(), StoreError> {
std::fs::create_dir_all(subtree_root).map_err(|e| StoreError::Io {
path: subtree_root.to_path_buf(),
source: e,
})?;
let gitignore = subtree_root.join(".gitignore");
if !gitignore.exists() {
let _ = std::fs::write(&gitignore, "*\n");
}
Ok(())
}
pub fn write_findings_store(
workspace_root: &Path,
mem: &str,
name: &str,
store: &FindingsStore,
) -> Result<(), StoreError> {
ensure_selfignoring_store_dir(
&workspace_root
.join(WORKSPACE_STORE_DIR)
.join(STATE_DIR)
.join(FINDINGS_DIR),
)?;
let path = findings_store_path(workspace_root, mem, name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| StoreError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
let bytes = serde_json::to_vec_pretty(store).map_err(|e| StoreError::Parse {
path: path.clone(),
message: e.to_string(),
})?;
std::fs::write(&path, bytes).map_err(|e| StoreError::Io { path, source: e })
}
pub fn delete_findings_store(
workspace_root: &Path,
mem: &str,
name: &str,
) -> Result<(), StoreError> {
let path = findings_store_path(workspace_root, mem, name);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(StoreError::Io { path, source: e }),
}
}
#[derive(Debug, thiserror::Error)]
pub enum FindingsError {
#[error("malformed binding id '{0}': expected `<mem>/<stem>`")]
MalformedId(String),
#[error("findings store error: {0}")]
Store(#[source] StoreError),
#[error("source '{source_name}' unreachable: `{path}` does not exist")]
SourceUnreachable {
source_name: String,
path: String,
},
#[error(
"full verify refused: facet '{}' resolves over non-enumerable medium type '{}' — {}",
.0.facet, .0.medium_type, .0.reason
)]
FullWalkNonEnumerable(FullResyncRefusal),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyOutcome {
pub binding: String,
pub key: FindingKey,
pub recorded: usize,
pub superseded: usize,
pub backlog: usize,
pub full_resync: FullResyncDecision,
pub facet_heads: BTreeMap<String, String>,
pub hash_backfill: Vec<ObservedArtifactHash>,
}
pub fn record_verified_baseline(
engine: &mut Engine,
destination_mem: &str,
outcome: &VerifyOutcome,
note: Option<&str>,
) -> Result<Vec<String>, crate::engine::EngineError> {
let mut written = Vec::with_capacity(outcome.facet_heads.len());
for (facet, token) in &outcome.facet_heads {
let key = format!("{}/{facet}#verified", outcome.binding);
engine.set_mem_sync_state(destination_mem, &key, token, note)?;
written.push(key);
}
Ok(written)
}
pub fn record_anchor_hash_backfill(
engine: &mut Engine,
destination_mem: &str,
outcome: &VerifyOutcome,
note: Option<&str>,
) -> Result<usize, crate::engine::EngineError> {
engine.record_anchor_observed_hashes(destination_mem, &outcome.hash_backfill, note)
}
fn split_binding_id(binding_id: &str) -> Result<(String, String), FindingsError> {
binding_id
.split_once('/')
.filter(|(m, n)| is_single_component(m) && is_single_component(n))
.map(|(m, n)| (m.to_string(), n.to_string()))
.ok_or_else(|| FindingsError::MalformedId(binding_id.to_string()))
}
fn source_facet_label(resolved: &ResolvedIngest) -> String {
let facets: Vec<&str> = resolved
.sources
.iter()
.filter_map(|s| match s {
ResolvedSource::Primary(p) => Some(p.name.as_str()),
ResolvedSource::Reference { .. } => None,
})
.collect();
facets.join(",")
}
fn now_seconds() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
secs.to_string()
}
fn current_facet_heads(
engine: &Engine,
workspace_root: &Path,
resolved: &ResolvedIngest,
) -> BTreeMap<String, String> {
let binding_id = &resolved.name;
let prefix = format!("{binding_id}/");
let mut tokens: BTreeMap<String, String> = BTreeMap::new();
if let Some(cfg) = engine.mem_config_for(&resolved.destination_mem) {
for (k, v) in &cfg.sync_state {
if let Some(rest) = k.strip_prefix(&prefix)
&& let Some(facet) = rest.strip_suffix("#synced")
{
tokens.insert(facet.to_string(), v.clone());
}
}
}
let cursor = compute_source_cursor(engine, resolved, workspace_root);
for c in cursor.write_commands.iter().chain(cursor.reseed.iter()) {
if let Some(rest) = c.key.strip_prefix(&prefix)
&& let Some(facet) = rest.strip_suffix("#synced")
{
tokens.insert(facet.to_string(), c.token.clone());
}
}
tokens
}
fn join_facet_heads(tokens: &BTreeMap<String, String>) -> String {
tokens
.iter()
.map(|(facet, token)| format!("{facet}={token}"))
.collect::<Vec<_>>()
.join(";")
}
fn current_source_head(
engine: &Engine,
workspace_root: &Path,
resolved: &ResolvedIngest,
) -> String {
join_facet_heads(¤t_facet_heads(engine, workspace_root, resolved))
}
fn binding_hash_of(binding: &Binding, _resolved: &ResolvedIngest) -> String {
hash_binding(binding)
}
fn current_key(
engine: &Engine,
workspace_root: &Path,
binding: &Binding,
resolved: &ResolvedIngest,
) -> FindingKey {
FindingKey {
binding_hash: binding_hash_of(binding, resolved),
source_head: current_source_head(engine, workspace_root, resolved),
}
}
pub fn current_findings(
engine: &Engine,
workspace_root: &Path,
binding: &Binding,
resolved: &ResolvedIngest,
) -> Result<(FindingKey, Vec<Finding>), FindingsError> {
let (mem, name) = split_binding_id(&resolved.name)?;
let key = current_key(engine, workspace_root, binding, resolved);
let findings = read_findings_store(workspace_root, &mem, &name)
.map_err(FindingsError::Store)?
.map(|s| s.current(&key).to_vec())
.unwrap_or_default();
Ok((key, findings))
}
pub fn adjudicate_anchor(
key: &FindingKey,
facet: &str,
entity: &str,
anchor: &Anchor,
state: AnchorState,
created_at: &str,
) -> Option<Finding> {
let (class, detail) = match state {
AnchorState::Resolves => return None,
AnchorState::Orphaned => (
FindingClass::UnresolvableAnchor,
format!(
"artifact '{}' the anchor references is no longer present in the medium",
anchor.artifact
),
),
AnchorState::Drifted | AnchorState::Recheck => {
if !anchor.class.is_hash_bearing() {
return None;
}
match state {
AnchorState::Drifted => (
FindingClass::Drifted,
format!(
"prepared-content hash of '{}' drifted from the anchored hash",
anchor.artifact
),
),
_ => (
FindingClass::QueuedForAdjudication,
format!(
"hash adjudication of '{}' deferred (recheck); queued",
anchor.artifact
),
),
}
}
};
Some(Finding {
key: key.clone(),
facet: facet.to_string(),
target: FindingTarget::Anchor {
entity: entity.to_string(),
artifact: anchor.artifact.clone(),
},
class,
detail,
created_at: created_at.to_string(),
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FacetEnumerability {
pub facet: String,
pub medium_type: String,
pub enumerable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FullResyncRefusal {
pub facet: String,
pub medium_type: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "kebab-case")]
pub enum FullResyncDecision {
Disabled,
NotDue {
run_count: u64,
every: u32,
runs_until_due: u32,
},
Due {
run_count: u64,
every: u32,
walked_facets: Vec<String>,
refused: Vec<FullResyncRefusal>,
},
Forced {
walked_facets: Vec<String>,
},
}
impl FullResyncDecision {
pub fn is_full_walk(&self) -> bool {
matches!(
self,
FullResyncDecision::Due { .. } | FullResyncDecision::Forced { .. }
)
}
}
pub fn schedule_full_resync(
every: u32,
run_count: u64,
facets: &[FacetEnumerability],
) -> FullResyncDecision {
if every == 0 {
return FullResyncDecision::Disabled;
}
let modulo = run_count % u64::from(every);
if modulo != 0 {
return FullResyncDecision::NotDue {
run_count,
every,
runs_until_due: (u64::from(every) - modulo) as u32,
};
}
let mut walked_facets = Vec::new();
let mut refused = Vec::new();
for f in facets {
if f.enumerable {
walked_facets.push(f.facet.clone());
} else {
refused.push(FullResyncRefusal {
facet: f.facet.clone(),
medium_type: f.medium_type.clone(),
reason: format!(
"medium type '{}' is non-enumerable — a full-enumeration walk cannot cover \
it; the scheduled full resync refuses rather than claim full coverage",
f.medium_type
),
});
}
}
FullResyncDecision::Due {
run_count,
every,
walked_facets,
refused,
}
}
fn candidate_key(entity: &str, anchor: &Anchor) -> String {
format!("{entity}\u{1f}{}", anchor.artifact)
}
fn adjudicate_candidates(
key: &FindingKey,
facet: &str,
candidates: &[(String, Anchor, AnchorState)],
window: Option<&BTreeSet<String>>,
created_at: &str,
) -> Vec<Finding> {
let mut out = Vec::new();
for (entity, anchor, state) in candidates {
let ck = candidate_key(entity, anchor);
let adjudicate_now = window.is_none_or(|w| w.contains(&ck));
if adjudicate_now {
if let Some(f) = adjudicate_anchor(key, facet, entity, anchor, *state, created_at) {
out.push(f);
}
} else {
out.push(Finding {
key: key.clone(),
facet: facet.to_string(),
target: FindingTarget::Anchor {
entity: entity.clone(),
artifact: anchor.artifact.clone(),
},
class: FindingClass::QueuedForAdjudication,
detail: format!(
"adjudication of '{}' deferred (per-run adjudication cap reached); queued",
anchor.artifact
),
created_at: created_at.to_string(),
});
}
}
out
}
fn target_key(target: &FindingTarget) -> String {
match target {
FindingTarget::Anchor { entity, artifact } => format!("a\u{1f}{entity}\u{1f}{artifact}"),
FindingTarget::Artifact { artifact } => format!("f\u{1f}{artifact}"),
}
}
struct PassObservation {
anchors_observed: BTreeSet<String>,
anchors_existing: BTreeSet<String>,
files_observed: BTreeSet<String>,
s_d: BTreeSet<String>,
}
fn merge_with_prior(
mut fresh: Vec<Finding>,
prior: &[Finding],
obs: &PassObservation,
covered_now: impl Fn(&str) -> bool,
) -> Vec<Finding> {
let fresh_idx: BTreeMap<String, usize> = fresh
.iter()
.enumerate()
.map(|(i, f)| (target_key(&f.target), i))
.collect();
let mut carried: Vec<Finding> = Vec::new();
for f in prior {
let tkey = target_key(&f.target);
let observed = match &f.target {
FindingTarget::Anchor { .. } => obs.anchors_observed.contains(&tkey),
FindingTarget::Artifact { artifact } => obs.files_observed.contains(artifact),
};
if observed {
if matches!(f.class, FindingClass::Drifted | FindingClass::Wrong)
&& let Some(&i) = fresh_idx.get(&tkey)
&& fresh[i].class == FindingClass::QueuedForAdjudication
{
fresh[i] = f.clone();
}
continue;
}
if fresh_idx.contains_key(&tkey) {
continue; }
let still_open = match &f.target {
FindingTarget::Anchor { .. } => obs.anchors_existing.contains(&tkey),
FindingTarget::Artifact { artifact } => {
obs.s_d.contains(artifact) && !covered_now(artifact)
}
};
if still_open {
carried.push(f.clone());
}
}
fresh.extend(carried);
fresh
}
pub fn verify_binding(
engine: &Engine,
workspace_root: &Path,
binding: &Binding,
resolved: &ResolvedIngest,
) -> Result<VerifyOutcome, FindingsError> {
run_verify(engine, workspace_root, binding, resolved, false)
}
pub fn verify_binding_full(
engine: &Engine,
workspace_root: &Path,
binding: &Binding,
resolved: &ResolvedIngest,
) -> Result<VerifyOutcome, FindingsError> {
run_verify(engine, workspace_root, binding, resolved, true)
}
fn run_verify(
engine: &Engine,
workspace_root: &Path,
binding: &Binding,
resolved: &ResolvedIngest,
full: bool,
) -> Result<VerifyOutcome, FindingsError> {
let binding_id = resolved.name.clone();
let (mem, name) = split_binding_id(&binding_id)?;
if full {
for source in &resolved.sources {
if let ResolvedSource::Primary(p) = source {
let medium_type = medium_type_wire(p.medium_type);
if !medium_capabilities(p.medium_type).enumerable {
return Err(FindingsError::FullWalkNonEnumerable(FullResyncRefusal {
facet: p.name.clone(),
medium_type: medium_type.clone(),
reason: format!(
"medium type '{medium_type}' is non-enumerable — a full-enumeration \
walk cannot cover it; the full measurement refuses rather than \
render a report with fabricated completeness"
),
}));
}
}
}
}
for source in &resolved.sources {
if let ResolvedSource::Primary(p) = source
&& matches!(
p.medium_type,
crate::pipeline::MediumType::Codebase
| crate::pipeline::MediumType::Filesystem
| crate::pipeline::MediumType::Git
)
{
let base = super::resolve::source_base_path(p, workspace_root);
if !base.exists() {
return Err(FindingsError::SourceUnreachable {
source_name: p.name.clone(),
path: base.display().to_string(),
});
}
}
}
let facet_heads = current_facet_heads(engine, workspace_root, resolved);
let key = FindingKey {
binding_hash: binding_hash_of(binding, resolved),
source_head: join_facet_heads(&facet_heads),
};
let now = now_seconds();
let facet = source_facet_label(resolved);
let cache_root = workspace_root.join(".memstead.cache").join("ingest");
let verify_op = binding.operations.verify.as_ref();
let cap = verify_op.map_or(DEFAULT_ADJUDICATION_CAP, |v| v.adjudication_cap);
let full_resync_every = verify_op.map_or(DEFAULT_FULL_RESYNC_EVERY, |v| v.full_resync_every);
let sample_batch = verify_op
.map_or(resolved.batch_size, |v| v.batch_size)
.max(1) as usize;
let run_count = bump_verify_runs(&cache_root, &binding_id);
let facet_enum: Vec<FacetEnumerability> = resolved
.sources
.iter()
.filter_map(|s| match s {
ResolvedSource::Primary(p) => Some(FacetEnumerability {
facet: p.name.clone(),
medium_type: medium_type_wire(p.medium_type),
enumerable: medium_capabilities(p.medium_type).enumerable,
}),
ResolvedSource::Reference { .. } => None,
})
.collect();
let full_resync = if full {
FullResyncDecision::Forced {
walked_facets: facet_enum.iter().map(|f| f.facet.clone()).collect(),
}
} else {
schedule_full_resync(full_resync_every, run_count, &facet_enum)
};
let mut findings: Vec<Finding> = Vec::new();
let mut existence: Vec<(String, Anchor, AnchorState)> = Vec::new();
let mut candidates: Vec<(String, Anchor, AnchorState)> = Vec::new();
let mut hash_backfill: Vec<ObservedArtifactHash> = Vec::new();
let mut backfill_seen: BTreeSet<(String, String)> = BTreeSet::new();
let mut anchors_existing: BTreeSet<String> = BTreeSet::new();
let mut anchors_observed: BTreeSet<String> = BTreeSet::new();
for (eid, resolved_anchor) in engine.mem_anchors_resolved(&resolved.destination_mem) {
let tkey = target_key(&FindingTarget::Anchor {
entity: eid.as_ref().to_string(),
artifact: resolved_anchor.anchor.artifact.clone(),
});
anchors_existing.insert(tkey.clone());
let Some(state) = resolved_anchor.state else {
continue;
};
anchors_observed.insert(tkey);
let observed_hash = resolved_anchor.observed_hash;
let anchor = resolved_anchor.anchor;
match state {
AnchorState::Resolves => {}
AnchorState::Orphaned => existence.push((eid.as_ref().to_string(), anchor, state)),
AnchorState::Drifted | AnchorState::Recheck => {
if !anchor.class.is_hash_bearing() {
continue;
}
if anchor.hash.is_none()
&& let Some(hash) = observed_hash
{
if backfill_seen.insert((eid.as_ref().to_string(), anchor.artifact.clone())) {
hash_backfill.push(ObservedArtifactHash {
entity: eid.as_ref().to_string(),
artifact: anchor.artifact.clone(),
hash,
});
}
continue;
}
candidates.push((eid.as_ref().to_string(), anchor, state));
}
}
}
for (entity, anchor, state) in &existence {
if let Some(f) = adjudicate_anchor(&key, &facet, entity, anchor, *state, &now) {
findings.push(f);
}
}
let window: Option<BTreeSet<String>> = if full || cap == 0 {
None
} else {
let mut keys: Vec<String> = candidates
.iter()
.map(|(e, a, _)| candidate_key(e, a))
.collect();
keys.sort();
keys.dedup();
next_rotation_batch(
&cache_root,
&binding_id,
ROTATION_ANCHOR_ADJUDICATION,
keys,
cap as usize,
)
.map(|b| b.files.into_iter().collect())
};
findings.extend(adjudicate_candidates(
&key,
&facet,
&candidates,
window.as_ref(),
&now,
));
let sample_files: Vec<String> = if full_resync.is_full_walk() {
let mut all: Vec<String> = Vec::new();
for source in &resolved.sources {
if let ResolvedSource::Primary(p) = source
&& medium_capabilities(p.medium_type).enumerable
{
all.extend(enumerate_facet_files(
p,
&resolved.deny_paths,
workspace_root,
));
}
}
all.sort();
all.dedup();
all
} else {
next_batch(resolved, workspace_root, &cache_root, sample_batch)
.map(|b| b.files)
.unwrap_or_default()
};
let covered_now = |artifact: &str| {
engine
.anchors_referencing_artifact(artifact)
.iter()
.any(|(eid, _)| eid.mem() == resolved.destination_mem.as_str())
};
for file in &sample_files {
if !covered_now(file) {
findings.push(Finding {
key: key.clone(),
facet: facet.clone(),
target: FindingTarget::Artifact {
artifact: file.clone(),
},
class: FindingClass::Uncovered,
detail: "source artifact in scope has no anchor in the destination mem".to_string(),
created_at: now.clone(),
});
}
}
let mut store = read_findings_store(workspace_root, &mem, &name)
.map_err(FindingsError::Store)?
.unwrap_or_else(|| FindingsStore {
binding: binding_id.clone(),
..Default::default()
});
let mut s_d: BTreeSet<String> = BTreeSet::new();
for source in &resolved.sources {
if let ResolvedSource::Primary(p) = source
&& medium_capabilities(p.medium_type).enumerable
{
s_d.extend(enumerate_facet_files(
p,
&resolved.deny_paths,
workspace_root,
));
}
}
let obs = PassObservation {
anchors_observed,
anchors_existing,
files_observed: sample_files.into_iter().collect(),
s_d,
};
let prior = store.current(&key).to_vec();
let findings = merge_with_prior(findings, &prior, &obs, covered_now);
let backlog = findings
.iter()
.filter(|f| f.class == FindingClass::QueuedForAdjudication)
.count();
let recorded = findings.len();
store.record(key.clone(), now, findings);
let superseded = store.superseded(&key).len();
write_findings_store(workspace_root, &mem, &name, &store).map_err(FindingsError::Store)?;
Ok(VerifyOutcome {
binding: binding_id,
key,
recorded,
superseded,
backlog,
full_resync,
facet_heads,
hash_backfill,
})
}
fn medium_type_wire(t: crate::pipeline::MediumType) -> String {
serde_json::to_value(t)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::anchor::{Anchor, AnchorGrain, AnchorHashStability, AnchorProvenanceClass};
fn key(hash: &str, head: &str) -> FindingKey {
FindingKey {
binding_hash: hash.to_string(),
source_head: head.to_string(),
}
}
fn anchor(class: AnchorProvenanceClass) -> Anchor {
Anchor {
artifact: "src/lib.rs".to_string(),
grain: AnchorGrain::File,
class,
at_version: None,
hash: if class.is_hash_bearing() {
Some("h1".to_string())
} else {
None
},
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
}
}
#[test]
fn store_round_trips_on_disk_and_delete_is_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
assert!(
read_findings_store(root, "engine", "graph")
.unwrap()
.is_none()
);
let mut store = FindingsStore {
binding: "engine/graph".to_string(),
..Default::default()
};
let k = key("hashA", "head1");
store.record(
k.clone(),
"1".to_string(),
vec![Finding {
key: k.clone(),
facet: "src".to_string(),
target: FindingTarget::Artifact {
artifact: "src/a.rs".to_string(),
},
class: FindingClass::Uncovered,
detail: "d".to_string(),
created_at: "1".to_string(),
}],
);
write_findings_store(root, "engine", "graph", &store).unwrap();
assert!(findings_store_path(root, "engine", "graph").exists());
let ignore = root
.join(WORKSPACE_STORE_DIR)
.join(STATE_DIR)
.join(FINDINGS_DIR)
.join(".gitignore");
assert_eq!(std::fs::read_to_string(&ignore).unwrap(), "*\n");
let back = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
assert_eq!(back, store);
assert_eq!(back.current(&k).len(), 1);
delete_findings_store(root, "engine", "graph").unwrap();
assert!(
read_findings_store(root, "engine", "graph")
.unwrap()
.is_none()
);
delete_findings_store(root, "engine", "graph").unwrap();
}
#[test]
fn changed_binding_hash_supersedes_prior_findings() {
let mut store = FindingsStore::default();
let old = key("hashOLD", "head1");
let new = key("hashNEW", "head1");
let f_old = Finding {
key: old.clone(),
facet: "src".to_string(),
target: FindingTarget::Artifact {
artifact: "src/old.rs".to_string(),
},
class: FindingClass::Uncovered,
detail: "old".to_string(),
created_at: "1".to_string(),
};
store.record(old.clone(), "1".to_string(), vec![f_old.clone()]);
store.record(new.clone(), "2".to_string(), Vec::new());
assert!(store.current(&new).is_empty(), "new key has its own view");
let superseded = store.superseded(&new);
assert_eq!(superseded.len(), 1, "old batch is segregated as superseded");
assert_eq!(superseded[0], &f_old);
assert!(!store.current(&new).contains(&f_old));
}
#[test]
fn moved_source_head_keeps_findings_current_until_superseded() {
let mut store = FindingsStore::default();
let before = key("hashA", "head1");
let after = key("hashA", "head2");
let f = Finding {
key: before.clone(),
facet: "src".to_string(),
target: FindingTarget::Anchor {
entity: "engine--e".to_string(),
artifact: "src/x.rs".to_string(),
},
class: FindingClass::UnresolvableAnchor,
detail: "gone".to_string(),
created_at: "1".to_string(),
};
store.record(before.clone(), "1".to_string(), vec![f.clone()]);
assert_eq!(store.current(&after), std::slice::from_ref(&f));
assert_eq!(store.current(&after)[0].key.source_head, "head1");
assert!(store.superseded(&after).is_empty());
store.record(after.clone(), "2".to_string(), Vec::new());
assert!(store.current(&after).is_empty());
assert!(store.current(&before).is_empty(), "at the old head too");
assert_eq!(store.batches.len(), 1, "one batch per hash(D)");
}
#[test]
fn legacy_per_head_store_loads_and_presents_head_agnostically() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let path = findings_store_path(root, "engine", "graph");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(
&path,
r#"{
"binding": "engine/graph",
"batches": [
{
"key": { "binding_hash": "hashOLD", "source_head": "src=aaa" },
"recorded_at": "100",
"findings": [
{
"key": { "binding_hash": "hashOLD", "source_head": "src=aaa" },
"facet": "src",
"target": { "kind": "artifact", "artifact": "src/old.rs" },
"class": "uncovered",
"detail": "old declaration",
"created_at": "100"
}
]
},
{
"key": { "binding_hash": "hashCUR", "source_head": "src=bbb" },
"recorded_at": "200",
"findings": [
{
"key": { "binding_hash": "hashCUR", "source_head": "src=bbb" },
"facet": "src",
"target": { "kind": "artifact", "artifact": "src/resolved-at-ccc.rs" },
"class": "uncovered",
"detail": "was open at bbb, absent from the ccc batch",
"created_at": "200"
}
]
},
{
"key": { "binding_hash": "hashCUR", "source_head": "src=ccc" },
"recorded_at": "300",
"findings": [
{
"key": { "binding_hash": "hashCUR", "source_head": "src=ccc" },
"facet": "src",
"target": { "kind": "anchor", "entity": "engine--e", "artifact": "src/x.rs" },
"class": "unresolvable-anchor",
"detail": "gone",
"created_at": "300"
}
]
}
]
}"#,
)
.unwrap();
let mut store = read_findings_store(root, "engine", "graph")
.unwrap()
.expect("the legacy on-disk format loads as-is");
assert_eq!(store.binding, "engine/graph");
assert_eq!(store.batches.len(), 3, "loaded without loss");
let now = key("hashCUR", "src=ddd");
let current = store.current(&now);
assert_eq!(current.len(), 1);
assert_eq!(current[0].detail, "gone");
assert_eq!(
current[0].key.source_head, "src=ccc",
"the finding keeps the head it was observed at"
);
let superseded = store.superseded(&now);
assert_eq!(superseded.len(), 2);
assert!(
!current.iter().any(|f| f.detail.contains("was open at bbb")),
"the older same-hash batch was superseded at write time and is not resurrected"
);
store.record(now.clone(), "400".to_string(), Vec::new());
assert_eq!(store.batches.len(), 2, "hashCUR collapsed, hashOLD kept");
assert_eq!(store.superseded(&now).len(), 1);
}
#[test]
fn merge_carries_unobserved_open_findings_and_closes_departed() {
let k_old = key("h", "head1");
let mk_artifact = |artifact: &str, detail: &str| Finding {
key: k_old.clone(),
facet: "src".to_string(),
target: FindingTarget::Artifact {
artifact: artifact.to_string(),
},
class: FindingClass::Uncovered,
detail: detail.to_string(),
created_at: "1".to_string(),
};
let anchor_finding = Finding {
key: k_old.clone(),
facet: "src".to_string(),
target: FindingTarget::Anchor {
entity: "engine--gone".to_string(),
artifact: "src/gone.rs".to_string(),
},
class: FindingClass::UnresolvableAnchor,
detail: "anchor since removed from the mem".to_string(),
created_at: "1".to_string(),
};
let prior = vec![
mk_artifact("src/unsampled.rs", "still open, not in this window"),
mk_artifact("src/departed.rs", "left S(D)"),
mk_artifact("src/now-covered.rs", "gained an anchor since"),
mk_artifact("src/observed-clean.rs", "re-sampled and now covered"),
anchor_finding,
];
let obs = PassObservation {
anchors_observed: BTreeSet::new(),
anchors_existing: BTreeSet::new(), files_observed: ["src/observed-clean.rs".to_string()].into(),
s_d: [
"src/unsampled.rs".to_string(),
"src/now-covered.rs".to_string(),
"src/observed-clean.rs".to_string(),
]
.into(),
};
let merged = merge_with_prior(Vec::new(), &prior, &obs, |artifact| {
artifact == "src/now-covered.rs" || artifact == "src/observed-clean.rs"
});
assert_eq!(merged.len(), 1, "only the still-open unsampled one carries");
assert_eq!(
merged[0].target,
FindingTarget::Artifact {
artifact: "src/unsampled.rs".to_string()
}
);
assert_eq!(
merged[0].key.source_head, "head1",
"a carried finding keeps the head it was observed at"
);
}
#[test]
fn merge_deferral_never_downgrades_prior_adjudication() {
let k_old = key("h", "head1");
let k_new = key("h", "head2");
let target = FindingTarget::Anchor {
entity: "engine--e".to_string(),
artifact: "src/x.rs".to_string(),
};
let prior_drifted = Finding {
key: k_old.clone(),
facet: "src".to_string(),
target: target.clone(),
class: FindingClass::Drifted,
detail: "adjudicated drifted at head1".to_string(),
created_at: "1".to_string(),
};
let fresh_queued = Finding {
key: k_new.clone(),
facet: "src".to_string(),
target: target.clone(),
class: FindingClass::QueuedForAdjudication,
detail: "deferred by the cap this run".to_string(),
created_at: "2".to_string(),
};
let obs = PassObservation {
anchors_observed: [target_key(&target)].into(),
anchors_existing: [target_key(&target)].into(),
files_observed: BTreeSet::new(),
s_d: BTreeSet::new(),
};
let merged = merge_with_prior(
vec![fresh_queued],
std::slice::from_ref(&prior_drifted),
&obs,
|_| true,
);
assert_eq!(merged.len(), 1);
assert_eq!(
merged[0].class,
FindingClass::Drifted,
"the prior verdict stands over a deferral"
);
assert_eq!(merged[0].key.source_head, "head1");
}
#[test]
fn informed_by_anchor_never_drifts() {
let k = key("h", "s");
for class in [
AnchorProvenanceClass::InformedBy,
AnchorProvenanceClass::Authored,
] {
let a = anchor(class);
assert!(
adjudicate_anchor(&k, "f", "engine--e", &a, AnchorState::Drifted, "1").is_none(),
"{class:?} must not produce a drift finding"
);
assert!(
adjudicate_anchor(&k, "f", "engine--e", &a, AnchorState::Recheck, "1").is_none(),
"{class:?} must not produce a queued finding"
);
}
}
#[test]
fn hash_bearing_drifts_and_orphan_is_class_independent() {
let k = key("h", "s");
let anchored = anchor(AnchorProvenanceClass::Anchored);
let drifted =
adjudicate_anchor(&k, "f", "engine--e", &anchored, AnchorState::Drifted, "1").unwrap();
assert_eq!(drifted.class, FindingClass::Drifted);
assert_eq!(drifted.key, k, "the finding carries its recording key (A2)");
let queued =
adjudicate_anchor(&k, "f", "engine--e", &anchored, AnchorState::Recheck, "1").unwrap();
assert_eq!(queued.class, FindingClass::QueuedForAdjudication);
let informed = anchor(AnchorProvenanceClass::InformedBy);
let orphan =
adjudicate_anchor(&k, "f", "engine--e", &informed, AnchorState::Orphaned, "1").unwrap();
assert_eq!(orphan.class, FindingClass::UnresolvableAnchor);
assert!(
adjudicate_anchor(&k, "f", "engine--e", &anchored, AnchorState::Resolves, "1")
.is_none()
);
}
#[test]
fn finding_class_wire_round_trips() {
for w in FindingClass::WIRE_VALUES {
let c = FindingClass::from_wire(w).expect("known wire value");
assert_eq!(c.as_wire(), *w);
}
assert!(FindingClass::from_wire("nonsense").is_none());
}
#[test]
fn malformed_binding_id_refuses() {
assert!(matches!(
split_binding_id("../escape"),
Err(FindingsError::MalformedId(_))
));
assert!(matches!(
split_binding_id("no-slash"),
Err(FindingsError::MalformedId(_))
));
assert_eq!(
split_binding_id("engine/graph").unwrap(),
("engine".to_string(), "graph".to_string())
);
}
use crate::anchor::AnchorSidecar;
use crate::binding::{
BINDING_VERSION, BuildMode, BuildOperation, CoverageSemantics, DEFAULT_ADJUDICATION_CAP,
DEFAULT_FULL_RESYNC_EVERY, Operations, VerifyOperation,
};
use crate::ingest::resolve::resolve_binding_run;
use crate::pipeline::{IngestTrigger, MediumType, PatternEntry, PatternMode};
use crate::pipeline_store::{load_pipeline_configs, write_binding};
use crate::workspace::{
Mount, MountCapability, MountLifecycle, MountStorage, Workspace, WorkspaceSettings,
};
use crate::workspace_store::WorkspaceStoreAdapter;
#[test]
fn verify_persists_findings_readable_fresh() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let out = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(root)
.output()
.unwrap();
assert!(out.status.success());
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
std::fs::write(root.join("src").join("uncovered.rs"), "fn b() {}\n").unwrap();
let mk = |artifact: &str, class: AnchorProvenanceClass| Anchor {
artifact: artifact.to_string(),
grain: AnchorGrain::File,
class,
at_version: None,
hash: class.is_hash_bearing().then(|| "recorded".to_string()),
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
};
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"engine--e",
vec![
mk("src/present.rs", AnchorProvenanceClass::Anchored), mk("src/gone.rs", AnchorProvenanceClass::Anchored), mk("src/present.rs", AnchorProvenanceClass::InformedBy), ],
);
std::fs::write(
mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
sidecar.to_bytes(),
)
.unwrap();
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: Some(BuildOperation {
mode: BuildMode::Discovery,
trigger: IngestTrigger::Loop,
batch_size: 20,
post_actions: None,
}),
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let engine = Engine::from_workspace_root(root).unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
assert!(
outcome.recorded >= 3,
"orphan + drifted + uncovered at least"
);
assert_eq!(outcome.superseded, 0, "no prior key yet");
assert_eq!(
outcome.backlog, 0,
"the mismatching hash adjudicated deterministically — nothing queued"
);
assert!(
outcome.hash_backfill.is_empty(),
"every hash-bearing anchor already carries a recorded hash — nothing to backfill"
);
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
let current = store.current(&outcome.key);
assert_eq!(current.len(), outcome.recorded);
let has = |c: FindingClass, art: &str| {
current.iter().any(|f| {
f.class == c
&& match &f.target {
FindingTarget::Anchor { artifact, .. } => artifact == art,
FindingTarget::Artifact { artifact } => artifact == art,
}
})
};
assert!(has(FindingClass::UnresolvableAnchor, "src/gone.rs"));
assert!(
has(FindingClass::Drifted, "src/present.rs"),
"recorded-hash mismatch on a stable medium adjudicates drifted deterministically"
);
assert!(has(FindingClass::Uncovered, "src/uncovered.rs"));
assert!(
!current
.iter()
.any(|f| f.class == FindingClass::QueuedForAdjudication
|| f.class == FindingClass::Wrong),
"deterministic adjudication leaves nothing queued"
);
assert!(!has(FindingClass::Uncovered, "src/present.rs"));
}
#[test]
fn finding_recorded_at_old_head_presents_in_brief_at_new_head() {
use crate::ingest::render::render_sync_brief_for;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let git = |args: &[&str]| {
let out = std::process::Command::new("git")
.args(args)
.current_dir(root)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
};
git(&["init", "-q"]);
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "head-a"]);
let mk = |artifact: &str| Anchor {
artifact: artifact.to_string(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::InformedBy,
at_version: None,
hash: None,
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
};
let mut sidecar = AnchorSidecar::default();
sidecar.set("engine--e", vec![mk("src/present.rs"), mk("src/gone.rs")]);
std::fs::write(
mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
sidecar.to_bytes(),
)
.unwrap();
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: None,
sync: Some(crate::binding::SyncOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
}),
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
let head_a_outcome = {
let engine = Engine::from_workspace_root(root).unwrap();
verify_binding(&engine, root, binding, &resolved).unwrap()
};
assert!(
head_a_outcome.key.source_head.contains("graph="),
"the run observed a facet head"
);
std::fs::write(root.join("src").join("present.rs"), "fn a() {} // more\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "head-b"]);
{
let engine = Engine::from_workspace_root(root).unwrap();
let (key_b, findings) = current_findings(&engine, root, binding, &resolved).unwrap();
assert_ne!(
key_b.source_head, head_a_outcome.key.source_head,
"the head really moved"
);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].class, FindingClass::UnresolvableAnchor);
assert_eq!(
findings[0].key.source_head, head_a_outcome.key.source_head,
"the finding still records the head it was observed at"
);
let brief = render_sync_brief_for(&engine, root, "engine/graph").unwrap();
assert!(brief.contains("## Open findings to repair"));
assert!(brief.contains("src/gone.rs"));
}
std::fs::write(root.join("src").join("gone.rs"), "fn g() {}\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "head-c"]);
{
let engine = Engine::from_workspace_root(root).unwrap();
verify_binding(&engine, root, binding, &resolved).unwrap();
}
{
let engine = Engine::from_workspace_root(root).unwrap();
let (_key, findings) = current_findings(&engine, root, binding, &resolved).unwrap();
assert!(
findings
.iter()
.all(|f| f.class != FindingClass::UnresolvableAnchor),
"the resolved orphan finding must not re-present: {findings:?}"
);
}
}
#[test]
fn hashless_anchor_backfills_once_then_drift_adjudicates_deterministically() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let git = |args: &[&str]| {
let out = std::process::Command::new("git")
.args(args)
.current_dir(root)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
};
git(&["init", "-q"]);
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src").join("present.rs"), "fn a() {}\n").unwrap();
std::fs::write(root.join("src").join("other.rs"), "fn o() {}\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "head-a"]);
let mk = |artifact: &str, class: AnchorProvenanceClass, stab: AnchorHashStability| Anchor {
artifact: artifact.to_string(),
grain: AnchorGrain::File,
class,
at_version: None,
hash: None,
hash_stability: stab,
derived_from: if class == AnchorProvenanceClass::Derived {
vec!["src/present.rs".to_string()]
} else {
Vec::new()
},
binding: None,
source: None,
};
use AnchorHashStability::{Stable, Unstable};
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"engine--e",
vec![
mk("src/present.rs", AnchorProvenanceClass::Anchored, Stable),
mk("src/present.rs", AnchorProvenanceClass::Derived, Stable),
mk("src/other.rs", AnchorProvenanceClass::Anchored, Unstable),
mk("src/present.rs", AnchorProvenanceClass::Authored, Stable),
mk("src/present.rs", AnchorProvenanceClass::InformedBy, Stable),
],
);
std::fs::write(
mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
sidecar.to_bytes(),
)
.unwrap();
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: None,
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
{
let mut engine = Engine::from_workspace_root(root).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
let mut backfilled: Vec<(&str, &str)> = outcome
.hash_backfill
.iter()
.map(|b| (b.entity.as_str(), b.artifact.as_str()))
.collect();
backfilled.sort();
backfilled.dedup();
assert_eq!(
backfilled,
vec![
("engine--e", "src/other.rs"),
("engine--e", "src/present.rs"),
],
"hash-bearing anchors backfill; authored/informed-by never appear"
);
assert_eq!(
outcome.backlog, 0,
"no recheck queue for backfilled anchors"
);
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
assert!(
store
.current(&outcome.key)
.iter()
.all(|f| !matches!(f.target, FindingTarget::Anchor { .. })),
"no anchor finding on the backfill pass: {:?}",
store.current(&outcome.key)
);
let written =
record_anchor_hash_backfill(&mut engine, "engine", &outcome, None).unwrap();
assert_eq!(
written, 3,
"anchored + derived + unstable-anchored gain hashes"
);
}
let expected_present = crate::anchor::prepared_content_hash(
&std::fs::read(root.join("src").join("present.rs")).unwrap(),
);
{
let sc = AnchorSidecar::from_bytes(
&std::fs::read(mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH)).unwrap(),
)
.unwrap();
for a in sc.get("engine--e") {
if a.class.is_hash_bearing() {
assert!(a.hash.is_some(), "hash-bearing anchor backfilled: {a:?}");
} else {
assert!(a.hash.is_none(), "non-hash class never gains a hash: {a:?}");
}
if a.artifact == "src/present.rs" && a.class.is_hash_bearing() {
assert_eq!(a.hash.as_deref(), Some(expected_present.as_str()));
}
}
}
{
let mut engine = Engine::from_workspace_root(root).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
assert!(
outcome.hash_backfill.is_empty(),
"backfill happens once — a re-verify observes an empty worklist"
);
assert_eq!(outcome.backlog, 0, "steady state: nothing re-queues");
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
assert!(
store
.current(&outcome.key)
.iter()
.all(|f| !matches!(f.target, FindingTarget::Anchor { .. })),
"recorded hashes match the source — no anchor finding"
);
let written =
record_anchor_hash_backfill(&mut engine, "engine", &outcome, None).unwrap();
assert_eq!(written, 0, "no write, no commit on the idempotent pass");
}
std::fs::write(
root.join("src").join("present.rs"),
"fn a() { /* changed */ }\n",
)
.unwrap();
std::fs::write(
root.join("src").join("other.rs"),
"fn o() { /* changed */ }\n",
)
.unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "head-b"]);
{
let engine = Engine::from_workspace_root(root).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
assert!(
outcome.hash_backfill.is_empty(),
"recorded hashes are never overwritten by observation"
);
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
let current = store.current(&outcome.key);
let drifted: Vec<&Finding> = current
.iter()
.filter(|f| f.class == FindingClass::Drifted)
.collect();
assert_eq!(
drifted.len(),
2,
"stable-medium mismatch → drifted: {current:?}"
);
assert!(drifted.iter().all(|f| matches!(
&f.target,
FindingTarget::Anchor { artifact, .. } if artifact == "src/present.rs"
)));
assert!(
current
.iter()
.any(|f| f.class == FindingClass::QueuedForAdjudication
&& matches!(
&f.target,
FindingTarget::Anchor { artifact, .. } if artifact == "src/other.rs"
)),
"unstable medium resolves recheck (queued), not drifted: {current:?}"
);
assert!(
!current.iter().any(|f| f.class == FindingClass::Drifted
&& matches!(
&f.target,
FindingTarget::Anchor { artifact, .. } if artifact == "src/other.rs"
)),
"an unstable hash break must never assert drift"
);
}
}
#[test]
fn backfill_writer_refuses_non_hash_classes_and_never_overwrites() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
}],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let anchor = |class: AnchorProvenanceClass, hash: Option<&str>| Anchor {
artifact: "src/a.rs".to_string(),
grain: AnchorGrain::File,
class,
at_version: None,
hash: hash.map(str::to_string),
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
};
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"engine--e",
vec![
anchor(AnchorProvenanceClass::Authored, None),
anchor(AnchorProvenanceClass::InformedBy, None),
anchor(AnchorProvenanceClass::Anchored, Some("recorded")),
],
);
std::fs::write(
mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
sidecar.to_bytes(),
)
.unwrap();
let mut engine = Engine::from_workspace_root(root).unwrap();
let written = engine
.record_anchor_observed_hashes(
"engine",
&[crate::anchor::ObservedArtifactHash {
entity: "engine--e".to_string(),
artifact: "src/a.rs".to_string(),
hash: "observed".to_string(),
}],
None,
)
.unwrap();
assert_eq!(
written, 0,
"non-hash classes refuse the hash; a recorded hash is never overwritten"
);
let sc = AnchorSidecar::from_bytes(
&std::fs::read(mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH)).unwrap(),
)
.unwrap();
for a in sc.get("engine--e") {
match a.class {
AnchorProvenanceClass::Anchored => {
assert_eq!(a.hash.as_deref(), Some("recorded"), "baseline stands")
}
_ => assert!(a.hash.is_none(), "non-hash class stays hash-less: {a:?}"),
}
}
}
#[test]
fn verify_refuses_unreachable_source_with_typed_error() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
write_binding(
root,
"engine",
"gone",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "gone".to_string(),
medium_type: MediumType::Codebase,
pointer: "vanished-src".to_string(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: None,
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let engine = Engine::from_workspace_root(root).unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/gone", binding).unwrap();
match verify_binding(&engine, root, binding, &resolved) {
Err(FindingsError::SourceUnreachable { source_name, path }) => {
assert_eq!(source_name, "gone");
assert!(
path.ends_with("vanished-src"),
"refusal must name the resolved missing path, got `{path}`",
);
}
other => panic!("expected SourceUnreachable refusal, got {other:?}"),
}
assert!(
!engine
.mem_config_for("engine")
.unwrap()
.sync_state
.keys()
.any(|k| k.ends_with("#verified")),
"a refused verify must not leave any #verified token",
);
}
#[test]
fn completed_verify_records_the_verified_baseline() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let out = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(root)
.output()
.unwrap();
assert!(out.status.success());
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: Some(BuildOperation {
mode: BuildMode::Discovery,
trigger: IngestTrigger::Loop,
batch_size: 20,
post_actions: None,
}),
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let mut engine = Engine::from_workspace_root(root).unwrap();
engine
.set_mem_sync_state("engine", "engine/graph/graph#synced", "deadbeef", None)
.unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
assert_eq!(
outcome.facet_heads.get("graph").map(String::as_str),
Some("deadbeef")
);
assert_eq!(outcome.key.source_head, "graph=deadbeef");
assert_eq!(
join_facet_heads(&outcome.facet_heads),
outcome.key.source_head
);
assert!(
!engine
.mem_config_for("engine")
.unwrap()
.sync_state
.contains_key("engine/graph/graph#verified")
);
let written = record_verified_baseline(&mut engine, "engine", &outcome, None).unwrap();
assert_eq!(written, vec!["engine/graph/graph#verified".to_string()]);
assert_eq!(
engine
.mem_config_for("engine")
.unwrap()
.sync_state
.get("engine/graph/graph#verified")
.map(String::as_str),
Some("deadbeef")
);
let disk: serde_json::Value = serde_json::from_slice(
&std::fs::read(mem_dir.join(".memstead").join("config.json")).unwrap(),
)
.unwrap();
assert_eq!(
disk["syncState"]["engine/graph/graph#verified"],
serde_json::json!("deadbeef")
);
}
#[test]
fn adjudication_cap_queues_the_remainder() {
let k = key("h", "s");
let mk = |art: &str| {
let mut a = anchor(AnchorProvenanceClass::Anchored);
a.artifact = art.to_string();
a
};
let candidates = vec![
(
"engine--a".to_string(),
mk("src/a.rs"),
AnchorState::Drifted,
),
(
"engine--b".to_string(),
mk("src/b.rs"),
AnchorState::Drifted,
),
(
"engine--c".to_string(),
mk("src/c.rs"),
AnchorState::Drifted,
),
];
let window: BTreeSet<String> = [candidate_key("engine--a", &mk("src/a.rs"))]
.into_iter()
.collect();
let out = adjudicate_candidates(&k, "f", &candidates, Some(&window), "1");
let drifted = out
.iter()
.filter(|f| f.class == FindingClass::Drifted)
.count();
let queued = out
.iter()
.filter(|f| f.class == FindingClass::QueuedForAdjudication)
.count();
assert_eq!(drifted, 1, "only the in-window candidate is adjudicated");
assert_eq!(queued, 2, "the remainder is queued as the tier-3 backlog");
assert!(
out.iter()
.any(|f| f.class == FindingClass::QueuedForAdjudication
&& f.detail.contains("cap reached")),
"capped remainder states it was deferred by the cap"
);
let uncapped = adjudicate_candidates(&k, "f", &candidates, None, "1");
assert_eq!(
uncapped
.iter()
.filter(|f| f.class == FindingClass::Drifted)
.count(),
3,
"uncapped adjudicates every candidate"
);
assert_eq!(
uncapped
.iter()
.filter(|f| f.class == FindingClass::QueuedForAdjudication)
.count(),
0
);
}
#[test]
fn full_resync_schedule_disabled_notdue_due() {
let codebase = FacetEnumerability {
facet: "src".to_string(),
medium_type: "codebase".to_string(),
enumerable: true,
};
assert_eq!(
schedule_full_resync(0, 5, std::slice::from_ref(&codebase)),
FullResyncDecision::Disabled
);
match schedule_full_resync(3, 2, std::slice::from_ref(&codebase)) {
FullResyncDecision::NotDue { runs_until_due, .. } => assert_eq!(runs_until_due, 1),
other => panic!("expected NotDue, got {other:?}"),
}
match schedule_full_resync(3, 3, std::slice::from_ref(&codebase)) {
FullResyncDecision::Due {
walked_facets,
refused,
..
} => {
assert_eq!(walked_facets, vec!["src".to_string()]);
assert!(refused.is_empty(), "enumerable facet is not refused");
}
other => panic!("expected Due, got {other:?}"),
}
}
#[test]
fn full_resync_refuses_non_enumerable_medium() {
let web = FacetEnumerability {
facet: "manual".to_string(),
medium_type: "web".to_string(),
enumerable: false,
};
let d = schedule_full_resync(1, 1, &[web]);
assert!(
d.is_full_walk(),
"a due sweep is a full walk even when refused"
);
match d {
FullResyncDecision::Due {
walked_facets,
refused,
..
} => {
assert!(walked_facets.is_empty(), "nothing enumerable to walk");
assert_eq!(refused.len(), 1, "the non-enumerable facet is refused");
assert_eq!(refused[0].facet, "manual");
assert_eq!(refused[0].medium_type, "web");
assert!(
refused[0].reason.contains("non-enumerable"),
"the refusal is typed and states why"
);
}
other => panic!("expected Due with a refusal, got {other:?}"),
}
}
#[test]
fn full_resync_full_walk_covers_whole_source() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
let mount = Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
};
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![mount],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let out = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(root)
.output()
.unwrap();
assert!(out.status.success());
std::fs::create_dir_all(root.join("src")).unwrap();
for f in ["a.rs", "b.rs", "c.rs"] {
std::fs::write(root.join("src").join(f), "fn x() {}\n").unwrap();
}
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: Some(BuildOperation {
mode: BuildMode::Discovery,
trigger: IngestTrigger::Loop,
batch_size: 20,
post_actions: None,
}),
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 1, adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: 1, }),
},
},
)
.unwrap();
let engine = Engine::from_workspace_root(root).unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
let outcome = verify_binding(&engine, root, binding, &resolved).unwrap();
match &outcome.full_resync {
FullResyncDecision::Due {
walked_facets,
refused,
run_count,
..
} => {
assert_eq!(*run_count, 1);
assert_eq!(walked_facets, &vec!["graph".to_string()]);
assert!(refused.is_empty());
}
other => panic!("expected a due full walk, got {other:?}"),
}
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
let uncovered = store
.current(&outcome.key)
.iter()
.filter(|f| f.class == FindingClass::Uncovered)
.count();
assert_eq!(
uncovered, 3,
"the scheduled full walk covers the whole source, not a batch of one"
);
}
#[test]
fn full_verify_uncaps_adjudication_and_walks_whole_source() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
}],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
let out = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(root)
.output()
.unwrap();
assert!(out.status.success());
std::fs::create_dir_all(root.join("src")).unwrap();
for f in ["a.rs", "b.rs", "c.rs", "d.rs", "e.rs", "f.rs"] {
std::fs::write(root.join("src").join(f), "fn x() {}\n").unwrap();
}
let mk = |art: &str| Anchor {
artifact: art.to_string(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Anchored,
at_version: None,
hash: Some("stale-recorded-hash".to_string()), hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
};
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"engine--e",
vec![mk("src/a.rs"), mk("src/b.rs"), mk("src/c.rs")],
);
std::fs::write(
mem_dir.join(crate::anchor::ANCHOR_SIDECAR_PATH),
sidecar.to_bytes(),
)
.unwrap();
write_binding(
root,
"engine",
"graph",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "graph".to_string(),
medium_type: MediumType::Codebase,
pointer: String::new(),
change_detection: Some("git".to_string()),
scope: vec![PatternEntry {
path: "src/**/*.rs".to_string(),
mode: PatternMode::Allow,
}],
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: None,
rules: None,
prune: None,
operations: Operations {
build: None,
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 1, adjudication_cap: 1, full_resync_every: 0, }),
},
},
)
.unwrap();
let engine = Engine::from_workspace_root(root).unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/graph", binding).unwrap();
let sampled = verify_binding(&engine, root, binding, &resolved).unwrap();
assert_eq!(sampled.full_resync, FullResyncDecision::Disabled);
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
let current = store.current(&sampled.key);
let count = |c: FindingClass| current.iter().filter(|f| f.class == c).count();
assert_eq!(count(FindingClass::Drifted), 1, "cap-1 adjudicates one");
assert_eq!(
count(FindingClass::QueuedForAdjudication),
2,
"the remainder queues"
);
assert!(
current
.iter()
.any(|f| f.class == FindingClass::QueuedForAdjudication
&& f.detail.contains("cap reached")),
"the sampled deferral states the cap"
);
assert!(
count(FindingClass::Uncovered) <= 1,
"batch-1 sample looks at one artifact"
);
let full = verify_binding_full(&engine, root, binding, &resolved).unwrap();
assert_eq!(
full.full_resync,
FullResyncDecision::Forced {
walked_facets: vec!["graph".to_string()]
}
);
assert_eq!(full.backlog, 0, "cap treated as unlimited — no backlog");
let store = read_findings_store(root, "engine", "graph")
.unwrap()
.unwrap();
let current = store.current(&full.key);
let count = |c: FindingClass| current.iter().filter(|f| f.class == c).count();
assert_eq!(
count(FindingClass::Drifted),
3,
"every candidate adjudicated"
);
assert_eq!(count(FindingClass::QueuedForAdjudication), 0);
assert_eq!(
count(FindingClass::Uncovered),
3,
"the whole S(D) walked — every uncovered file flagged"
);
assert!(
current.iter().all(|f| !f.detail.contains("cap reached")),
"a full run's findings carry no cap-deferral caveat"
);
}
#[test]
fn full_verify_refuses_non_enumerable_medium_typed() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mem_dir = root.join("mem");
std::fs::create_dir_all(mem_dir.join(".memstead")).unwrap();
std::fs::write(
mem_dir.join(".memstead").join("config.json"),
r#"{"format":1,"schema":"default@1.0.0","version":"1.0.0"}"#,
)
.unwrap();
std::fs::create_dir_all(root.join(".memstead")).unwrap();
std::fs::write(
root.join(".memstead").join("workspace.toml"),
"format = \"memstead-git-branch-2\"\n\n[persistence_adapter]\nname = \"file-two-layer\"\n",
)
.unwrap();
crate::FileWorkspaceStore::new()
.save_state(
root,
&Workspace {
mounts: vec![Mount {
mem: "engine".to_string(),
schema: Some("default@1.0.0".parse().unwrap()),
storage: MountStorage::Folder {
path: mem_dir.clone(),
},
capability: MountCapability::Write,
lifecycle: MountLifecycle::Eager,
cross_linkable: false,
migration_target: None,
}],
settings: WorkspaceSettings::default(),
},
)
.unwrap();
write_binding(
root,
"engine",
"manual",
&Binding {
version: BINDING_VERSION,
intent: None,
sources: vec![crate::pipeline::Source {
name: "manual".to_string(),
medium_type: MediumType::Web,
pointer: "https://example.com/docs".to_string(),
change_detection: None,
scope: Vec::new(),
engagement: None,
preparation: None,
}],
reference_mems: Vec::new(),
destination_mem: "engine".to_string(),
deny_paths: Vec::new(),
coverage_semantics: Some(CoverageSemantics::Curated),
rules: None,
prune: None,
operations: Operations {
build: None,
sync: None,
verify: Some(VerifyOperation {
trigger: IngestTrigger::Manual,
batch_size: 20,
adjudication_cap: DEFAULT_ADJUDICATION_CAP,
full_resync_every: DEFAULT_FULL_RESYNC_EVERY,
}),
},
},
)
.unwrap();
let engine = Engine::from_workspace_root(root).unwrap();
let configs = load_pipeline_configs(root).unwrap();
let binding = &configs.bindings[0].config;
let resolved = resolve_binding_run("engine/manual", binding).unwrap();
let err = verify_binding_full(&engine, root, binding, &resolved).unwrap_err();
match &err {
FindingsError::FullWalkNonEnumerable(refusal) => {
assert_eq!(refusal.facet, "manual");
assert_eq!(refusal.medium_type, "web");
assert!(refusal.reason.contains("non-enumerable"));
}
other => panic!("expected FullWalkNonEnumerable, got {other:?}"),
}
assert!(
read_findings_store(root, "engine", "manual")
.unwrap()
.is_none(),
"a refused full run records nothing"
);
let sampled = verify_binding(&engine, root, binding, &resolved).unwrap();
assert_eq!(sampled.binding, "engine/manual");
}
}