use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub const ANCHOR_SIDECAR_PATH: &str = ".memstead/anchors.json";
pub const ANCHOR_SIDECAR_VERSION: u32 = 2;
pub const ANCHOR_SIDECAR_VERSIONS_READ: &[u32] = &[1, 2];
pub const INVALID_ANCHOR_CODE: &str = "INVALID_ANCHOR";
pub const REDACTED_ARTIFACT_SENTINEL: &str = "[redacted]";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AnchorProvenanceClass {
Anchored,
Derived,
Authored,
InformedBy,
}
impl AnchorProvenanceClass {
pub const WIRE_VALUES: &'static [&'static str] =
&["anchored", "derived", "authored", "informed-by"];
pub fn as_wire(&self) -> &'static str {
match self {
AnchorProvenanceClass::Anchored => "anchored",
AnchorProvenanceClass::Derived => "derived",
AnchorProvenanceClass::Authored => "authored",
AnchorProvenanceClass::InformedBy => "informed-by",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
match s {
"anchored" => Some(AnchorProvenanceClass::Anchored),
"derived" => Some(AnchorProvenanceClass::Derived),
"authored" => Some(AnchorProvenanceClass::Authored),
"informed-by" => Some(AnchorProvenanceClass::InformedBy),
_ => None,
}
}
pub fn is_hash_bearing(&self) -> bool {
matches!(
self,
AnchorProvenanceClass::Anchored | AnchorProvenanceClass::Derived
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnchorGrain {
Span,
File,
Tree,
Url,
Entity,
}
impl AnchorGrain {
pub const WIRE_VALUES: &'static [&'static str] = &["span", "file", "tree", "url", "entity"];
pub fn as_wire(&self) -> &'static str {
match self {
AnchorGrain::Span => "span",
AnchorGrain::File => "file",
AnchorGrain::Tree => "tree",
AnchorGrain::Url => "url",
AnchorGrain::Entity => "entity",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
match s {
"span" => Some(AnchorGrain::Span),
"file" => Some(AnchorGrain::File),
"tree" => Some(AnchorGrain::Tree),
"url" => Some(AnchorGrain::Url),
"entity" => Some(AnchorGrain::Entity),
_ => None,
}
}
pub fn supported_by_namespace(&self, anchor_namespace: &str) -> bool {
let path_shaped = matches!(anchor_namespace, "path" | "path+commit");
match self {
AnchorGrain::Span | AnchorGrain::File | AnchorGrain::Tree => path_shaped,
AnchorGrain::Url => true,
AnchorGrain::Entity => anchor_namespace == "entity",
}
}
pub fn is_path_shaped(&self) -> bool {
matches!(
self,
AnchorGrain::Span | AnchorGrain::File | AnchorGrain::Tree
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnchorHashStability {
Stable,
Unstable,
}
impl AnchorHashStability {
pub const WIRE_VALUES: &'static [&'static str] = &["stable", "unstable"];
pub fn as_wire(&self) -> &'static str {
match self {
AnchorHashStability::Stable => "stable",
AnchorHashStability::Unstable => "unstable",
}
}
pub fn from_wire(s: &str) -> Option<Self> {
match s {
"stable" => Some(AnchorHashStability::Stable),
"unstable" => Some(AnchorHashStability::Unstable),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
pub enum AnchorVersion {
Commit(String),
Snapshot(String),
Etag(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Anchor {
pub artifact: String,
pub grain: AnchorGrain,
pub class: AnchorProvenanceClass,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at_version: Option<AnchorVersion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
pub hash_stability: AnchorHashStability,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub derived_from: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binding: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub span_unvalidated: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash_source: Option<AnchorHashSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_observed: Option<AnchorObservation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorObservation {
pub at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
pub state: AnchorState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AnchorHashSource {
Author,
Backfill,
}
impl AnchorHashSource {
pub fn as_wire(self) -> &'static str {
match self {
AnchorHashSource::Author => "author",
AnchorHashSource::Backfill => "backfill",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanLocator<'a> {
Lines { start: usize, end: usize },
Unit(&'a str),
}
pub fn parse_span_locator(artifact: &str) -> Result<Option<SpanLocator<'_>>, &'static str> {
let locator = match artifact.split_once('#') {
None => return Ok(None),
Some((_, loc)) if loc.trim().is_empty() => {
return Err("the span locator after `#` is empty");
}
Some((_, loc)) => loc,
};
let looks_like_lines = locator.starts_with('L')
&& locator[1..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit());
if !looks_like_lines {
return Ok(Some(SpanLocator::Unit(locator)));
}
let (start_raw, end_raw) = match locator.split_once('-') {
None => (locator, locator),
Some((a, b)) => (a, b),
};
let num = |part: &str| -> Option<usize> {
part.strip_prefix('L')
.filter(|d| !d.is_empty() && d.chars().all(|c| c.is_ascii_digit()))
.and_then(|d| d.parse::<usize>().ok())
};
let (Some(start), Some(end)) = (num(start_raw), num(end_raw)) else {
return Err("a line-range span locator must read `L<start>` or `L<start>-L<end>`");
};
if start == 0 {
return Err("line numbers are 1-based, so `L0` addresses nothing");
}
if end < start {
return Err("a line-range span locator ends before it starts");
}
Ok(Some(SpanLocator::Lines { start, end }))
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnchorInput {
#[serde(default)]
pub artifact: Option<String>,
#[serde(default)]
pub grain: Option<String>,
#[serde(default)]
pub class: Option<String>,
#[serde(default)]
pub at_version: Option<AnchorVersion>,
#[serde(default)]
pub hash: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub hash_stability: Option<String>,
#[serde(default)]
pub derived_from: Option<Vec<String>>,
#[serde(default)]
pub binding: Option<String>,
#[serde(default)]
pub source: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnchorUnsetInput {
#[serde(default)]
pub artifact: Option<String>,
#[serde(default)]
pub grain: Option<String>,
#[serde(default)]
pub class: Option<String>,
}
impl AnchorUnsetInput {
pub fn validate(&self) -> Result<AnchorUnset, AnchorValidationError> {
let artifact = self
.artifact
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.ok_or(AnchorValidationError::MissingArtifact)?;
let grain = match self.grain.as_deref() {
None => None,
Some(s) => Some(AnchorGrain::from_wire(s).ok_or_else(|| {
AnchorValidationError::UnknownGrain {
got: Some(s.to_string()),
allowed: AnchorGrain::WIRE_VALUES,
}
})?),
};
let class = match self.class.as_deref() {
None => None,
Some(s) => Some(AnchorProvenanceClass::from_wire(s).ok_or_else(|| {
AnchorValidationError::UnknownClass {
got: Some(s.to_string()),
allowed: AnchorProvenanceClass::WIRE_VALUES,
}
})?),
};
Ok(AnchorUnset {
artifact,
grain,
class,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnchorUnset {
pub artifact: String,
pub grain: Option<AnchorGrain>,
pub class: Option<AnchorProvenanceClass>,
}
impl AnchorUnset {
pub fn matches(&self, anchor: &Anchor) -> bool {
anchor.artifact == self.artifact
&& self.grain.is_none_or(|g| anchor.grain == g)
&& self.class.is_none_or(|c| anchor.class == c)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AnchorValidationError {
#[error("unknown anchor provenance class {got:?}; allowed: {}", allowed.join(", "))]
UnknownClass {
got: Option<String>,
allowed: &'static [&'static str],
},
#[error("unknown anchor grain {got:?}; allowed: {}", allowed.join(", "))]
UnknownGrain {
got: Option<String>,
allowed: &'static [&'static str],
},
#[error("unknown anchor hash stability {got:?}; allowed: {}", allowed.join(", "))]
UnknownHashStability {
got: String,
allowed: &'static [&'static str],
},
#[error("anchor is missing its artifact reference")]
MissingArtifact,
#[error("anchor class '{class}' carries no hash semantics — a content hash is not permitted")]
HashOnNonHashClass { class: &'static str },
#[error(
"anchor supplies both `hash` and `content`; supply one — the engine computes the hash from `content`"
)]
ContentAndHash,
#[error(
"anchor grain '{grain}' does not accept `content`: its prepared form is not computed \
from supplied bytes (accepted for span / file / url)"
)]
ContentNotAcceptedForGrain { grain: &'static str },
#[error(
"anchor artifact {artifact:?} names a delivery unit the supplied `content` does not \
yield; supply the whole file's content, or address a unit it contains"
)]
UnitAbsentFromContent { artifact: String },
#[error("anchor artifact {artifact:?} is not a usable span reference: {reason}")]
SpanLocatorUnusable {
artifact: String,
reason: &'static str,
},
#[error(
"anchor artifact {artifact:?} names lines the supplied `content` does not have \
(it has {lines} line(s)); address a range the artifact contains"
)]
SpanOutsideContent { artifact: String, lines: usize },
#[error(
"the anchors payload names {artifact:?} at grain `{grain}` and class `{class}` more \
than once; that triple is one row, so the repeats would silently collapse to the \
last one: send it once, or vary the grain or class"
)]
DuplicateAnchorTriple {
artifact: String,
grain: &'static str,
class: &'static str,
},
#[error("anchor `source`, when present, must be a non-empty source name")]
EmptySource,
#[error(
"anchor `source` {got:?} is not declared by the anchor's producing binding; \
declared sources: {}",
declared.join(", ")
)]
SourceNotDeclared { got: String, declared: Vec<String> },
#[error(
"anchor artifact {artifact:?} resolves under no candidate path (tried: {}); artifact \
paths are source-relative (joined onto the source's pointer) or workspace-relative — \
write the path exactly as the brief lists it",
candidates.join(", ")
)]
ArtifactUnresolvable {
artifact: String,
candidates: Vec<String>,
},
#[error(
"anchor grain '{grain}' is unsupported by a '{medium_type}' medium: its \
'{anchor_namespace}' namespace does not admit that grain"
)]
GrainNamespaceUnsupported {
grain: &'static str,
medium_type: String,
anchor_namespace: &'static str,
},
#[error(
"anchor grain '{grain}' selects within a path namespace, but its artifact '{artifact}' is a URL — a URL never enters a path namespace; use `grain: url` for the resource"
)]
PathGrainOnUrlArtifact {
grain: &'static str,
artifact: String,
},
}
pub fn looks_like_url(artifact: &str) -> bool {
let Some((scheme, rest)) = artifact.split_once("://") else {
return false;
};
!rest.is_empty()
&& scheme
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
}
impl AnchorValidationError {
pub fn code(&self) -> &'static str {
INVALID_ANCHOR_CODE
}
pub fn detail(&self) -> BTreeMap<String, serde_json::Value> {
let mut d = BTreeMap::new();
match self {
AnchorValidationError::UnknownClass { got, allowed } => {
d.insert("field".into(), "class".into());
d.insert("got".into(), serde_json::json!(got));
d.insert("allowed".into(), serde_json::json!(allowed));
}
AnchorValidationError::UnknownGrain { got, allowed } => {
d.insert("field".into(), "grain".into());
d.insert("got".into(), serde_json::json!(got));
d.insert("allowed".into(), serde_json::json!(allowed));
}
AnchorValidationError::UnknownHashStability { got, allowed } => {
d.insert("field".into(), "hash_stability".into());
d.insert("got".into(), serde_json::json!(got));
d.insert("allowed".into(), serde_json::json!(allowed));
}
AnchorValidationError::MissingArtifact => {
d.insert("field".into(), "artifact".into());
}
AnchorValidationError::EmptySource => {
d.insert("field".into(), "source".into());
}
AnchorValidationError::SourceNotDeclared { got, declared } => {
d.insert("field".into(), "source".into());
d.insert("got".into(), serde_json::json!(got));
d.insert("declared".into(), serde_json::json!(declared));
}
AnchorValidationError::HashOnNonHashClass { class } => {
d.insert("field".into(), "hash".into());
d.insert("class".into(), serde_json::json!(class));
}
AnchorValidationError::ContentAndHash => {
d.insert("field".into(), "content".into());
d.insert(
"expected".into(),
serde_json::json!("either `hash` or `content`, never both"),
);
}
AnchorValidationError::ContentNotAcceptedForGrain { grain } => {
d.insert("field".into(), "content".into());
d.insert("grain".into(), serde_json::json!(grain));
d.insert(
"accepted_grains".into(),
serde_json::json!(["span", "file", "url"]),
);
}
AnchorValidationError::UnitAbsentFromContent { artifact } => {
d.insert("field".into(), "content".into());
d.insert("got".into(), serde_json::json!(artifact));
}
AnchorValidationError::SpanLocatorUnusable { artifact, reason } => {
d.insert("field".into(), "artifact".into());
d.insert("got".into(), serde_json::json!(artifact));
d.insert("expected".into(), serde_json::json!(reason));
}
AnchorValidationError::SpanOutsideContent { artifact, lines } => {
d.insert("field".into(), "artifact".into());
d.insert("got".into(), serde_json::json!(artifact));
d.insert("content_lines".into(), serde_json::json!(lines));
}
AnchorValidationError::DuplicateAnchorTriple {
artifact,
grain,
class,
} => {
d.insert("field".into(), "anchors".into());
d.insert(
"got".into(),
serde_json::json!({ "artifact": artifact, "grain": grain, "class": class }),
);
d.insert(
"expected".into(),
serde_json::json!(
"each (artifact, grain, class) triple at most once per payload"
),
);
}
AnchorValidationError::ArtifactUnresolvable {
artifact,
candidates,
} => {
d.insert("field".into(), "artifact".into());
d.insert("got".into(), serde_json::json!(artifact));
d.insert("candidates_tried".into(), serde_json::json!(candidates));
d.insert(
"expected".into(),
serde_json::json!(
"a source-relative path (joined onto the source's pointer) or a \
workspace-relative path that resolves to an existing artifact"
),
);
}
AnchorValidationError::GrainNamespaceUnsupported {
grain,
medium_type,
anchor_namespace,
} => {
d.insert("field".into(), "grain".into());
d.insert("grain".into(), serde_json::json!(grain));
d.insert("medium_type".into(), serde_json::json!(medium_type));
d.insert(
"anchor_namespace".into(),
serde_json::json!(anchor_namespace),
);
}
AnchorValidationError::PathGrainOnUrlArtifact { grain, artifact } => {
d.insert("field".into(), "grain".into());
d.insert("grain".into(), serde_json::json!(grain));
d.insert("got".into(), serde_json::json!(artifact));
d.insert(
"expected".into(),
serde_json::json!(
"`grain: url` for a web resource — a URL never enters a path namespace"
),
);
}
}
d
}
}
impl AnchorInput {
pub fn validate(&self, medium: Option<(&str, &str)>) -> Result<Anchor, AnchorValidationError> {
let class = match self
.class
.as_deref()
.and_then(AnchorProvenanceClass::from_wire)
{
Some(c) => c,
None => {
return Err(AnchorValidationError::UnknownClass {
got: self.class.clone(),
allowed: AnchorProvenanceClass::WIRE_VALUES,
});
}
};
let grain = match self.grain.as_deref().and_then(AnchorGrain::from_wire) {
Some(g) => g,
None => {
return Err(AnchorValidationError::UnknownGrain {
got: self.grain.clone(),
allowed: AnchorGrain::WIRE_VALUES,
});
}
};
let artifact = self
.artifact
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.ok_or(AnchorValidationError::MissingArtifact)?;
let hash_stability = match self.hash_stability.as_deref() {
None => crate::preparation::default_hash_stability(grain),
Some(s) => AnchorHashStability::from_wire(s).ok_or_else(|| {
AnchorValidationError::UnknownHashStability {
got: s.to_string(),
allowed: AnchorHashStability::WIRE_VALUES,
}
})?,
};
let hash = self
.hash
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string);
if (hash.is_some() || self.content.is_some()) && !class.is_hash_bearing() {
return Err(AnchorValidationError::HashOnNonHashClass {
class: class.as_wire(),
});
}
let hash = match self.content.as_deref() {
None => hash,
Some(_) if hash.is_some() => return Err(AnchorValidationError::ContentAndHash),
Some(content) => {
match crate::preparation::supplied_content_hash(grain, content.as_bytes()) {
Some(h) => Some(h),
None => {
return Err(AnchorValidationError::ContentNotAcceptedForGrain {
grain: grain.as_wire(),
});
}
}
}
};
let mut span_unvalidated = false;
if grain == AnchorGrain::Span {
let locator = parse_span_locator(&artifact).map_err(|reason| {
AnchorValidationError::SpanLocatorUnusable {
artifact: artifact.clone(),
reason,
}
})?;
match (locator, self.content.as_deref()) {
(Some(SpanLocator::Lines { end, .. }), Some(content)) => {
let lines = content.lines().count();
if end > lines {
return Err(AnchorValidationError::SpanOutsideContent {
artifact: artifact.clone(),
lines,
});
}
}
(Some(SpanLocator::Unit(_)), Some(_)) => {}
(None, _) => {}
(Some(_), None) => span_unvalidated = true,
}
}
if grain.is_path_shaped() && looks_like_url(&artifact) {
return Err(AnchorValidationError::PathGrainOnUrlArtifact {
grain: grain.as_wire(),
artifact,
});
}
if let Some((medium_type, namespace)) = medium
&& !grain.supported_by_namespace(namespace)
{
let anchor_namespace = match namespace {
"path" => "path",
"path+commit" => "path+commit",
"entity" => "entity",
"url" => "url",
_ => "path",
};
return Err(AnchorValidationError::GrainNamespaceUnsupported {
grain: grain.as_wire(),
medium_type: medium_type.to_string(),
anchor_namespace,
});
}
let source = match self.source.as_deref() {
None => None,
Some(raw) => {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(AnchorValidationError::EmptySource);
}
Some(trimmed.to_string())
}
};
Ok(Anchor {
artifact,
grain,
class,
at_version: self.at_version.clone(),
hash_source: hash.is_some().then_some(AnchorHashSource::Author),
hash,
hash_stability,
derived_from: self.derived_from.clone().unwrap_or_default(),
binding: self
.binding
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string),
source,
span_unvalidated,
last_observed: None,
})
}
}
pub fn prepared_content_hash(bytes: &[u8]) -> String {
use sha2::{Digest as _, Sha256};
let digest = match std::str::from_utf8(bytes) {
Ok(text) => {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
Sha256::digest(normalized.trim_end_matches('\n').as_bytes())
}
Err(_) => Sha256::digest(bytes),
};
crate::hex_lower(&digest)[..16].to_string()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObservedArtifactHash {
pub entity: String,
pub artifact: String,
pub hash: String,
}
pub const INVALID_OBSERVATION_CODE: &str = "INVALID_OBSERVATION";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SuppliedObservationInput {
#[serde(default)]
pub artifact: Option<String>,
#[serde(default)]
pub hash: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub absent: Option<bool>,
#[serde(default)]
pub observed_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SuppliedOutcome {
Present { hash: String },
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuppliedObservation {
pub artifact: String,
pub at: String,
pub outcome: SuppliedOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ObservationValidationError {
#[error("observation row {row}: `artifact` is required and must be non-empty")]
MissingArtifact { row: usize },
#[error(
"observation row {row} (`{artifact}`): give exactly one of `hash`, `content`, or \
`absent: true`"
)]
OutcomeAmbiguous { row: usize, artifact: String },
#[error(
"observation row {row} (`{artifact}`): `observed_at` '{got}' is not an ISO-8601 \
timestamp (`YYYY-MM-DDTHH:MM:SSZ`) or date (`YYYY-MM-DD`)"
)]
BadTimestamp {
row: usize,
artifact: String,
got: String,
},
#[error("observation rows name `{artifact}` more than once (rows {first} and {second})")]
DuplicateArtifact {
artifact: String,
first: usize,
second: usize,
},
}
impl ObservationValidationError {
pub fn code(&self) -> &'static str {
INVALID_OBSERVATION_CODE
}
pub fn detail(&self) -> BTreeMap<String, serde_json::Value> {
let mut d = BTreeMap::new();
match self {
ObservationValidationError::MissingArtifact { row } => {
d.insert("row".into(), serde_json::json!(row));
d.insert("field".into(), "artifact".into());
}
ObservationValidationError::OutcomeAmbiguous { row, artifact } => {
d.insert("row".into(), serde_json::json!(row));
d.insert("artifact".into(), serde_json::json!(artifact));
d.insert(
"expected".into(),
serde_json::json!("exactly one of `hash`, `content`, `absent: true`"),
);
}
ObservationValidationError::BadTimestamp { row, artifact, got } => {
d.insert("row".into(), serde_json::json!(row));
d.insert("artifact".into(), serde_json::json!(artifact));
d.insert("field".into(), "observed_at".into());
d.insert("got".into(), serde_json::json!(got));
}
ObservationValidationError::DuplicateArtifact {
artifact,
first,
second,
} => {
d.insert("artifact".into(), serde_json::json!(artifact));
d.insert("rows".into(), serde_json::json!([first, second]));
}
}
d
}
}
fn timestamp_is_wellformed(ts: &str) -> bool {
let b = ts.as_bytes();
let date_ok = b.len() >= 10
&& b[..10].iter().enumerate().all(|(i, c)| {
if i == 4 || i == 7 {
*c == b'-'
} else {
c.is_ascii_digit()
}
});
if !date_ok {
return false;
}
if b.len() == 10 {
return true;
}
b.len() == 20
&& b[10] == b'T'
&& b[19] == b'Z'
&& b[11..19].iter().enumerate().all(|(i, c)| {
if i == 2 || i == 5 {
*c == b':'
} else {
c.is_ascii_digit()
}
})
}
pub fn validate_supplied_observations(
rows: &[SuppliedObservationInput],
now: &str,
) -> Result<BTreeMap<String, SuppliedObservation>, ObservationValidationError> {
let mut out: BTreeMap<String, SuppliedObservation> = BTreeMap::new();
let mut first_row: BTreeMap<String, usize> = BTreeMap::new();
for (i, row) in rows.iter().enumerate() {
let n = i + 1;
let artifact = row
.artifact
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or(ObservationValidationError::MissingArtifact { row: n })?
.to_string();
let absent = row.absent.unwrap_or(false);
let given = usize::from(row.hash.is_some())
+ usize::from(row.content.is_some())
+ usize::from(absent);
if given != 1 {
return Err(ObservationValidationError::OutcomeAmbiguous { row: n, artifact });
}
let at = match row.observed_at.as_deref().map(str::trim) {
None | Some("") => now.to_string(),
Some(ts) if timestamp_is_wellformed(ts) => ts.to_string(),
Some(ts) => {
return Err(ObservationValidationError::BadTimestamp {
row: n,
artifact,
got: ts.to_string(),
});
}
};
if let Some(first) = first_row.get(&artifact) {
return Err(ObservationValidationError::DuplicateArtifact {
artifact,
first: *first,
second: n,
});
}
let outcome = if absent {
SuppliedOutcome::Absent
} else if let Some(hash) = &row.hash {
SuppliedOutcome::Present {
hash: hash.trim().to_string(),
}
} else {
SuppliedOutcome::Present {
hash: prepared_content_hash(row.content.as_deref().unwrap_or_default().as_bytes()),
}
};
first_row.insert(artifact.clone(), n);
out.insert(
artifact.clone(),
SuppliedObservation {
artifact,
at,
outcome,
},
);
}
Ok(out)
}
pub fn iso_days_since_epoch(ts: &str) -> Option<i64> {
if !timestamp_is_wellformed(ts) {
return None;
}
let y: i64 = ts[..4].parse().ok()?;
let m: u32 = ts[5..7].parse().ok()?;
let d: u32 = ts[8..10].parse().ok()?;
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
}
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = ((m + 9) % 12) as i64;
let doy = (153 * mp + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
Some(era * 146097 + doe - 719468)
}
pub fn days_between(observed_at: &str, now: &str) -> Option<u64> {
let a = iso_days_since_epoch(observed_at)?;
let b = iso_days_since_epoch(now)?;
Some((b - a).max(0) as u64)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnchorState {
Resolves,
Drifted,
Recheck,
Orphaned,
}
impl AnchorState {
pub fn as_wire(&self) -> &'static str {
match self {
AnchorState::Resolves => "resolves",
AnchorState::Drifted => "drifted",
AnchorState::Recheck => "recheck",
AnchorState::Orphaned => "orphaned",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArtifactObservation {
Absent,
Present { current_hash: Option<String> },
}
pub fn resolve_anchor(anchor: &Anchor, observation: &ArtifactObservation) -> AnchorState {
let current_hash = match observation {
ArtifactObservation::Absent => return AnchorState::Orphaned,
ArtifactObservation::Present { current_hash } => current_hash,
};
if !anchor.class.is_hash_bearing() {
return AnchorState::Resolves;
}
match (&anchor.hash, current_hash) {
(Some(recorded), Some(current)) if recorded == current => AnchorState::Resolves,
(Some(_), Some(_)) => match anchor.hash_stability {
AnchorHashStability::Stable => AnchorState::Drifted,
AnchorHashStability::Unstable => AnchorState::Recheck,
},
_ => AnchorState::Recheck,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityAnchorComposition {
pub by_class: BTreeMap<String, usize>,
pub by_grain: BTreeMap<String, usize>,
pub derived_inputs: Vec<Vec<String>>,
pub tree_grain_artifacts: Vec<String>,
}
pub fn compose_entity_anchors(anchors: &[Anchor]) -> EntityAnchorComposition {
let mut comp = EntityAnchorComposition::default();
for a in anchors {
*comp
.by_class
.entry(a.class.as_wire().to_string())
.or_insert(0) += 1;
*comp
.by_grain
.entry(a.grain.as_wire().to_string())
.or_insert(0) += 1;
if a.class == AnchorProvenanceClass::Derived {
comp.derived_inputs.push(a.derived_from.clone());
}
if a.grain == AnchorGrain::Tree {
comp.tree_grain_artifacts.push(a.artifact.clone());
}
}
comp
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorSidecar {
pub version: u32,
#[serde(default)]
pub entities: BTreeMap<String, Vec<Anchor>>,
}
impl Default for AnchorSidecar {
fn default() -> Self {
Self {
version: ANCHOR_SIDECAR_VERSION,
entities: BTreeMap::new(),
}
}
}
impl AnchorSidecar {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
if bytes.iter().all(u8::is_ascii_whitespace) {
return Ok(Self::default());
}
let sidecar: Self = serde_json::from_slice(bytes)?;
if !ANCHOR_SIDECAR_VERSIONS_READ.contains(&sidecar.version) {
return Err(serde::de::Error::custom(format!(
"unsupported anchors sidecar version {} (this engine reads versions {}) — \
the file was written by a different engine; upgrade, or remove the sidecar \
to re-record anchors",
sidecar.version,
ANCHOR_SIDECAR_VERSIONS_READ
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ")
)));
}
let mut sidecar = sidecar;
sidecar.version = ANCHOR_SIDECAR_VERSION;
Ok(sidecar)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut s = serde_json::to_string_pretty(self).expect("anchor sidecar serialises");
s.push('\n');
s.into_bytes()
}
pub fn get(&self, entity_id: &str) -> &[Anchor] {
self.entities
.get(entity_id)
.map(Vec::as_slice)
.unwrap_or(&[])
}
pub fn set(&mut self, entity_id: &str, anchors: Vec<Anchor>) {
if anchors.is_empty() {
self.entities.remove(entity_id);
} else {
self.entities.insert(entity_id.to_string(), anchors);
}
}
pub fn merge(&mut self, entity_id: &str, unsets: &[AnchorUnset], incoming: Vec<Anchor>) {
let mut row = self.entities.remove(entity_id).unwrap_or_default();
row.retain(|a| !unsets.iter().any(|u| u.matches(a)));
for mut anchor in incoming {
match row.iter_mut().find(|e| {
e.artifact == anchor.artifact && e.grain == anchor.grain && e.class == anchor.class
}) {
Some(existing) => {
if anchor.hash.is_none()
&& let Some(kept) = existing.hash.clone()
{
anchor.hash = Some(kept);
anchor.hash_source = existing.hash_source;
}
*existing = anchor;
}
None => row.push(anchor),
}
}
if !row.is_empty() {
self.entities.insert(entity_id.to_string(), row);
}
}
pub fn redact_artifact_references(&mut self) {
for anchors in self.entities.values_mut() {
for anchor in anchors {
anchor.artifact = REDACTED_ARTIFACT_SENTINEL.to_string();
for input in &mut anchor.derived_from {
*input = REDACTED_ARTIFACT_SENTINEL.to_string();
}
}
}
}
pub fn validate_artifact_references(&self) -> Result<(), String> {
for (entity_id, anchors) in &self.entities {
for anchor in anchors {
if anchor.artifact.trim().is_empty() {
return Err(format!(
"entity `{entity_id}` carries an anchor with an empty artifact \
reference"
));
}
if anchor.derived_from.iter().any(|d| d.trim().is_empty()) {
return Err(format!(
"entity `{entity_id}` carries an anchor with an empty \
`derived_from` entry"
));
}
}
}
Ok(())
}
pub fn remove(&mut self, entity_id: &str) {
self.entities.remove(entity_id);
}
pub fn rename(&mut self, from: &str, to: &str) {
if let Some(anchors) = self.entities.remove(from) {
self.entities.insert(to.to_string(), anchors);
}
}
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn redaction_blanks_references_and_keeps_trust_metadata() {
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"m--alpha",
vec![
Anchor {
artifact: "src/lib.rs".into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Anchored,
at_version: Some(AnchorVersion::Commit("abc123".into())),
hash: Some("h1".into()),
hash_stability: AnchorHashStability::Stable,
derived_from: vec![],
binding: Some("bhash".into()),
source: Some("source-tree".into()),
span_unvalidated: false,
hash_source: None,
last_observed: None,
},
Anchor {
artifact: "docs/summary.md".into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Derived,
at_version: None,
hash: Some("h2".into()),
hash_stability: AnchorHashStability::Unstable,
derived_from: vec!["notes/a.md".into(), "notes/b.md".into()],
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
},
],
);
sidecar.redact_artifact_references();
let anchors = sidecar.get("m--alpha");
assert_eq!(anchors.len(), 2, "no anchor entry is dropped");
for a in anchors {
assert_eq!(a.artifact, REDACTED_ARTIFACT_SENTINEL);
for d in &a.derived_from {
assert_eq!(d, REDACTED_ARTIFACT_SENTINEL);
}
}
assert_eq!(
anchors[0].at_version,
Some(AnchorVersion::Commit("abc123".into()))
);
assert_eq!(anchors[0].hash.as_deref(), Some("h1"));
assert_eq!(anchors[0].binding.as_deref(), Some("bhash"));
assert_eq!(anchors[0].source.as_deref(), Some("source-tree"));
assert_eq!(anchors[1].class, AnchorProvenanceClass::Derived);
assert_eq!(anchors[1].derived_from.len(), 2, "derivation arity kept");
sidecar.validate_artifact_references().unwrap();
}
#[test]
fn empty_artifact_references_are_refused() {
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"m--alpha",
vec![Anchor {
artifact: "".into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Anchored,
at_version: None,
hash: None,
hash_stability: AnchorHashStability::Stable,
derived_from: vec![],
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
}],
);
assert!(sidecar.validate_artifact_references().is_err());
let mut sidecar = AnchorSidecar::default();
sidecar.set(
"m--beta",
vec![Anchor {
artifact: "docs/x.md".into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Derived,
at_version: None,
hash: None,
hash_stability: AnchorHashStability::Stable,
derived_from: vec![" ".into()],
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
}],
);
assert!(sidecar.validate_artifact_references().is_err());
}
#[test]
fn class_wire_strings_are_stable() {
assert_eq!(AnchorProvenanceClass::Anchored.as_wire(), "anchored");
assert_eq!(AnchorProvenanceClass::Derived.as_wire(), "derived");
assert_eq!(AnchorProvenanceClass::Authored.as_wire(), "authored");
assert_eq!(AnchorProvenanceClass::InformedBy.as_wire(), "informed-by");
for w in AnchorProvenanceClass::WIRE_VALUES {
assert_eq!(AnchorProvenanceClass::from_wire(w).unwrap().as_wire(), *w);
}
assert!(AnchorProvenanceClass::from_wire("bogus").is_none());
}
#[test]
fn grain_wire_strings_are_stable() {
for w in AnchorGrain::WIRE_VALUES {
assert_eq!(AnchorGrain::from_wire(w).unwrap().as_wire(), *w);
}
assert_eq!(
AnchorGrain::WIRE_VALUES,
&["span", "file", "tree", "url", "entity"]
);
assert!(AnchorGrain::from_wire("chunk").is_none());
}
#[test]
fn stability_and_state_wire_strings_are_stable() {
assert_eq!(AnchorHashStability::Stable.as_wire(), "stable");
assert_eq!(AnchorHashStability::Unstable.as_wire(), "unstable");
assert_eq!(AnchorState::Resolves.as_wire(), "resolves");
assert_eq!(AnchorState::Drifted.as_wire(), "drifted");
assert_eq!(AnchorState::Recheck.as_wire(), "recheck");
assert_eq!(AnchorState::Orphaned.as_wire(), "orphaned");
}
#[test]
fn only_anchored_and_derived_are_hash_bearing() {
assert!(AnchorProvenanceClass::Anchored.is_hash_bearing());
assert!(AnchorProvenanceClass::Derived.is_hash_bearing());
assert!(!AnchorProvenanceClass::Authored.is_hash_bearing());
assert!(!AnchorProvenanceClass::InformedBy.is_hash_bearing());
}
#[test]
fn grain_namespace_support_matches_capability_matrix() {
for g in [AnchorGrain::Span, AnchorGrain::File, AnchorGrain::Tree] {
assert!(g.supported_by_namespace("path"));
assert!(g.supported_by_namespace("path+commit"));
assert!(!g.supported_by_namespace("url"));
assert!(!g.supported_by_namespace("entity"));
}
assert!(AnchorGrain::Url.supported_by_namespace("url"));
assert!(AnchorGrain::Url.supported_by_namespace("path"));
assert!(AnchorGrain::Url.supported_by_namespace("entity"));
assert!(AnchorGrain::Entity.supported_by_namespace("entity"));
assert!(!AnchorGrain::Entity.supported_by_namespace("path"));
}
fn valid_input() -> AnchorInput {
AnchorInput {
artifact: Some("src/lib.rs".into()),
grain: Some("file".into()),
class: Some("anchored".into()),
hash_stability: Some("stable".into()),
hash: Some("abc123".into()),
..Default::default()
}
}
fn span_input(artifact: &str) -> AnchorInput {
AnchorInput {
artifact: Some(artifact.into()),
grain: Some("span".into()),
class: Some("anchored".into()),
..Default::default()
}
}
#[test]
fn a_span_locator_that_addresses_nothing_is_refused() {
for artifact in [
"src/lib.rs#", "src/lib.rs# ", "src/lib.rs#L0", "src/lib.rs#L0-L4", "src/lib.rs#L9-L2", "src/lib.rs#L4-L", "src/lib.rs#L4-x", ] {
let err = span_input(artifact)
.validate(Some(("codebase", "path")))
.expect_err(artifact);
assert!(
matches!(err, AnchorValidationError::SpanLocatorUnusable { .. }),
"{artifact} refused as {err:?}"
);
assert_eq!(err.code(), INVALID_ANCHOR_CODE);
assert!(err.detail().contains_key("expected"), "carries the repair");
}
}
#[test]
fn a_usable_span_locator_still_writes() {
for artifact in [
"src/lib.rs",
"src/lib.rs#L1",
"src/lib.rs#L4-L7",
"logs/ops.md#2026-08-25T00:00:00",
] {
span_input(artifact)
.validate(Some(("codebase", "path")))
.unwrap_or_else(|e| panic!("{artifact} refused: {e}"));
}
}
#[test]
fn a_span_beyond_supplied_content_is_refused() {
let mut i = span_input("src/lib.rs#L2-L9");
i.content = Some(
"one
two
three
"
.into(),
);
let err = i.validate(Some(("codebase", "path"))).unwrap_err();
match err {
AnchorValidationError::SpanOutsideContent { lines, .. } => assert_eq!(lines, 3),
other => panic!("wrong refusal: {other:?}"),
}
let mut ok = span_input("src/lib.rs#L2-L3");
ok.content = Some(
"one
two
three
"
.into(),
);
let a = ok.validate(Some(("codebase", "path"))).unwrap();
assert!(
!a.span_unvalidated,
"a span checked against content is not unvalidated"
);
}
#[test]
fn an_uncheckable_span_is_accepted_and_recorded_as_unchecked() {
let a = span_input("src/lib.rs#L4-L7")
.validate(Some(("codebase", "path")))
.unwrap();
assert!(a.span_unvalidated);
let whole_file = span_input("src/lib.rs")
.validate(Some(("codebase", "path")))
.unwrap();
assert!(
!whole_file.span_unvalidated,
"no locator addresses the whole artifact, which the existence gate checks"
);
let file_grain = valid_input().validate(Some(("codebase", "path"))).unwrap();
assert!(!file_grain.span_unvalidated, "never set off the span grain");
}
#[test]
fn an_authored_hash_records_that_the_author_pinned_it() {
let a = valid_input().validate(Some(("codebase", "path"))).unwrap();
assert_eq!(a.hash_source, Some(AnchorHashSource::Author));
let mut hashless = valid_input();
hashless.hash = None;
let b = hashless.validate(Some(("codebase", "path"))).unwrap();
assert_eq!(b.hash_source, None, "no baseline, no origin to record");
}
#[test]
fn a_re_pin_keeps_the_baseline_it_did_not_mention() {
let mut sc = AnchorSidecar::default();
let mut pinned = file_anchor("src/a.rs", "h-original");
pinned.hash_source = Some(AnchorHashSource::Author);
sc.set("m--e", vec![pinned]);
let mut repin = file_anchor("src/a.rs", "");
repin.hash = None;
repin.hash_source = None;
sc.merge("m--e", &[], vec![repin]);
let row = &sc.entities["m--e"][0];
assert_eq!(
row.hash.as_deref(),
Some("h-original"),
"the baseline the caller did not mention survives"
);
assert_eq!(row.hash_source, Some(AnchorHashSource::Author));
sc.merge("m--e", &[], vec![file_anchor("src/a.rs", "h-new")]);
assert_eq!(
sc.entities["m--e"][0].hash.as_deref(),
Some("h-new"),
"a supplied hash still replaces"
);
let unset = AnchorUnset {
artifact: "src/a.rs".into(),
grain: None,
class: None,
};
let mut fresh = file_anchor("src/a.rs", "");
fresh.hash = None;
fresh.hash_source = None;
sc.merge("m--e", &[unset], vec![fresh]);
assert_eq!(
sc.entities["m--e"][0].hash, None,
"unset-then-write is how a caller clears a baseline"
);
}
#[test]
fn validate_accepts_a_well_formed_anchor() {
let a = valid_input().validate(Some(("codebase", "path"))).unwrap();
assert_eq!(a.artifact, "src/lib.rs");
assert_eq!(a.grain, AnchorGrain::File);
assert_eq!(a.class, AnchorProvenanceClass::Anchored);
assert_eq!(a.hash.as_deref(), Some("abc123"));
assert_eq!(a.hash_stability, AnchorHashStability::Stable);
}
#[test]
fn validate_defaults_hash_stability_to_stable() {
for grain in ["span", "file", "tree"] {
let mut i = valid_input();
i.grain = Some(grain.into());
i.hash_stability = None;
let a = i.validate(None).unwrap();
assert_eq!(a.hash_stability, AnchorHashStability::Stable, "{grain}");
}
let mut e = valid_input();
e.grain = Some("entity".into());
e.artifact = Some("m--e".into());
e.hash_stability = None;
assert_eq!(
e.validate(None).unwrap().hash_stability,
AnchorHashStability::Stable
);
}
#[test]
fn validate_defaults_url_grain_to_unstable_unless_declared() {
let mut i = valid_input();
i.grain = Some("url".into());
i.artifact = Some("https://example.invalid/doc".into());
i.hash_stability = None;
assert_eq!(
i.validate(None).unwrap().hash_stability,
AnchorHashStability::Unstable
);
i.hash_stability = Some("stable".into());
assert_eq!(
i.validate(None).unwrap().hash_stability,
AnchorHashStability::Stable
);
}
#[test]
fn content_yields_the_prepared_hash_through_the_registry() {
let mut u = valid_input();
u.grain = Some("url".into());
u.artifact = Some("https://example.invalid/doc".into());
u.hash = None;
u.hash_stability = None;
u.content = Some("<p>hello</p>\r\n".into());
let a = u.validate(None).unwrap();
assert_eq!(
a.hash.as_deref(),
Some(crate::preparation::url_prepared_hash(b"<p>hello</p>\n").as_str())
);
assert_eq!(a.hash_stability, AnchorHashStability::Unstable);
let mut f = valid_input();
f.hash = None;
f.content = Some("fn a() {}\n".into());
assert_eq!(
f.validate(None).unwrap().hash.as_deref(),
Some(prepared_content_hash(b"fn a() {}").as_str())
);
let mut both = valid_input();
both.content = Some("x".into());
assert_eq!(
both.validate(None).unwrap_err(),
AnchorValidationError::ContentAndHash
);
let mut ent = valid_input();
ent.grain = Some("entity".into());
ent.artifact = Some("m--e".into());
ent.hash = None;
ent.content = Some("x".into());
let err = ent.validate(None).unwrap_err();
assert_eq!(
err,
AnchorValidationError::ContentNotAcceptedForGrain { grain: "entity" }
);
assert_eq!(err.detail()["field"], "content");
let mut tree = valid_input();
tree.grain = Some("tree".into());
tree.hash = None;
tree.content = Some("x".into());
assert!(matches!(
tree.validate(None).unwrap_err(),
AnchorValidationError::ContentNotAcceptedForGrain { grain: "tree" }
));
let mut informed = valid_input();
informed.class = Some("informed-by".into());
informed.hash = None;
informed.content = Some("x".into());
assert!(matches!(
informed.validate(None).unwrap_err(),
AnchorValidationError::HashOnNonHashClass { .. }
));
}
#[test]
fn validate_refuses_unknown_class() {
let mut i = valid_input();
i.class = Some("guessed".into());
let err = i.validate(None).unwrap_err();
assert_eq!(err.code(), INVALID_ANCHOR_CODE);
assert!(matches!(err, AnchorValidationError::UnknownClass { .. }));
assert_eq!(err.detail()["field"], serde_json::json!("class"));
}
#[test]
fn validate_refuses_unknown_grain() {
let mut i = valid_input();
i.grain = Some("paragraph".into());
let err = i.validate(None).unwrap_err();
assert!(matches!(err, AnchorValidationError::UnknownGrain { .. }));
}
#[test]
fn validate_refuses_missing_artifact() {
let mut i = valid_input();
i.artifact = Some(" ".into());
let err = i.validate(None).unwrap_err();
assert!(matches!(err, AnchorValidationError::MissingArtifact));
i.artifact = None;
assert!(matches!(
valid_input_with_artifact(None).validate(None).unwrap_err(),
AnchorValidationError::MissingArtifact
));
let _ = i;
}
fn valid_input_with_artifact(a: Option<String>) -> AnchorInput {
AnchorInput {
artifact: a,
..valid_input()
}
}
#[test]
fn validate_refuses_hash_on_non_hash_class() {
let mut i = valid_input();
i.class = Some("authored".into());
let err = i.validate(None).unwrap_err();
assert!(matches!(
err,
AnchorValidationError::HashOnNonHashClass { class: "authored" }
));
}
#[test]
fn validate_accepts_non_hash_class_without_hash() {
let mut i = valid_input();
i.class = Some("informed-by".into());
i.hash = None;
let a = i.validate(None).unwrap();
assert_eq!(a.class, AnchorProvenanceClass::InformedBy);
assert!(a.hash.is_none());
}
#[test]
fn validate_refuses_grain_unsupported_by_medium_namespace() {
let mut i = valid_input();
i.grain = Some("span".into());
i.class = Some("authored".into());
i.hash = None;
let err = i.validate(Some(("web", "url"))).unwrap_err();
match err {
AnchorValidationError::GrainNamespaceUnsupported {
grain,
anchor_namespace,
..
} => {
assert_eq!(grain, "span");
assert_eq!(anchor_namespace, "url");
}
other => panic!("expected GrainNamespaceUnsupported, got {other:?}"),
}
}
#[test]
fn validate_skips_namespace_check_without_medium_context() {
let mut i = valid_input();
i.grain = Some("span".into());
assert!(i.validate(None).is_ok());
}
#[test]
fn prepared_hash_is_stable_across_byte_noise() {
let base = prepared_content_hash(b"fn a() {}\nfn b() {}\n");
assert_eq!(prepared_content_hash(b"fn a() {}\r\nfn b() {}\r\n"), base);
assert_eq!(prepared_content_hash(b"fn a() {}\rfn b() {}\r"), base);
assert_eq!(prepared_content_hash(b"fn a() {}\nfn b() {}"), base);
assert_eq!(prepared_content_hash(b"fn a() {}\nfn b() {}\n\n\n"), base);
assert_eq!(
prepared_content_hash("\u{feff}fn a() {}\nfn b() {}\n".as_bytes()),
base
);
assert_ne!(prepared_content_hash(b"fn a() {}\nfn c() {}\n"), base);
assert_eq!(base.len(), 16);
assert!(
base.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
);
}
#[test]
fn prepared_hash_preserves_interior_whitespace() {
assert_ne!(
prepared_content_hash(b"line one \nline two\n"),
prepared_content_hash(b"line one\nline two\n")
);
}
#[test]
fn prepared_hash_hashes_binary_bytes_raw() {
let bin_a = [0xff_u8, 0xfe, 0x00, 0x0d, 0x0a];
let bin_b = [0xff_u8, 0xfe, 0x00, 0x0a];
assert_ne!(prepared_content_hash(&bin_a), prepared_content_hash(&bin_b));
assert_eq!(prepared_content_hash(&bin_a), prepared_content_hash(&bin_a));
}
fn anchor(
class: AnchorProvenanceClass,
hash: Option<&str>,
stab: AnchorHashStability,
) -> Anchor {
Anchor {
artifact: "src/lib.rs".into(),
grain: AnchorGrain::File,
class,
at_version: None,
hash: hash.map(str::to_string),
hash_stability: stab,
derived_from: Vec::new(),
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
}
}
#[test]
fn resolves_when_hash_matches() {
let a = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
let obs = ArtifactObservation::Present {
current_hash: Some("h1".into()),
};
assert_eq!(resolve_anchor(&a, &obs), AnchorState::Resolves);
}
#[test]
fn stable_hash_break_drifts_unstable_rechecks() {
let stable = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
let unstable = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Unstable,
);
let obs = ArtifactObservation::Present {
current_hash: Some("h2".into()),
};
assert_eq!(resolve_anchor(&stable, &obs), AnchorState::Drifted);
assert_eq!(resolve_anchor(&unstable, &obs), AnchorState::Recheck);
}
#[test]
fn absent_artifact_is_orphaned() {
let a = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
assert_eq!(
resolve_anchor(&a, &ArtifactObservation::Absent),
AnchorState::Orphaned
);
}
#[test]
fn non_hash_classes_never_drift() {
for class in [
AnchorProvenanceClass::Authored,
AnchorProvenanceClass::InformedBy,
] {
let a = anchor(class, None, AnchorHashStability::Stable);
let obs = ArtifactObservation::Present {
current_hash: Some("whatever".into()),
};
assert_eq!(resolve_anchor(&a, &obs), AnchorState::Resolves);
assert_eq!(
resolve_anchor(&a, &ArtifactObservation::Absent),
AnchorState::Orphaned
);
}
}
#[test]
fn unavailable_hash_rechecks_not_drifts() {
let a = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
let obs = ArtifactObservation::Present { current_hash: None };
assert_eq!(resolve_anchor(&a, &obs), AnchorState::Recheck);
}
#[test]
fn composition_counts_classes_grains_and_tree_fanout() {
let anchors = vec![
Anchor {
artifact: "a.rs".into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Anchored,
at_version: None,
hash: Some("h".into()),
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
},
Anchor {
artifact: "src/".into(),
grain: AnchorGrain::Tree,
class: AnchorProvenanceClass::Derived,
at_version: None,
hash: Some("t".into()),
hash_stability: AnchorHashStability::Stable,
derived_from: vec!["a.rs".into(), "b.rs".into()],
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
},
];
let comp = compose_entity_anchors(&anchors);
assert_eq!(comp.by_class["anchored"], 1);
assert_eq!(comp.by_class["derived"], 1);
assert_eq!(comp.by_grain["file"], 1);
assert_eq!(comp.by_grain["tree"], 1);
assert_eq!(comp.tree_grain_artifacts, vec!["src/".to_string()]);
assert_eq!(
comp.derived_inputs,
vec![vec!["a.rs".to_string(), "b.rs".to_string()]]
);
}
#[test]
fn sidecar_round_trips_and_prunes_empty() {
let mut sc = AnchorSidecar::default();
assert!(sc.is_empty());
let a = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
sc.set("specs--x", vec![a.clone()]);
assert_eq!(sc.get("specs--x").len(), 1);
let bytes = sc.to_bytes();
let round = AnchorSidecar::from_bytes(&bytes).unwrap();
assert_eq!(round, sc);
sc.set("specs--x", vec![]);
assert!(sc.is_empty());
assert!(sc.get("specs--x").is_empty());
}
fn file_anchor(artifact: &str, hash: &str) -> Anchor {
Anchor {
artifact: artifact.into(),
grain: AnchorGrain::File,
class: AnchorProvenanceClass::Anchored,
at_version: None,
hash: Some(hash.into()),
hash_stability: AnchorHashStability::Stable,
derived_from: Vec::new(),
binding: None,
source: None,
span_unvalidated: false,
hash_source: None,
last_observed: None,
}
}
#[test]
fn merge_appends_new_triple_without_touching_others() {
let mut sc = AnchorSidecar::default();
sc.set(
"m--e",
vec![file_anchor("a.rs", "h-a"), file_anchor("b.rs", "h-b")],
);
sc.merge("m--e", &[], vec![file_anchor("c.rs", "h-c")]);
let row = sc.get("m--e");
assert_eq!(row.len(), 3);
assert_eq!(row[0], file_anchor("a.rs", "h-a"));
assert_eq!(row[1], file_anchor("b.rs", "h-b"));
assert_eq!(row[2], file_anchor("c.rs", "h-c"));
}
#[test]
fn merge_replaces_same_triple_in_place() {
let mut sc = AnchorSidecar::default();
sc.set(
"m--e",
vec![file_anchor("a.rs", "h-old"), file_anchor("b.rs", "h-b")],
);
sc.merge("m--e", &[], vec![file_anchor("a.rs", "h-new")]);
let row = sc.get("m--e");
assert_eq!(row.len(), 2);
assert_eq!(row[0], file_anchor("a.rs", "h-new"));
assert_eq!(row[1], file_anchor("b.rs", "h-b"));
}
#[test]
fn merge_treats_grain_and_class_as_identity() {
let mut sc = AnchorSidecar::default();
sc.set("m--e", vec![file_anchor("a.rs", "h-a")]);
let mut span = file_anchor("a.rs", "h-span");
span.grain = AnchorGrain::Span;
let mut informed = file_anchor("a.rs", "h-a");
informed.class = AnchorProvenanceClass::InformedBy;
informed.hash = None;
sc.merge("m--e", &[], vec![span, informed]);
assert_eq!(sc.get("m--e").len(), 3);
}
#[test]
fn merge_full_resend_and_empty_are_noops() {
let mut sc = AnchorSidecar::default();
sc.set(
"m--e",
vec![file_anchor("a.rs", "h-a"), file_anchor("b.rs", "h-b")],
);
let before = sc.to_bytes();
sc.merge(
"m--e",
&[],
vec![file_anchor("a.rs", "h-a"), file_anchor("b.rs", "h-b")],
);
assert_eq!(sc.to_bytes(), before, "full re-send is byte-stable");
sc.merge("m--e", &[], Vec::new());
assert_eq!(sc.to_bytes(), before, "empty merge is a no-op");
}
#[test]
fn unset_selects_by_artifact_with_optional_narrowing() {
let mut span = file_anchor("a.rs", "h-span");
span.grain = AnchorGrain::Span;
let mut sc = AnchorSidecar::default();
sc.set(
"m--e",
vec![
file_anchor("a.rs", "h-a"),
span.clone(),
file_anchor("b.rs", "h-b"),
],
);
let narrowed = AnchorUnset {
artifact: "a.rs".into(),
grain: Some(AnchorGrain::Span),
class: None,
};
sc.merge("m--e", &[narrowed], Vec::new());
assert_eq!(
sc.get("m--e"),
&[file_anchor("a.rs", "h-a"), file_anchor("b.rs", "h-b")]
);
let missing = AnchorUnset {
artifact: "never-there.rs".into(),
grain: None,
class: None,
};
sc.merge("m--e", &[missing], Vec::new());
assert_eq!(sc.get("m--e").len(), 2);
let bare = AnchorUnset {
artifact: "a.rs".into(),
grain: None,
class: None,
};
sc.merge("m--e", &[bare], Vec::new());
assert_eq!(sc.get("m--e"), &[file_anchor("b.rs", "h-b")]);
}
#[test]
fn unset_applies_before_merge() {
let mut span = file_anchor("a.rs", "h-span");
span.grain = AnchorGrain::Span;
let mut sc = AnchorSidecar::default();
sc.set("m--e", vec![file_anchor("a.rs", "h-old"), span]);
let bare = AnchorUnset {
artifact: "a.rs".into(),
grain: None,
class: None,
};
sc.merge("m--e", &[bare], vec![file_anchor("a.rs", "h-new")]);
assert_eq!(sc.get("m--e"), &[file_anchor("a.rs", "h-new")]);
}
#[test]
fn merge_prunes_row_emptied_by_unset() {
let mut sc = AnchorSidecar::default();
sc.set("m--e", vec![file_anchor("a.rs", "h-a")]);
let bare = AnchorUnset {
artifact: "a.rs".into(),
grain: None,
class: None,
};
sc.merge("m--e", &[bare], Vec::new());
assert!(sc.is_empty());
assert!(!sc.to_bytes().windows(5).any(|w| w == b"m--e\""));
}
#[test]
fn unset_input_validates_typed() {
let ok = AnchorUnsetInput {
artifact: Some(" a.rs ".into()),
grain: Some("span".into()),
class: None,
}
.validate()
.unwrap();
assert_eq!(ok.artifact, "a.rs");
assert_eq!(ok.grain, Some(AnchorGrain::Span));
assert_eq!(ok.class, None);
let missing = AnchorUnsetInput::default().validate().unwrap_err();
assert!(matches!(missing, AnchorValidationError::MissingArtifact));
assert_eq!(missing.code(), INVALID_ANCHOR_CODE);
let bad_grain = AnchorUnsetInput {
artifact: Some("a.rs".into()),
grain: Some("paragraph".into()),
class: None,
}
.validate()
.unwrap_err();
assert!(matches!(
bad_grain,
AnchorValidationError::UnknownGrain { .. }
));
let bad_class = AnchorUnsetInput {
artifact: Some("a.rs".into()),
grain: None,
class: Some("guessed".into()),
}
.validate()
.unwrap_err();
assert!(matches!(
bad_class,
AnchorValidationError::UnknownClass { .. }
));
}
#[test]
fn sidecar_rename_leaves_zero_rows_under_old_id() {
let mut sc = AnchorSidecar::default();
sc.set(
"specs--old",
vec![anchor(
AnchorProvenanceClass::Anchored,
Some("h"),
AnchorHashStability::Stable,
)],
);
sc.rename("specs--old", "specs--new");
assert!(sc.get("specs--old").is_empty());
assert_eq!(sc.get("specs--new").len(), 1);
}
#[test]
fn sidecar_remove_drops_entity_anchors() {
let mut sc = AnchorSidecar::default();
sc.set(
"specs--gone",
vec![anchor(
AnchorProvenanceClass::Anchored,
Some("h"),
AnchorHashStability::Stable,
)],
);
sc.remove("specs--gone");
assert!(sc.get("specs--gone").is_empty());
sc.remove("specs--gone");
}
#[test]
fn empty_bytes_parse_as_empty_sidecar() {
assert!(AnchorSidecar::from_bytes(b"").unwrap().is_empty());
assert!(AnchorSidecar::from_bytes(b" \n ").unwrap().is_empty());
}
#[test]
fn anchor_json_shape_omits_empty_optionals() {
let a = anchor(
AnchorProvenanceClass::Anchored,
Some("h1"),
AnchorHashStability::Stable,
);
let v = serde_json::to_value(&a).unwrap();
assert_eq!(v["artifact"], "src/lib.rs");
assert_eq!(v["grain"], "file");
assert_eq!(v["class"], "anchored");
assert_eq!(v["hash"], "h1");
assert_eq!(v["hash_stability"], "stable");
assert!(v.get("at_version").is_none());
assert!(v.get("derived_from").is_none());
assert!(v.get("binding").is_none());
}
#[test]
fn anchor_version_serialises_tagged() {
let a = Anchor {
at_version: Some(AnchorVersion::Commit("deadbeef".into())),
..anchor(
AnchorProvenanceClass::Anchored,
Some("h"),
AnchorHashStability::Stable,
)
};
let v = serde_json::to_value(&a).unwrap();
assert_eq!(v["at_version"]["kind"], "commit");
assert_eq!(v["at_version"]["value"], "deadbeef");
}
#[test]
fn validate_source_carried_absent_or_refused_when_empty() {
let mut input = AnchorInput {
artifact: Some("src/lib.rs".into()),
grain: Some("file".into()),
class: Some("anchored".into()),
..Default::default()
};
assert_eq!(
input.validate(None).unwrap().source,
None,
"absent stays absent"
);
input.source = Some(" api-docs ".into());
assert_eq!(
input.validate(None).unwrap().source.as_deref(),
Some("api-docs"),
"non-empty name is carried (trimmed)"
);
input.source = Some(" ".into());
let err = input.validate(None).unwrap_err();
assert_eq!(err.code(), INVALID_ANCHOR_CODE);
assert!(matches!(err, AnchorValidationError::EmptySource));
assert_eq!(
err.detail().get("field"),
Some(&serde_json::json!("source"))
);
}
#[test]
fn source_is_additive_on_the_persisted_shape() {
let pre_plan = r#"{
"artifact": "src/lib.rs",
"grain": "file",
"class": "anchored",
"hash_stability": "stable"
}"#;
let a: Anchor = serde_json::from_str(pre_plan).expect("pre-plan anchor loads");
assert_eq!(a.source, None, "no backfill, no default");
let sourced = Anchor {
source: Some("api-docs".into()),
..a
};
let json = serde_json::to_string(&sourced).unwrap();
let back: Anchor = serde_json::from_str(&json).unwrap();
assert_eq!(back.source.as_deref(), Some("api-docs"));
}
#[test]
fn sidecar_v1_loads_and_upgrades_in_memory_v3_refuses() {
let v1 = br#"{"version":1,"entities":{"m--e":[{"artifact":"https://x.test/a","grain":"url","class":"informed-by","hash_stability":"unstable"}]}}"#;
let sc = AnchorSidecar::from_bytes(v1).expect("version 1 loads");
assert_eq!(sc.version, ANCHOR_SIDECAR_VERSION, "upgraded in memory");
assert_eq!(sc.get("m--e").len(), 1);
assert!(sc.get("m--e")[0].last_observed.is_none(), "rows unchanged");
let rewritten = String::from_utf8(sc.to_bytes()).unwrap();
assert!(rewritten.contains("\"version\": 2"), "{rewritten}");
let v3 = br#"{"version":3,"entities":{}}"#;
let err = AnchorSidecar::from_bytes(v3).expect_err("unknown higher version refuses");
assert!(
err.to_string()
.contains("unsupported anchors sidecar version 3"),
"{err}"
);
}
#[test]
fn last_observed_round_trips_and_is_absent_when_none() {
let mut a = valid_input().validate(None).unwrap();
let json = serde_json::to_value(&a).unwrap();
assert!(json.get("last_observed").is_none());
a.last_observed = Some(AnchorObservation {
at: "2026-09-01T10:00:00Z".into(),
hash: Some("abc".into()),
state: AnchorState::Resolves,
});
let json = serde_json::to_value(&a).unwrap();
assert_eq!(json["last_observed"]["state"], "resolves");
let back: Anchor = serde_json::from_value(json).unwrap();
assert_eq!(back, a);
}
#[test]
fn url_grain_is_admitted_beside_a_path_medium_and_path_grains_refuse_a_url_artifact() {
let mut i = valid_input();
i.grain = Some("url".into());
i.artifact = Some("https://example.org/doc.pdf".into());
i.class = Some("anchored".into());
i.hash = None;
i.content = Some("the document text".into());
i.hash_stability = None;
let a = i
.validate(Some(("filesystem", "path")))
.expect("url beside a path medium is legal");
assert_eq!(a.grain, AnchorGrain::Url);
assert_eq!(a.hash_source, Some(AnchorHashSource::Author));
assert_eq!(
a.hash_stability,
AnchorHashStability::Unstable,
"url default"
);
for grain in ["span", "file", "tree"] {
let mut i = valid_input();
i.grain = Some(grain.into());
i.artifact = Some("https://example.org/doc.pdf#L1-L3".into());
i.class = Some("informed-by".into());
i.hash = None;
let err = i.validate(Some(("filesystem", "path"))).unwrap_err();
assert!(
matches!(&err, AnchorValidationError::PathGrainOnUrlArtifact { grain: g, .. } if *g == grain),
"{grain}: {err:?}"
);
assert_eq!(err.code(), INVALID_ANCHOR_CODE);
assert!(err.to_string().contains("never enters a path namespace"));
}
assert!(looks_like_url("https://a.b/c"));
assert!(looks_like_url("file://x"));
assert!(!looks_like_url("src/main.rs"));
assert!(!looks_like_url("://nope"));
assert!(!looks_like_url("http://"));
}
#[test]
fn supplied_observations_validate_all_or_nothing() {
let now = "2026-09-02T12:00:00Z";
let rows = vec![
SuppliedObservationInput {
artifact: Some("https://a.test/1".into()),
hash: Some("h1".into()),
..Default::default()
},
SuppliedObservationInput {
artifact: Some("https://a.test/2".into()),
content: Some("body\r\n".into()),
observed_at: Some("2026-08-01".into()),
..Default::default()
},
SuppliedObservationInput {
artifact: Some("https://a.test/3".into()),
absent: Some(true),
..Default::default()
},
];
let ok = validate_supplied_observations(&rows, now).unwrap();
assert_eq!(ok.len(), 3);
assert_eq!(ok["https://a.test/1"].at, now);
assert_eq!(
ok["https://a.test/2"].outcome,
SuppliedOutcome::Present {
hash: prepared_content_hash(b"body\r\n")
},
"content hashes under the write path's canonicalization"
);
assert_eq!(ok["https://a.test/2"].at, "2026-08-01");
assert_eq!(ok["https://a.test/3"].outcome, SuppliedOutcome::Absent);
let bad = vec![SuppliedObservationInput {
artifact: Some("https://a.test/1".into()),
hash: Some("h".into()),
content: Some("c".into()),
..Default::default()
}];
let err = validate_supplied_observations(&bad, now).unwrap_err();
assert!(matches!(
err,
ObservationValidationError::OutcomeAmbiguous { row: 1, .. }
));
assert_eq!(err.code(), INVALID_OBSERVATION_CODE);
let bad = vec![SuppliedObservationInput {
artifact: Some("https://a.test/1".into()),
..Default::default()
}];
assert!(matches!(
validate_supplied_observations(&bad, now).unwrap_err(),
ObservationValidationError::OutcomeAmbiguous { .. }
));
let bad = vec![SuppliedObservationInput {
artifact: Some("https://a.test/1".into()),
hash: Some("h".into()),
observed_at: Some("yesterday".into()),
..Default::default()
}];
assert!(matches!(
validate_supplied_observations(&bad, now).unwrap_err(),
ObservationValidationError::BadTimestamp { .. }
));
let dup = vec![rows[0].clone(), rows[0].clone()];
assert!(matches!(
validate_supplied_observations(&dup, now).unwrap_err(),
ObservationValidationError::DuplicateArtifact {
first: 1,
second: 2,
..
}
));
assert!(matches!(
validate_supplied_observations(&[SuppliedObservationInput::default()], now)
.unwrap_err(),
ObservationValidationError::MissingArtifact { row: 1 }
));
}
#[test]
fn days_between_ages_by_civil_date() {
assert_eq!(days_between("2026-08-01", "2026-09-02T00:00:00Z"), Some(32));
assert_eq!(
days_between("2026-09-02T23:59:59Z", "2026-09-02T00:00:00Z"),
Some(0)
);
assert_eq!(days_between("2026-09-03", "2026-09-02"), Some(0), "floored");
assert_eq!(days_between("garbage", "2026-09-02"), None);
assert_eq!(iso_days_since_epoch("1970-01-01"), Some(0));
assert_eq!(iso_days_since_epoch("2000-03-01"), Some(11017));
}
}