use crate::snippets::error::Result;
use crate::snippets::types::{SideEffectClass, Snippet, ValidationLevel, ValidationResult};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
const CACHE_SCHEMA_VERSION: u32 = 2;
const ALEF_VALIDATOR_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Serialize, Deserialize)]
struct CacheEntry {
schema_version: u32,
result: ValidationResult,
}
pub struct ValidationCache {
directory: PathBuf,
}
impl ValidationCache {
#[must_use]
pub fn new(directory: PathBuf) -> Self {
Self { directory }
}
#[must_use]
pub fn key(snippet: &Snippet, level: ValidationLevel, session_fingerprint: Option<&str>) -> String {
Self::key_for_validator_version(snippet, level, session_fingerprint, ALEF_VALIDATOR_VERSION)
}
fn key_for_validator_version(
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
validator_version: &str,
) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(validator_version.as_bytes());
hasher.update(snippet.language.to_string().as_bytes());
hasher.update(level.to_string().as_bytes());
hasher.update(snippet.code.as_bytes());
hasher.update(format!("{:?}", snippet.metadata).as_bytes());
if let Some(fingerprint) = session_fingerprint {
hasher.update(fingerprint.as_bytes());
}
hasher.finalize().to_hex().to_string()
}
fn invalidation_key(
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
deny_unclassified: bool,
allowed_side_effects: &[SideEffectClass],
) -> String {
Self::invalidation_key_for_build_identity(
snippet,
level,
session_fingerprint,
deny_unclassified,
allowed_side_effects,
crate::bin_cli::build_info::build_identity(),
)
}
fn invalidation_key_for_build_identity(
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
deny_unclassified: bool,
allowed_side_effects: &[SideEffectClass],
alef_build_identity: &str,
) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(alef_build_identity.as_bytes());
hasher.update(snippet.language.to_string().as_bytes());
hasher.update(level.to_string().as_bytes());
hasher.update(snippet.code.as_bytes());
hasher.update(format!("{:?}", snippet.metadata).as_bytes());
hasher.update(format!("{:?}", snippet.annotation).as_bytes());
hasher.update(deny_unclassified.to_string().as_bytes());
hasher.update(format!("{allowed_side_effects:?}").as_bytes());
if let Some(fingerprint) = session_fingerprint {
hasher.update(fingerprint.as_bytes());
}
hasher.finalize().to_hex().to_string()
}
pub fn load(
&self,
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
deny_unclassified: bool,
allowed_side_effects: &[SideEffectClass],
) -> Option<ValidationResult> {
let path = self.path_for(
snippet,
level,
session_fingerprint,
deny_unclassified,
allowed_side_effects,
);
let content = std::fs::read_to_string(path).ok()?;
let entry: CacheEntry = serde_json::from_str(&content).ok()?;
(entry.schema_version == CACHE_SCHEMA_VERSION).then_some(entry.result)
}
pub fn store(
&self,
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
deny_unclassified: bool,
allowed_side_effects: &[SideEffectClass],
result: &ValidationResult,
) -> Result<()> {
std::fs::create_dir_all(&self.directory)?;
let entry = CacheEntry {
schema_version: CACHE_SCHEMA_VERSION,
result: result.clone(),
};
std::fs::write(
self.path_for(
snippet,
level,
session_fingerprint,
deny_unclassified,
allowed_side_effects,
),
serde_json::to_vec_pretty(&entry)?,
)?;
Ok(())
}
fn path_for(
&self,
snippet: &Snippet,
level: ValidationLevel,
session_fingerprint: Option<&str>,
deny_unclassified: bool,
allowed_side_effects: &[SideEffectClass],
) -> PathBuf {
self.directory.join(format!(
"{}.json",
Self::invalidation_key(
snippet,
level,
session_fingerprint,
deny_unclassified,
allowed_side_effects
)
))
}
}
#[must_use]
pub fn default_cache_dir(root: &Path) -> PathBuf {
root.join(".alef/snippets")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::{
Language, SnippetAnnotation, SnippetAnnotationKind, SnippetMetadata, SnippetStatus, SourceOrigin,
};
fn snippet(code: &str) -> Snippet {
let path = PathBuf::from("example.md");
Snippet {
id: None,
path: path.clone(),
language: Language::Rust,
title: None,
code: code.to_string(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path,
line: 1,
block_index: 0,
},
}
}
#[test]
fn cache_key_changes_with_content_and_level() {
let first = snippet("fn main() {}");
let second = snippet("fn main() { panic!() }");
assert_ne!(
ValidationCache::key(&first, ValidationLevel::Syntax, None),
ValidationCache::key(&second, ValidationLevel::Syntax, None)
);
assert_ne!(
ValidationCache::key(&first, ValidationLevel::Syntax, None),
ValidationCache::key(&first, ValidationLevel::Run, None)
);
assert_ne!(
ValidationCache::key(&first, ValidationLevel::Run, Some("binding-a")),
ValidationCache::key(&first, ValidationLevel::Run, Some("binding-b"))
);
}
#[test]
fn cache_key_changes_with_the_alef_version_that_computed_it() {
let snippet = snippet("fn main() {}");
let older = ValidationCache::key_for_validator_version(&snippet, ValidationLevel::Syntax, None, "0.64.0");
let newer = ValidationCache::key_for_validator_version(&snippet, ValidationLevel::Syntax, None, "0.64.1");
assert_ne!(
older, newer,
"a validator fix in a new alef release must invalidate every prior release's cache \
entries, not replay their verdicts"
);
}
#[test]
fn the_public_key_function_is_pinned_to_this_build_own_alef_version() {
let snippet = snippet("fn main() {}");
assert_eq!(
ValidationCache::key(&snippet, ValidationLevel::Syntax, None),
ValidationCache::key_for_validator_version(
&snippet,
ValidationLevel::Syntax,
None,
env!("CARGO_PKG_VERSION")
)
);
}
fn passing_result(snippet: &Snippet, level: ValidationLevel) -> ValidationResult {
ValidationResult {
snippet: snippet.clone(),
status: SnippetStatus::Pass,
level,
requested_level: level,
effective_level: level,
message: None,
duration_ms: 1,
capability_capped: false,
downgrade_reason: None,
unresolved_dependency: false,
timed_out: false,
preflight_skipped: false,
}
}
#[test]
fn invalidation_key_separates_annotation_and_side_effect_policy() {
let plain = snippet("value = 1");
let mut annotated = plain.clone();
annotated.annotation = Some(SnippetAnnotation {
kind: SnippetAnnotationKind::SyntaxOnly,
reason: None,
});
let base = ValidationCache::invalidation_key(&plain, ValidationLevel::Compile, None, false, &[]);
assert_ne!(
base,
ValidationCache::invalidation_key(&annotated, ValidationLevel::Compile, None, false, &[]),
"an annotation-only change must move the invalidation key"
);
assert_ne!(
base,
ValidationCache::invalidation_key(&plain, ValidationLevel::Compile, None, true, &[]),
"a deny_unclassified-only change must move the invalidation key"
);
assert_ne!(
base,
ValidationCache::invalidation_key(
&plain,
ValidationLevel::Compile,
None,
false,
&[SideEffectClass::Network]
),
"an allowed_side_effects-only change must move the invalidation key"
);
assert_eq!(
base,
ValidationCache::invalidation_key(&plain, ValidationLevel::Compile, None, false, &[]),
"the invalidation key must be deterministic for unchanged inputs"
);
}
#[test]
fn cache_load_misses_when_only_the_annotation_changes() {
let directory = tempfile::tempdir().expect("cache directory");
let cache = ValidationCache::new(directory.path().into());
let mut original = snippet("value = 1");
original.language = Language::Toml;
let cached = passing_result(&original, ValidationLevel::Compile);
cache
.store(&original, ValidationLevel::Compile, None, false, &[], &cached)
.expect("store cache entry");
assert!(
cache
.load(&original, ValidationLevel::Compile, None, false, &[])
.is_some(),
"querying with the exact snippet that wrote the cache must still be a hit"
);
let mut annotated = original.clone();
annotated.annotation = Some(SnippetAnnotation {
kind: SnippetAnnotationKind::SyntaxOnly,
reason: Some("author correction".to_string()),
});
assert!(
cache
.load(&annotated, ValidationLevel::Compile, None, false, &[])
.is_none(),
"correcting only the snippet's annotation must miss the cache, not replay the \
previous annotation's verdict -- in either direction: a snippet whose corrected \
annotation should now pass must not keep reporting the old downgrade, and a snippet \
whose new annotation should now fail must not keep reporting the old cached pass"
);
}
fn clean_build(commit: &str) -> String {
crate::bin_cli::build_info::render_build_identity(env!("CARGO_PKG_VERSION"), commit, "clean", "1000000000")
}
const SHA_A: &str = "964c552a267ccfb50a0e6f1d3b2c4a8e7f019d24";
const SHA_B: &str = "1d3b5f7902e4c6a8b0d2f4061e3c5a7b9d0f2e48";
#[test]
fn invalidation_key_separates_two_builds_of_one_version_at_different_commits() {
let snippet = snippet("value = 1");
let key_of = |identity: &str| {
ValidationCache::invalidation_key_for_build_identity(
&snippet,
ValidationLevel::Compile,
None,
false,
&[],
identity,
)
};
assert_ne!(
key_of(&clean_build(SHA_A)),
key_of(&clean_build(SHA_B)),
"two candidate binaries reporting the same semver from different commits must not \
share a snippet cache key, or one candidate's verdicts are replayed for the other -- \
masking both a fix shipped between them and a breakage introduced between them"
);
}
#[test]
fn invalidation_key_is_shared_by_two_clean_builds_of_one_commit() {
let snippet = snippet("value = 1");
let key_of = |identity: &str| {
ValidationCache::invalidation_key_for_build_identity(
&snippet,
ValidationLevel::Compile,
None,
false,
&[],
identity,
)
};
let morning =
crate::bin_cli::build_info::render_build_identity(env!("CARGO_PKG_VERSION"), SHA_A, "clean", "1000000000");
let evening =
crate::bin_cli::build_info::render_build_identity(env!("CARGO_PKG_VERSION"), SHA_A, "clean", "1999999999");
assert_eq!(
key_of(&morning),
key_of(&evening),
"two clean builds of one commit must share a cache key; a key that varies per build \
turns every release run into a cold-cache run of the whole validation pass"
);
assert_eq!(
key_of(&morning),
key_of(&clean_build(SHA_A)),
"the invalidation key must be deterministic for one build identity"
);
}
#[test]
fn cache_load_ignores_a_verdict_keyed_on_the_semver_alone() {
let directory = tempfile::tempdir().expect("cache directory");
let cache = ValidationCache::new(directory.path().into());
let subject = snippet("value = 1");
let verdict = passing_result(&subject, ValidationLevel::Compile);
let stale_key = ValidationCache::invalidation_key_for_build_identity(
&subject,
ValidationLevel::Compile,
None,
false,
&[],
ALEF_VALIDATOR_VERSION,
);
let entry = CacheEntry {
schema_version: CACHE_SCHEMA_VERSION,
result: verdict.clone(),
};
std::fs::write(
directory.path().join(format!("{stale_key}.json")),
serde_json::to_vec_pretty(&entry).expect("serialize cache entry"),
)
.expect("plant a semver-keyed cache entry");
assert!(
cache
.load(&subject, ValidationLevel::Compile, None, false, &[])
.is_none(),
"a verdict keyed on the semver alone must not be replayed: it could have been computed \
by any of the candidate binaries that share this version string, including one built \
before the validator fix under test"
);
cache
.store(&subject, ValidationLevel::Compile, None, false, &[], &verdict)
.expect("store cache entry");
assert!(
cache
.load(&subject, ValidationLevel::Compile, None, false, &[])
.is_some(),
"this binary's own verdict must still be served, or the fix has replaced stale caching \
with no caching"
);
}
#[test]
fn invalidation_key_is_pinned_to_this_build_provenance_not_just_its_semver() {
let subject = snippet("value = 1");
let live = ValidationCache::invalidation_key(&subject, ValidationLevel::Compile, None, false, &[]);
assert_ne!(
live,
ValidationCache::invalidation_key_for_build_identity(
&subject,
ValidationLevel::Compile,
None,
false,
&[],
ALEF_VALIDATOR_VERSION,
),
"the snippet cache key must salt on more than the semver -- a semver does not identify \
a build, and every candidate binary in a release cycle shares one"
);
assert_eq!(
live,
ValidationCache::invalidation_key_for_build_identity(
&subject,
ValidationLevel::Compile,
None,
false,
&[],
crate::bin_cli::build_info::build_identity(),
),
"the salt must be the same build identity `alef --version` reports, asked for rather \
than re-derived"
);
}
#[test]
fn cache_load_misses_when_only_the_side_effect_policy_changes() {
let directory = tempfile::tempdir().expect("cache directory");
let cache = ValidationCache::new(directory.path().into());
let side_effecting = snippet("run_side_effecting_thing();");
let cached = passing_result(&side_effecting, ValidationLevel::Run);
cache
.store(&side_effecting, ValidationLevel::Run, None, false, &[], &cached)
.expect("store cache entry");
assert!(
cache
.load(&side_effecting, ValidationLevel::Run, None, false, &[])
.is_some(),
"querying with the exact policy that wrote the cache must still be a hit"
);
assert!(
cache
.load(&side_effecting, ValidationLevel::Run, None, true, &[])
.is_none(),
"tightening deny_unclassified must miss the cache, not replay a Pass computed under \
the previous, looser policy"
);
assert!(
cache
.load(
&side_effecting,
ValidationLevel::Run,
None,
false,
&[SideEffectClass::Network]
)
.is_none(),
"widening allowed_side_effects must also miss the cache"
);
}
}