use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::policy::import_auth::AuthenticatedHeader;
pub const EXPORT_HEADER_SCHEMA_V1: &str = "ee.export.header.v1";
pub const EXPORT_MEMORY_SCHEMA_V1: &str = "ee.export.memory.v1";
pub const EXPORT_ARTIFACT_SCHEMA_V1: &str = "ee.export.artifact.v1";
pub const EXPORT_FOOTER_SCHEMA_V1: &str = "ee.export.footer.v1";
pub const EXPORT_AUDIT_SCHEMA_V1: &str = "ee.export.audit.v1";
pub const EXPORT_LINK_SCHEMA_V1: &str = "ee.export.link.v1";
pub const EXPORT_TAG_SCHEMA_V1: &str = "ee.export.tag.v1";
pub const EXPORT_AGENT_SCHEMA_V1: &str = "ee.export.agent.v1";
pub const EXPORT_WORKSPACE_SCHEMA_V1: &str = "ee.export.workspace.v1";
pub const ALL_EXPORT_SCHEMAS: &[&str] = &[
EXPORT_HEADER_SCHEMA_V1,
EXPORT_MEMORY_SCHEMA_V1,
EXPORT_ARTIFACT_SCHEMA_V1,
EXPORT_FOOTER_SCHEMA_V1,
EXPORT_AUDIT_SCHEMA_V1,
EXPORT_LINK_SCHEMA_V1,
EXPORT_TAG_SCHEMA_V1,
EXPORT_AGENT_SCHEMA_V1,
EXPORT_WORKSPACE_SCHEMA_V1,
];
pub const EXPORT_FORMAT_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImportSource {
#[default]
Native,
CassImport,
LegacyScan,
ExternalImport,
Unknown,
}
impl ImportSource {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::CassImport => "cass_import",
Self::LegacyScan => "legacy_scan",
Self::ExternalImport => "external_import",
Self::Unknown => "unknown",
}
}
#[must_use]
pub const fn is_external(self) -> bool {
!matches!(self, Self::Native)
}
}
impl fmt::Display for ImportSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseImportSourceError {
pub invalid: String,
}
impl fmt::Display for ParseImportSourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid import source '{}'; expected one of: native, cass_import, legacy_scan, external_import, unknown",
self.invalid
)
}
}
impl std::error::Error for ParseImportSourceError {}
impl FromStr for ImportSource {
type Err = ParseImportSourceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_jsonl_token(s).as_str() {
"native" => Ok(Self::Native),
"cass_import" => Ok(Self::CassImport),
"legacy_scan" => Ok(Self::LegacyScan),
"external_import" => Ok(Self::ExternalImport),
"unknown" => Ok(Self::Unknown),
_ => Err(ParseImportSourceError {
invalid: s.to_owned(),
}),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustLevel {
#[default]
Untrusted,
Validated,
Verified,
Quarantined,
}
impl TrustLevel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Untrusted => "untrusted",
Self::Validated => "validated",
Self::Verified => "verified",
Self::Quarantined => "quarantined",
}
}
#[must_use]
pub const fn is_trusted(self) -> bool {
matches!(self, Self::Validated | Self::Verified)
}
#[must_use]
pub const fn is_quarantined(self) -> bool {
matches!(self, Self::Quarantined)
}
}
impl fmt::Display for TrustLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseTrustLevelError {
pub invalid: String,
}
impl fmt::Display for ParseTrustLevelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid trust level '{}'; expected one of: untrusted, validated, verified, quarantined",
self.invalid
)
}
}
impl std::error::Error for ParseTrustLevelError {}
impl FromStr for TrustLevel {
type Err = ParseTrustLevelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_jsonl_token(s).as_str() {
"untrusted" => Ok(Self::Untrusted),
"validated" => Ok(Self::Validated),
"verified" => Ok(Self::Verified),
"quarantined" => Ok(Self::Quarantined),
_ => Err(ParseTrustLevelError {
invalid: s.to_owned(),
}),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExportRecordType {
Header,
Memory,
Artifact,
Link,
Tag,
Agent,
Workspace,
Audit,
Footer,
}
impl ExportRecordType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Header => "header",
Self::Memory => "memory",
Self::Artifact => "artifact",
Self::Link => "link",
Self::Tag => "tag",
Self::Agent => "agent",
Self::Workspace => "workspace",
Self::Audit => "audit",
Self::Footer => "footer",
}
}
#[must_use]
pub const fn schema(self) -> &'static str {
match self {
Self::Header => EXPORT_HEADER_SCHEMA_V1,
Self::Memory => EXPORT_MEMORY_SCHEMA_V1,
Self::Artifact => EXPORT_ARTIFACT_SCHEMA_V1,
Self::Link => EXPORT_LINK_SCHEMA_V1,
Self::Tag => EXPORT_TAG_SCHEMA_V1,
Self::Agent => EXPORT_AGENT_SCHEMA_V1,
Self::Workspace => EXPORT_WORKSPACE_SCHEMA_V1,
Self::Audit => EXPORT_AUDIT_SCHEMA_V1,
Self::Footer => EXPORT_FOOTER_SCHEMA_V1,
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Header,
Self::Memory,
Self::Artifact,
Self::Link,
Self::Tag,
Self::Agent,
Self::Workspace,
Self::Audit,
Self::Footer,
]
}
}
impl fmt::Display for ExportRecordType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseExportRecordTypeError {
pub invalid: String,
}
impl fmt::Display for ParseExportRecordTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid export record type '{}'; expected one of: header, memory, artifact, link, tag, agent, workspace, audit, footer",
self.invalid
)
}
}
impl std::error::Error for ParseExportRecordTypeError {}
impl FromStr for ExportRecordType {
type Err = ParseExportRecordTypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_jsonl_token(s).as_str() {
"header" => Ok(Self::Header),
"memory" => Ok(Self::Memory),
"artifact" => Ok(Self::Artifact),
"link" => Ok(Self::Link),
"tag" => Ok(Self::Tag),
"agent" => Ok(Self::Agent),
"workspace" => Ok(Self::Workspace),
"audit" => Ok(Self::Audit),
"footer" => Ok(Self::Footer),
_ => Err(ParseExportRecordTypeError {
invalid: s.to_owned(),
}),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ExportRecordBuildError {
pub record_type: ExportRecordType,
pub field: &'static str,
}
impl fmt::Display for ExportRecordBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"missing required non-empty field '{}' for {} export record",
self.field, self.record_type
)
}
}
impl std::error::Error for ExportRecordBuildError {}
fn missing_required(record_type: ExportRecordType, field: &'static str) -> ExportRecordBuildError {
ExportRecordBuildError { record_type, field }
}
fn required_string(
record_type: ExportRecordType,
field: &'static str,
value: Option<String>,
) -> Result<String, ExportRecordBuildError> {
match value {
Some(value) if !value.trim().is_empty() => Ok(value.trim().to_owned()),
_ => Err(missing_required(record_type, field)),
}
}
fn required_u64(
record_type: ExportRecordType,
field: &'static str,
value: Option<u64>,
) -> Result<u64, ExportRecordBuildError> {
value.ok_or_else(|| missing_required(record_type, field))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RedactionLevel {
#[default]
None,
Minimal,
Standard,
Strict,
Paranoid,
Full,
}
impl RedactionLevel {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Minimal => "minimal",
Self::Standard => "standard",
Self::Strict => "strict",
Self::Paranoid => "paranoid",
Self::Full => "full",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::None,
Self::Minimal,
Self::Standard,
Self::Strict,
Self::Paranoid,
]
}
#[must_use]
pub const fn redacts_secrets(self) -> bool {
!matches!(self, Self::None)
}
#[must_use]
pub const fn redacts_paths(self) -> bool {
matches!(
self,
Self::Standard | Self::Strict | Self::Paranoid | Self::Full
)
}
#[must_use]
pub const fn redacts_identifiers(self) -> bool {
matches!(self, Self::Standard | Self::Paranoid | Self::Full)
}
#[must_use]
pub const fn redacts_content(self) -> bool {
matches!(self, Self::Strict | Self::Paranoid | Self::Full)
}
}
impl fmt::Display for RedactionLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseRedactionLevelError {
pub invalid: String,
}
impl fmt::Display for ParseRedactionLevelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid redaction level '{}'; expected one of: none, minimal, standard, strict, paranoid, full",
self.invalid
)
}
}
impl std::error::Error for ParseRedactionLevelError {}
impl FromStr for RedactionLevel {
type Err = ParseRedactionLevelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_jsonl_token(s).as_str() {
"none" => Ok(Self::None),
"minimal" => Ok(Self::Minimal),
"standard" => Ok(Self::Standard),
"strict" => Ok(Self::Strict),
"paranoid" => Ok(Self::Paranoid),
"full" => Ok(Self::Full),
_ => Err(ParseRedactionLevelError {
invalid: s.to_owned(),
}),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExportScope {
#[default]
All,
Memories,
Audit,
Links,
MetadataOnly,
}
impl ExportScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::All => "all",
Self::Memories => "memories",
Self::Audit => "audit",
Self::Links => "links",
Self::MetadataOnly => "metadata_only",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::All,
Self::Memories,
Self::Audit,
Self::Links,
Self::MetadataOnly,
]
}
#[must_use]
pub const fn includes_memories(self) -> bool {
matches!(self, Self::All | Self::Memories | Self::MetadataOnly)
}
#[must_use]
pub const fn includes_artifacts(self) -> bool {
matches!(self, Self::All | Self::MetadataOnly)
}
#[must_use]
pub const fn includes_audit(self) -> bool {
matches!(self, Self::All | Self::Audit)
}
#[must_use]
pub const fn includes_links(self) -> bool {
matches!(self, Self::All | Self::Links)
}
#[must_use]
pub const fn includes_content(self) -> bool {
!matches!(self, Self::MetadataOnly)
}
}
impl fmt::Display for ExportScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseExportScopeError {
pub invalid: String,
}
impl fmt::Display for ParseExportScopeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid export scope '{}'; expected one of: all, memories, audit, links, metadata_only",
self.invalid
)
}
}
impl std::error::Error for ParseExportScopeError {}
impl FromStr for ExportScope {
type Err = ParseExportScopeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_jsonl_token(s).as_str() {
"all" => Ok(Self::All),
"memories" => Ok(Self::Memories),
"audit" => Ok(Self::Audit),
"links" => Ok(Self::Links),
"metadata_only" => Ok(Self::MetadataOnly),
_ => Err(ParseExportScopeError {
invalid: s.to_owned(),
}),
}
}
}
fn normalized_jsonl_token(input: &str) -> String {
let trimmed = input.trim();
let mut normalized = String::with_capacity(trimmed.len());
let mut previous_was_lowercase = false;
let mut previous_was_separator = false;
for character in trimmed.chars() {
match character {
'-' | '_' => {
if !normalized.is_empty() && !previous_was_separator {
normalized.push('_');
}
previous_was_lowercase = false;
previous_was_separator = true;
}
character if character.is_ascii_uppercase() => {
if previous_was_lowercase && !previous_was_separator {
normalized.push('_');
}
normalized.push(character.to_ascii_lowercase());
previous_was_lowercase = false;
previous_was_separator = false;
}
character => {
normalized.push(character.to_ascii_lowercase());
previous_was_lowercase = character.is_ascii_lowercase();
previous_was_separator = false;
}
}
}
normalized
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportHeader {
pub schema: String,
pub format_version: u32,
pub created_at: String,
pub workspace_id: Option<String>,
pub workspace_path: Option<String>,
pub export_scope: ExportScope,
pub redaction_level: RedactionLevel,
pub record_count: Option<u64>,
pub ee_version: String,
pub hostname: Option<String>,
pub export_id: String,
#[serde(default)]
pub import_source: ImportSource,
#[serde(default)]
pub trust_level: TrustLevel,
pub checksum: Option<String>,
pub signature: Option<String>,
pub source_schema_version: Option<String>,
}
impl ExportHeader {
#[must_use]
pub fn builder() -> ExportHeaderBuilder {
ExportHeaderBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportHeaderBuilder {
created_at: Option<String>,
workspace_id: Option<String>,
workspace_path: Option<String>,
export_scope: ExportScope,
redaction_level: RedactionLevel,
record_count: Option<u64>,
ee_version: Option<String>,
hostname: Option<String>,
export_id: Option<String>,
import_source: ImportSource,
trust_level: TrustLevel,
checksum: Option<String>,
signature: Option<String>,
source_schema_version: Option<String>,
}
impl ExportHeaderBuilder {
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
self.workspace_id = Some(workspace_id.into());
self
}
#[must_use]
pub fn workspace_path(mut self, workspace_path: impl Into<String>) -> Self {
self.workspace_path = Some(workspace_path.into());
self
}
#[must_use]
pub fn export_scope(mut self, export_scope: ExportScope) -> Self {
self.export_scope = export_scope;
self
}
#[must_use]
pub fn redaction_level(mut self, redaction_level: RedactionLevel) -> Self {
self.redaction_level = redaction_level;
self
}
#[must_use]
pub fn record_count(mut self, record_count: u64) -> Self {
self.record_count = Some(record_count);
self
}
#[must_use]
pub fn ee_version(mut self, ee_version: impl Into<String>) -> Self {
self.ee_version = Some(ee_version.into());
self
}
#[must_use]
pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
self.hostname = Some(hostname.into());
self
}
#[must_use]
pub fn export_id(mut self, export_id: impl Into<String>) -> Self {
self.export_id = Some(export_id.into());
self
}
#[must_use]
pub fn import_source(mut self, import_source: ImportSource) -> Self {
self.import_source = import_source;
self
}
#[must_use]
pub fn trust_level(mut self, trust_level: TrustLevel) -> Self {
self.trust_level = trust_level;
self
}
#[must_use]
pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
self.checksum = Some(checksum.into());
self
}
#[must_use]
pub fn signature(mut self, signature: impl Into<String>) -> Self {
self.signature = Some(signature.into());
self
}
#[must_use]
pub fn source_schema_version(mut self, version: impl Into<String>) -> Self {
self.source_schema_version = Some(version.into());
self
}
pub fn build(self) -> Result<ExportHeader, ExportRecordBuildError> {
Ok(ExportHeader {
schema: EXPORT_HEADER_SCHEMA_V1.to_owned(),
format_version: EXPORT_FORMAT_VERSION,
created_at: required_string(ExportRecordType::Header, "created_at", self.created_at)?,
workspace_id: self.workspace_id,
workspace_path: self.workspace_path,
export_scope: self.export_scope,
redaction_level: self.redaction_level,
record_count: self.record_count,
ee_version: required_string(ExportRecordType::Header, "ee_version", self.ee_version)?,
hostname: self.hostname,
export_id: required_string(ExportRecordType::Header, "export_id", self.export_id)?,
import_source: self.import_source,
trust_level: self.trust_level,
checksum: self.checksum,
signature: self.signature,
source_schema_version: self.source_schema_version,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportFooter {
pub schema: String,
pub export_id: String,
pub completed_at: String,
pub total_records: u64,
pub memory_count: u64,
#[serde(default)]
pub artifact_count: u64,
pub link_count: u64,
pub tag_count: u64,
pub audit_count: u64,
pub checksum: Option<String>,
pub success: bool,
pub error_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<AuthenticatedHeader>,
}
impl ExportFooter {
#[must_use]
pub fn builder() -> ExportFooterBuilder {
ExportFooterBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportFooterBuilder {
export_id: Option<String>,
completed_at: Option<String>,
total_records: u64,
memory_count: u64,
artifact_count: u64,
link_count: u64,
tag_count: u64,
audit_count: u64,
checksum: Option<String>,
success: bool,
error_message: Option<String>,
authentication: Option<AuthenticatedHeader>,
}
impl ExportFooterBuilder {
#[must_use]
pub fn export_id(mut self, export_id: impl Into<String>) -> Self {
self.export_id = Some(export_id.into());
self
}
#[must_use]
pub fn completed_at(mut self, completed_at: impl Into<String>) -> Self {
self.completed_at = Some(completed_at.into());
self
}
#[must_use]
pub fn total_records(mut self, total_records: u64) -> Self {
self.total_records = total_records;
self
}
#[must_use]
pub fn memory_count(mut self, memory_count: u64) -> Self {
self.memory_count = memory_count;
self
}
#[must_use]
pub fn artifact_count(mut self, artifact_count: u64) -> Self {
self.artifact_count = artifact_count;
self
}
#[must_use]
pub fn link_count(mut self, link_count: u64) -> Self {
self.link_count = link_count;
self
}
#[must_use]
pub fn tag_count(mut self, tag_count: u64) -> Self {
self.tag_count = tag_count;
self
}
#[must_use]
pub fn audit_count(mut self, audit_count: u64) -> Self {
self.audit_count = audit_count;
self
}
#[must_use]
pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
self.checksum = Some(checksum.into());
self
}
#[must_use]
pub fn success(mut self, success: bool) -> Self {
self.success = success;
self
}
#[must_use]
pub fn error_message(mut self, error_message: impl Into<String>) -> Self {
self.error_message = Some(error_message.into());
self
}
#[must_use]
pub fn authentication(mut self, authentication: Option<AuthenticatedHeader>) -> Self {
self.authentication = authentication;
self
}
pub fn build(self) -> Result<ExportFooter, ExportRecordBuildError> {
Ok(ExportFooter {
schema: EXPORT_FOOTER_SCHEMA_V1.to_owned(),
export_id: required_string(ExportRecordType::Footer, "export_id", self.export_id)?,
completed_at: required_string(
ExportRecordType::Footer,
"completed_at",
self.completed_at,
)?,
total_records: self.total_records,
memory_count: self.memory_count,
artifact_count: self.artifact_count,
link_count: self.link_count,
tag_count: self.tag_count,
audit_count: self.audit_count,
checksum: self.checksum,
success: self.success,
error_message: self.error_message,
authentication: self.authentication,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportAttemptFamilyRecord {
pub family_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub declared_size: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempt_index: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disposition: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExportMemoryRecord {
pub schema: String,
pub memory_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logical_id: Option<String>,
pub workspace_id: String,
pub level: String,
pub kind: String,
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
pub importance: Option<f64>,
pub confidence: Option<f64>,
pub utility: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pagerank_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub betweenness_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hits_authority: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hits_hub: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub onion_layer: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub k_truss_max: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub articulation_point: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bayes_alpha: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bayes_beta: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_class: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_subclass: Option<String>,
pub created_at: String,
pub updated_at: Option<String>,
pub tombstoned_at: Option<String>,
pub tombstoned_reason: Option<String>,
pub valid_from: Option<String>,
pub valid_to: Option<String>,
pub expires_at: Option<String>,
pub source_agent: Option<String>,
pub provenance_uri: Option<String>,
pub superseded_by: Option<String>,
pub supersedes: Option<String>,
pub redacted: bool,
pub redaction_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempt_family: Option<ExportAttemptFamilyRecord>,
}
impl ExportMemoryRecord {
#[must_use]
pub fn builder() -> ExportMemoryRecordBuilder {
ExportMemoryRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportMemoryRecordBuilder {
memory_id: Option<String>,
logical_id: Option<String>,
workspace_id: Option<String>,
level: Option<String>,
kind: Option<String>,
content: Option<String>,
content_hash: Option<String>,
importance: Option<f64>,
confidence: Option<f64>,
utility: Option<f64>,
pagerank_score: Option<f64>,
betweenness_score: Option<f64>,
hits_authority: Option<f64>,
hits_hub: Option<f64>,
onion_layer: Option<u32>,
k_truss_max: Option<u32>,
articulation_point: Option<bool>,
bayes_alpha: Option<f64>,
bayes_beta: Option<f64>,
trust_class: Option<String>,
trust_subclass: Option<String>,
created_at: Option<String>,
updated_at: Option<String>,
tombstoned_at: Option<String>,
tombstoned_reason: Option<String>,
valid_from: Option<String>,
valid_to: Option<String>,
expires_at: Option<String>,
source_agent: Option<String>,
provenance_uri: Option<String>,
superseded_by: Option<String>,
supersedes: Option<String>,
redacted: bool,
redaction_reason: Option<String>,
attempt_family: Option<ExportAttemptFamilyRecord>,
}
impl ExportMemoryRecordBuilder {
#[must_use]
pub fn memory_id(mut self, memory_id: impl Into<String>) -> Self {
self.memory_id = Some(memory_id.into());
self
}
#[must_use]
pub fn logical_id(mut self, logical_id: impl Into<String>) -> Self {
self.logical_id = Some(logical_id.into());
self
}
#[must_use]
pub fn workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
self.workspace_id = Some(workspace_id.into());
self
}
#[must_use]
pub fn level(mut self, level: impl Into<String>) -> Self {
self.level = Some(level.into());
self
}
#[must_use]
pub fn kind(mut self, kind: impl Into<String>) -> Self {
self.kind = Some(kind.into());
self
}
#[must_use]
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
#[must_use]
pub fn content_hash(mut self, content_hash: impl Into<String>) -> Self {
self.content_hash = Some(content_hash.into());
self
}
#[must_use]
pub fn importance(mut self, importance: f64) -> Self {
self.importance = Some(importance);
self
}
#[must_use]
pub fn confidence(mut self, confidence: f64) -> Self {
self.confidence = Some(confidence);
self
}
#[must_use]
pub fn utility(mut self, utility: f64) -> Self {
self.utility = Some(utility);
self
}
#[must_use]
pub fn pagerank_score(mut self, pagerank_score: f64) -> Self {
self.pagerank_score = Some(pagerank_score);
self
}
#[must_use]
pub fn betweenness_score(mut self, betweenness_score: f64) -> Self {
self.betweenness_score = Some(betweenness_score);
self
}
#[must_use]
pub fn hits_authority(mut self, hits_authority: f64) -> Self {
self.hits_authority = Some(hits_authority);
self
}
#[must_use]
pub fn hits_hub(mut self, hits_hub: f64) -> Self {
self.hits_hub = Some(hits_hub);
self
}
#[must_use]
pub fn onion_layer(mut self, onion_layer: u32) -> Self {
self.onion_layer = Some(onion_layer);
self
}
#[must_use]
pub fn k_truss_max(mut self, k_truss_max: u32) -> Self {
self.k_truss_max = Some(k_truss_max);
self
}
#[must_use]
pub fn articulation_point(mut self, articulation_point: bool) -> Self {
self.articulation_point = Some(articulation_point);
self
}
#[must_use]
pub fn bayes_alpha(mut self, bayes_alpha: f64) -> Self {
self.bayes_alpha = Some(bayes_alpha);
self
}
#[must_use]
pub fn bayes_beta(mut self, bayes_beta: f64) -> Self {
self.bayes_beta = Some(bayes_beta);
self
}
#[must_use]
pub fn trust_class(mut self, trust_class: impl Into<String>) -> Self {
self.trust_class = Some(trust_class.into());
self
}
#[must_use]
pub fn trust_subclass(mut self, trust_subclass: impl Into<String>) -> Self {
self.trust_subclass = Some(trust_subclass.into());
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn updated_at(mut self, updated_at: impl Into<String>) -> Self {
self.updated_at = Some(updated_at.into());
self
}
#[must_use]
pub fn tombstoned_at(mut self, tombstoned_at: impl Into<String>) -> Self {
self.tombstoned_at = Some(tombstoned_at.into());
self
}
#[must_use]
pub fn tombstoned_reason(mut self, tombstoned_reason: impl Into<String>) -> Self {
self.tombstoned_reason = Some(tombstoned_reason.into());
self
}
#[must_use]
pub fn valid_from(mut self, valid_from: impl Into<String>) -> Self {
self.valid_from = Some(valid_from.into());
self
}
#[must_use]
pub fn valid_to(mut self, valid_to: impl Into<String>) -> Self {
self.valid_to = Some(valid_to.into());
self
}
#[must_use]
pub fn expires_at(mut self, expires_at: impl Into<String>) -> Self {
self.expires_at = Some(expires_at.into());
self
}
#[must_use]
pub fn source_agent(mut self, source_agent: impl Into<String>) -> Self {
self.source_agent = Some(source_agent.into());
self
}
#[must_use]
pub fn provenance_uri(mut self, provenance_uri: impl Into<String>) -> Self {
self.provenance_uri = Some(provenance_uri.into());
self
}
#[must_use]
pub fn superseded_by(mut self, superseded_by: impl Into<String>) -> Self {
self.superseded_by = Some(superseded_by.into());
self
}
#[must_use]
pub fn supersedes(mut self, supersedes: impl Into<String>) -> Self {
self.supersedes = Some(supersedes.into());
self
}
#[must_use]
pub fn redacted(mut self, redacted: bool) -> Self {
self.redacted = redacted;
self
}
#[must_use]
pub fn redaction_reason(mut self, redaction_reason: impl Into<String>) -> Self {
self.redaction_reason = Some(redaction_reason.into());
self
}
#[must_use]
pub fn attempt_family(mut self, attempt_family: ExportAttemptFamilyRecord) -> Self {
self.attempt_family = Some(attempt_family);
self
}
pub fn build(self) -> Result<ExportMemoryRecord, ExportRecordBuildError> {
Ok(ExportMemoryRecord {
schema: EXPORT_MEMORY_SCHEMA_V1.to_owned(),
memory_id: required_string(ExportRecordType::Memory, "memory_id", self.memory_id)?,
logical_id: self.logical_id,
workspace_id: required_string(
ExportRecordType::Memory,
"workspace_id",
self.workspace_id,
)?,
level: required_string(ExportRecordType::Memory, "level", self.level)?,
kind: required_string(ExportRecordType::Memory, "kind", self.kind)?,
content: self
.content
.filter(|content| !content.trim().is_empty())
.ok_or_else(|| missing_required(ExportRecordType::Memory, "content"))?,
content_hash: self.content_hash,
importance: self.importance,
confidence: self.confidence,
utility: self.utility,
pagerank_score: self.pagerank_score,
betweenness_score: self.betweenness_score,
hits_authority: self.hits_authority,
hits_hub: self.hits_hub,
onion_layer: self.onion_layer,
k_truss_max: self.k_truss_max,
articulation_point: self.articulation_point,
bayes_alpha: self.bayes_alpha,
bayes_beta: self.bayes_beta,
trust_class: self.trust_class,
trust_subclass: self.trust_subclass,
created_at: required_string(ExportRecordType::Memory, "created_at", self.created_at)?,
updated_at: self.updated_at,
tombstoned_at: self.tombstoned_at,
tombstoned_reason: self.tombstoned_reason,
valid_from: self.valid_from,
valid_to: self.valid_to,
expires_at: self.expires_at,
source_agent: self.source_agent,
provenance_uri: self.provenance_uri,
superseded_by: self.superseded_by,
supersedes: self.supersedes,
redacted: self.redacted,
redaction_reason: self.redaction_reason,
attempt_family: self.attempt_family,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportArtifactRecord {
pub schema: String,
pub artifact_id: String,
pub workspace_id: String,
pub source_kind: String,
pub artifact_type: String,
pub original_path: Option<String>,
pub canonical_path: Option<String>,
pub external_ref: Option<String>,
pub content_hash: String,
pub media_type: String,
pub size_bytes: u64,
pub redaction_status: String,
pub snippet: Option<String>,
pub snippet_hash: Option<String>,
pub provenance_uri: Option<String>,
pub metadata: Option<serde_json::Value>,
pub created_at: String,
pub updated_at: String,
}
impl ExportArtifactRecord {
#[must_use]
pub fn builder() -> ExportArtifactRecordBuilder {
ExportArtifactRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportArtifactRecordBuilder {
artifact_id: Option<String>,
workspace_id: Option<String>,
source_kind: Option<String>,
artifact_type: Option<String>,
original_path: Option<String>,
canonical_path: Option<String>,
external_ref: Option<String>,
content_hash: Option<String>,
media_type: Option<String>,
size_bytes: Option<u64>,
redaction_status: Option<String>,
snippet: Option<String>,
snippet_hash: Option<String>,
provenance_uri: Option<String>,
metadata: Option<serde_json::Value>,
created_at: Option<String>,
updated_at: Option<String>,
}
impl ExportArtifactRecordBuilder {
#[must_use]
pub fn artifact_id(mut self, artifact_id: impl Into<String>) -> Self {
self.artifact_id = Some(artifact_id.into());
self
}
#[must_use]
pub fn workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
self.workspace_id = Some(workspace_id.into());
self
}
#[must_use]
pub fn source_kind(mut self, source_kind: impl Into<String>) -> Self {
self.source_kind = Some(source_kind.into());
self
}
#[must_use]
pub fn artifact_type(mut self, artifact_type: impl Into<String>) -> Self {
self.artifact_type = Some(artifact_type.into());
self
}
#[must_use]
pub fn original_path(mut self, original_path: impl Into<String>) -> Self {
self.original_path = Some(original_path.into());
self
}
#[must_use]
pub fn canonical_path(mut self, canonical_path: impl Into<String>) -> Self {
self.canonical_path = Some(canonical_path.into());
self
}
#[must_use]
pub fn external_ref(mut self, external_ref: impl Into<String>) -> Self {
self.external_ref = Some(external_ref.into());
self
}
#[must_use]
pub fn content_hash(mut self, content_hash: impl Into<String>) -> Self {
self.content_hash = Some(content_hash.into());
self
}
#[must_use]
pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
self.media_type = Some(media_type.into());
self
}
#[must_use]
pub fn size_bytes(mut self, size_bytes: u64) -> Self {
self.size_bytes = Some(size_bytes);
self
}
#[must_use]
pub fn redaction_status(mut self, redaction_status: impl Into<String>) -> Self {
self.redaction_status = Some(redaction_status.into());
self
}
#[must_use]
pub fn snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
#[must_use]
pub fn snippet_hash(mut self, snippet_hash: impl Into<String>) -> Self {
self.snippet_hash = Some(snippet_hash.into());
self
}
#[must_use]
pub fn provenance_uri(mut self, provenance_uri: impl Into<String>) -> Self {
self.provenance_uri = Some(provenance_uri.into());
self
}
#[must_use]
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn updated_at(mut self, updated_at: impl Into<String>) -> Self {
self.updated_at = Some(updated_at.into());
self
}
pub fn build(self) -> Result<ExportArtifactRecord, ExportRecordBuildError> {
Ok(ExportArtifactRecord {
schema: EXPORT_ARTIFACT_SCHEMA_V1.to_owned(),
artifact_id: required_string(
ExportRecordType::Artifact,
"artifact_id",
self.artifact_id,
)?,
workspace_id: required_string(
ExportRecordType::Artifact,
"workspace_id",
self.workspace_id,
)?,
source_kind: required_string(
ExportRecordType::Artifact,
"source_kind",
self.source_kind,
)?,
artifact_type: required_string(
ExportRecordType::Artifact,
"artifact_type",
self.artifact_type,
)?,
original_path: self.original_path,
canonical_path: self.canonical_path,
external_ref: self.external_ref,
content_hash: required_string(
ExportRecordType::Artifact,
"content_hash",
self.content_hash,
)?,
media_type: required_string(ExportRecordType::Artifact, "media_type", self.media_type)?,
size_bytes: required_u64(ExportRecordType::Artifact, "size_bytes", self.size_bytes)?,
redaction_status: required_string(
ExportRecordType::Artifact,
"redaction_status",
self.redaction_status,
)?,
snippet: self.snippet,
snippet_hash: self.snippet_hash,
provenance_uri: self.provenance_uri,
metadata: self.metadata,
created_at: required_string(ExportRecordType::Artifact, "created_at", self.created_at)?,
updated_at: required_string(ExportRecordType::Artifact, "updated_at", self.updated_at)?,
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExportLinkRecord {
pub schema: String,
pub link_id: String,
pub source_memory_id: String,
pub target_memory_id: String,
pub link_type: String,
pub weight: Option<f64>,
pub created_at: String,
pub metadata: Option<serde_json::Value>,
}
impl ExportLinkRecord {
#[must_use]
pub fn builder() -> ExportLinkRecordBuilder {
ExportLinkRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportLinkRecordBuilder {
link_id: Option<String>,
source_memory_id: Option<String>,
target_memory_id: Option<String>,
link_type: Option<String>,
weight: Option<f64>,
created_at: Option<String>,
metadata: Option<serde_json::Value>,
}
impl ExportLinkRecordBuilder {
#[must_use]
pub fn link_id(mut self, link_id: impl Into<String>) -> Self {
self.link_id = Some(link_id.into());
self
}
#[must_use]
pub fn source_memory_id(mut self, source_memory_id: impl Into<String>) -> Self {
self.source_memory_id = Some(source_memory_id.into());
self
}
#[must_use]
pub fn target_memory_id(mut self, target_memory_id: impl Into<String>) -> Self {
self.target_memory_id = Some(target_memory_id.into());
self
}
#[must_use]
pub fn link_type(mut self, link_type: impl Into<String>) -> Self {
self.link_type = Some(link_type.into());
self
}
#[must_use]
pub fn weight(mut self, weight: f64) -> Self {
self.weight = Some(weight);
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn build(self) -> Result<ExportLinkRecord, ExportRecordBuildError> {
Ok(ExportLinkRecord {
schema: EXPORT_LINK_SCHEMA_V1.to_owned(),
link_id: required_string(ExportRecordType::Link, "link_id", self.link_id)?,
source_memory_id: required_string(
ExportRecordType::Link,
"source_memory_id",
self.source_memory_id,
)?,
target_memory_id: required_string(
ExportRecordType::Link,
"target_memory_id",
self.target_memory_id,
)?,
link_type: required_string(ExportRecordType::Link, "link_type", self.link_type)?,
weight: self.weight,
created_at: required_string(ExportRecordType::Link, "created_at", self.created_at)?,
metadata: self.metadata,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportTagRecord {
pub schema: String,
pub memory_id: String,
pub tag: String,
pub created_at: String,
}
impl ExportTagRecord {
#[must_use]
pub fn builder() -> ExportTagRecordBuilder {
ExportTagRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportTagRecordBuilder {
memory_id: Option<String>,
tag: Option<String>,
created_at: Option<String>,
}
impl ExportTagRecordBuilder {
#[must_use]
pub fn memory_id(mut self, memory_id: impl Into<String>) -> Self {
self.memory_id = Some(memory_id.into());
self
}
#[must_use]
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
pub fn build(self) -> Result<ExportTagRecord, ExportRecordBuildError> {
Ok(ExportTagRecord {
schema: EXPORT_TAG_SCHEMA_V1.to_owned(),
memory_id: required_string(ExportRecordType::Tag, "memory_id", self.memory_id)?,
tag: required_string(ExportRecordType::Tag, "tag", self.tag)?,
created_at: required_string(ExportRecordType::Tag, "created_at", self.created_at)?,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "UncheckedExportAuditRecord")]
pub struct ExportAuditRecord {
pub schema: String,
pub audit_id: String,
pub operation: String,
pub target_type: Option<String>,
pub target_id: Option<String>,
pub performed_at: String,
pub performed_by: Option<String>,
pub details: Option<serde_json::Value>,
}
#[derive(Deserialize)]
struct UncheckedExportAuditRecord {
schema: String,
audit_id: String,
operation: String,
target_type: Option<String>,
target_id: Option<String>,
performed_at: String,
performed_by: Option<String>,
details: Option<serde_json::Value>,
}
impl TryFrom<UncheckedExportAuditRecord> for ExportAuditRecord {
type Error = ExportRecordBuildError;
fn try_from(record: UncheckedExportAuditRecord) -> Result<Self, Self::Error> {
let (target_type, target_id) =
validated_audit_target_fields(record.target_type, record.target_id)?;
Ok(Self {
schema: record.schema,
audit_id: record.audit_id,
operation: record.operation,
target_type,
target_id,
performed_at: record.performed_at,
performed_by: record.performed_by,
details: record.details,
})
}
}
fn validated_audit_target_fields(
target_type: Option<String>,
target_id: Option<String>,
) -> Result<(Option<String>, Option<String>), ExportRecordBuildError> {
let target_type = target_type
.map(|value| required_string(ExportRecordType::Audit, "target_type", Some(value)))
.transpose()?;
let target_id = target_id
.map(|value| required_string(ExportRecordType::Audit, "target_id", Some(value)))
.transpose()?;
Ok((target_type, target_id))
}
impl ExportAuditRecord {
#[must_use]
pub fn builder() -> ExportAuditRecordBuilder {
ExportAuditRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportAuditRecordBuilder {
audit_id: Option<String>,
operation: Option<String>,
target_type: Option<String>,
target_id: Option<String>,
performed_at: Option<String>,
performed_by: Option<String>,
details: Option<serde_json::Value>,
}
impl ExportAuditRecordBuilder {
#[must_use]
pub fn audit_id(mut self, audit_id: impl Into<String>) -> Self {
self.audit_id = Some(audit_id.into());
self
}
#[must_use]
pub fn operation(mut self, operation: impl Into<String>) -> Self {
self.operation = Some(operation.into());
self
}
#[must_use]
pub fn target_type(mut self, target_type: impl Into<String>) -> Self {
self.target_type = Some(target_type.into());
self
}
#[must_use]
pub fn target_id(mut self, target_id: impl Into<String>) -> Self {
self.target_id = Some(target_id.into());
self
}
#[must_use]
pub fn performed_at(mut self, performed_at: impl Into<String>) -> Self {
self.performed_at = Some(performed_at.into());
self
}
#[must_use]
pub fn performed_by(mut self, performed_by: impl Into<String>) -> Self {
self.performed_by = Some(performed_by.into());
self
}
#[must_use]
pub fn details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
pub fn build(self) -> Result<ExportAuditRecord, ExportRecordBuildError> {
let (target_type, target_id) =
validated_audit_target_fields(self.target_type, self.target_id)?;
Ok(ExportAuditRecord {
schema: EXPORT_AUDIT_SCHEMA_V1.to_owned(),
audit_id: required_string(ExportRecordType::Audit, "audit_id", self.audit_id)?,
operation: required_string(ExportRecordType::Audit, "operation", self.operation)?,
target_type,
target_id,
performed_at: required_string(
ExportRecordType::Audit,
"performed_at",
self.performed_at,
)?,
performed_by: self.performed_by,
details: self.details,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportWorkspaceRecord {
pub schema: String,
pub workspace_id: String,
pub path: String,
pub name: Option<String>,
pub created_at: String,
pub last_accessed: Option<String>,
}
impl ExportWorkspaceRecord {
#[must_use]
pub fn builder() -> ExportWorkspaceRecordBuilder {
ExportWorkspaceRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportWorkspaceRecordBuilder {
workspace_id: Option<String>,
path: Option<String>,
name: Option<String>,
created_at: Option<String>,
last_accessed: Option<String>,
}
impl ExportWorkspaceRecordBuilder {
#[must_use]
pub fn workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
self.workspace_id = Some(workspace_id.into());
self
}
#[must_use]
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn last_accessed(mut self, last_accessed: impl Into<String>) -> Self {
self.last_accessed = Some(last_accessed.into());
self
}
pub fn build(self) -> Result<ExportWorkspaceRecord, ExportRecordBuildError> {
Ok(ExportWorkspaceRecord {
schema: EXPORT_WORKSPACE_SCHEMA_V1.to_owned(),
workspace_id: required_string(
ExportRecordType::Workspace,
"workspace_id",
self.workspace_id,
)?,
path: required_string(ExportRecordType::Workspace, "path", self.path)?,
name: self.name,
created_at: required_string(
ExportRecordType::Workspace,
"created_at",
self.created_at,
)?,
last_accessed: self.last_accessed,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExportAgentRecord {
pub schema: String,
pub agent_id: String,
pub name: String,
pub program: Option<String>,
pub model: Option<String>,
pub created_at: String,
pub last_seen: Option<String>,
}
impl ExportAgentRecord {
#[must_use]
pub fn builder() -> ExportAgentRecordBuilder {
ExportAgentRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct ExportAgentRecordBuilder {
agent_id: Option<String>,
name: Option<String>,
program: Option<String>,
model: Option<String>,
created_at: Option<String>,
last_seen: Option<String>,
}
impl ExportAgentRecordBuilder {
#[must_use]
pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = Some(agent_id.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn program(mut self, program: impl Into<String>) -> Self {
self.program = Some(program.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn created_at(mut self, created_at: impl Into<String>) -> Self {
self.created_at = Some(created_at.into());
self
}
#[must_use]
pub fn last_seen(mut self, last_seen: impl Into<String>) -> Self {
self.last_seen = Some(last_seen.into());
self
}
pub fn build(self) -> Result<ExportAgentRecord, ExportRecordBuildError> {
Ok(ExportAgentRecord {
schema: EXPORT_AGENT_SCHEMA_V1.to_owned(),
agent_id: required_string(ExportRecordType::Agent, "agent_id", self.agent_id)?,
name: required_string(ExportRecordType::Agent, "name", self.name)?,
program: self.program,
model: self.model,
created_at: required_string(ExportRecordType::Agent, "created_at", self.created_at)?,
last_seen: self.last_seen,
})
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum ExportRecord {
Header(ExportHeader),
Memory(Box<ExportMemoryRecord>),
Artifact(ExportArtifactRecord),
Link(ExportLinkRecord),
Tag(ExportTagRecord),
Agent(ExportAgentRecord),
Workspace(ExportWorkspaceRecord),
Audit(ExportAuditRecord),
Footer(ExportFooter),
}
impl<'de> Deserialize<'de> for ExportRecord {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let schema = value
.get("schema")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| serde::de::Error::custom("export record requires string schema"))?;
match schema {
EXPORT_HEADER_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Header)
.map_err(serde::de::Error::custom),
EXPORT_MEMORY_SCHEMA_V1 => serde_json::from_value(value)
.map(Box::new)
.map(Self::Memory)
.map_err(serde::de::Error::custom),
EXPORT_ARTIFACT_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Artifact)
.map_err(serde::de::Error::custom),
EXPORT_LINK_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Link)
.map_err(serde::de::Error::custom),
EXPORT_TAG_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Tag)
.map_err(serde::de::Error::custom),
EXPORT_AGENT_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Agent)
.map_err(serde::de::Error::custom),
EXPORT_WORKSPACE_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Workspace)
.map_err(serde::de::Error::custom),
EXPORT_AUDIT_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Audit)
.map_err(serde::de::Error::custom),
EXPORT_FOOTER_SCHEMA_V1 => serde_json::from_value(value)
.map(Self::Footer)
.map_err(serde::de::Error::custom),
_ => Err(serde::de::Error::custom(format!(
"unsupported export record schema `{schema}`"
))),
}
}
}
impl ExportRecord {
#[must_use]
pub fn record_type(&self) -> ExportRecordType {
match self {
Self::Header(_) => ExportRecordType::Header,
Self::Memory(_) => ExportRecordType::Memory,
Self::Artifact(_) => ExportRecordType::Artifact,
Self::Link(_) => ExportRecordType::Link,
Self::Tag(_) => ExportRecordType::Tag,
Self::Agent(_) => ExportRecordType::Agent,
Self::Workspace(_) => ExportRecordType::Workspace,
Self::Audit(_) => ExportRecordType::Audit,
Self::Footer(_) => ExportRecordType::Footer,
}
}
#[must_use]
pub fn schema(&self) -> &str {
match self {
Self::Header(h) => &h.schema,
Self::Memory(m) => &m.schema,
Self::Artifact(a) => &a.schema,
Self::Link(l) => &l.schema,
Self::Tag(t) => &t.schema,
Self::Agent(a) => &a.schema,
Self::Workspace(w) => &w.schema,
Self::Audit(a) => &a.schema,
Self::Footer(f) => &f.schema,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
type TestResult = Result<(), String>;
fn ensure<T: std::fmt::Debug + PartialEq>(actual: T, expected: T, ctx: &str) -> TestResult {
if actual == expected {
Ok(())
} else {
Err(format!("{ctx}: expected {expected:?}, got {actual:?}"))
}
}
fn ensure_json_round_trip<T>(value: &T, ctx: &str) -> TestResult
where
T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + PartialEq,
{
let json = serde_json::to_string(value)
.map_err(|error| format!("{ctx} must serialize as JSON: {error}"))?;
let parsed: T = serde_json::from_str(&json)
.map_err(|error| format!("{ctx} must deserialize from JSON: {error}"))?;
ensure(&parsed, value, ctx)
}
fn ensure_build_error<T: std::fmt::Debug>(
result: Result<T, ExportRecordBuildError>,
record_type: ExportRecordType,
field: &'static str,
ctx: &str,
) -> TestResult {
let error = result
.map(|_| ())
.expect_err("avoid unwrap_err in production code");
ensure(
error.record_type,
record_type,
&format!("{ctx} record type"),
)?;
ensure(error.field, field, &format!("{ctx} field"))
}
fn ensure_export_record_match(
actual: &ExportRecord,
expected: &ExportRecord,
ctx: &str,
) -> TestResult {
ensure(
actual.record_type(),
expected.record_type(),
&format!("{ctx} type"),
)?;
match (actual, expected) {
(ExportRecord::Header(actual), ExportRecord::Header(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Memory(actual), ExportRecord::Memory(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Artifact(actual), ExportRecord::Artifact(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Link(actual), ExportRecord::Link(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Tag(actual), ExportRecord::Tag(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Agent(actual), ExportRecord::Agent(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Workspace(actual), ExportRecord::Workspace(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Audit(actual), ExportRecord::Audit(expected)) => {
ensure(actual, expected, ctx)
}
(ExportRecord::Footer(actual), ExportRecord::Footer(expected)) => {
ensure(actual, expected, ctx)
}
_ => Err(format!("{ctx}: mismatched record variants")),
}
}
#[test]
fn export_record_type_roundtrip() -> TestResult {
for rt in ExportRecordType::all() {
let s = rt.as_str();
let parsed: ExportRecordType = s
.parse()
.map_err(|e: ParseExportRecordTypeError| e.to_string())?;
ensure(parsed, *rt, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn export_record_type_parse_normalizes_external_values() -> TestResult {
ensure(
" Memory ".parse::<ExportRecordType>(),
Ok(ExportRecordType::Memory),
"record type trims and lowercases",
)?;
ensure(
"WORKSPACE".parse::<ExportRecordType>(),
Ok(ExportRecordType::Workspace),
"record type accepts uppercase",
)
}
#[test]
fn export_record_type_display() {
assert_eq!(ExportRecordType::Header.to_string(), "header");
assert_eq!(ExportRecordType::Memory.to_string(), "memory");
assert_eq!(ExportRecordType::Footer.to_string(), "footer");
}
#[test]
fn export_record_type_schema_mapping() {
assert_eq!(ExportRecordType::Header.schema(), EXPORT_HEADER_SCHEMA_V1);
assert_eq!(ExportRecordType::Memory.schema(), EXPORT_MEMORY_SCHEMA_V1);
assert_eq!(
ExportRecordType::Artifact.schema(),
EXPORT_ARTIFACT_SCHEMA_V1
);
assert_eq!(ExportRecordType::Footer.schema(), EXPORT_FOOTER_SCHEMA_V1);
assert_eq!(ExportRecordType::Audit.schema(), EXPORT_AUDIT_SCHEMA_V1);
assert_eq!(ExportRecordType::Link.schema(), EXPORT_LINK_SCHEMA_V1);
assert_eq!(ExportRecordType::Tag.schema(), EXPORT_TAG_SCHEMA_V1);
assert_eq!(ExportRecordType::Agent.schema(), EXPORT_AGENT_SCHEMA_V1);
assert_eq!(
ExportRecordType::Workspace.schema(),
EXPORT_WORKSPACE_SCHEMA_V1
);
}
#[test]
fn redaction_level_roundtrip() -> TestResult {
for level in RedactionLevel::all() {
let s = level.as_str();
let parsed: RedactionLevel = s
.parse()
.map_err(|e: ParseRedactionLevelError| e.to_string())?;
ensure(parsed, *level, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn redaction_level_parse_normalizes_external_values_and_legacy_alias() -> TestResult {
ensure(
" Strict ".parse::<RedactionLevel>(),
Ok(RedactionLevel::Strict),
"redaction level trims and lowercases",
)?;
ensure(
"FULL".parse::<RedactionLevel>(),
Ok(RedactionLevel::Full),
"redaction level accepts legacy full alias",
)
}
#[test]
fn redaction_level_capabilities() {
assert!(!RedactionLevel::None.redacts_secrets());
assert!(RedactionLevel::Minimal.redacts_secrets());
assert!(!RedactionLevel::Minimal.redacts_paths());
assert!(RedactionLevel::Standard.redacts_paths());
assert!(RedactionLevel::Standard.redacts_identifiers());
assert!(!RedactionLevel::Standard.redacts_content());
assert!(RedactionLevel::Strict.redacts_content());
assert!(RedactionLevel::Paranoid.redacts_content());
assert!(RedactionLevel::Paranoid.redacts_identifiers());
assert!(RedactionLevel::Full.redacts_content());
}
#[test]
fn export_scope_roundtrip() -> TestResult {
for scope in ExportScope::all() {
let s = scope.as_str();
let parsed: ExportScope = s
.parse()
.map_err(|e: ParseExportScopeError| e.to_string())?;
ensure(parsed, *scope, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn export_scope_parse_normalizes_external_values() -> TestResult {
ensure(
" Metadata-Only ".parse::<ExportScope>(),
Ok(ExportScope::MetadataOnly),
"export scope trims, lowercases, and accepts hyphen separator",
)?;
ensure(
"metadataOnly".parse::<ExportScope>(),
Ok(ExportScope::MetadataOnly),
"export scope accepts camelCase",
)
}
#[test]
fn export_scope_includes_checks() {
assert!(ExportScope::All.includes_memories());
assert!(ExportScope::All.includes_audit());
assert!(ExportScope::All.includes_links());
assert!(ExportScope::All.includes_content());
assert!(ExportScope::Memories.includes_memories());
assert!(!ExportScope::Memories.includes_audit());
assert!(!ExportScope::Memories.includes_links());
assert!(!ExportScope::Audit.includes_memories());
assert!(ExportScope::Audit.includes_audit());
assert!(ExportScope::MetadataOnly.includes_memories());
assert!(!ExportScope::MetadataOnly.includes_content());
}
#[test]
fn export_header_builder() {
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.workspace_id("ws-123")
.export_scope(ExportScope::Memories)
.redaction_level(RedactionLevel::Standard)
.record_count(42)
.ee_version("0.1.0")
.export_id("exp-001")
.build()
.expect("header has required fields");
assert_eq!(header.schema, EXPORT_HEADER_SCHEMA_V1);
assert_eq!(header.format_version, EXPORT_FORMAT_VERSION);
assert_eq!(header.created_at, "2026-04-30T12:00:00Z");
assert_eq!(header.workspace_id, Some("ws-123".to_owned()));
assert_eq!(header.export_scope, ExportScope::Memories);
assert_eq!(header.redaction_level, RedactionLevel::Standard);
assert_eq!(header.record_count, Some(42));
assert_eq!(header.ee_version, "0.1.0");
assert_eq!(header.export_id, "exp-001");
}
#[test]
fn export_footer_builder() {
let footer = ExportFooter::builder()
.export_id("exp-001")
.completed_at("2026-04-30T12:01:00Z")
.total_records(100)
.memory_count(50)
.artifact_count(7)
.link_count(20)
.tag_count(25)
.audit_count(5)
.checksum("abc123")
.success(true)
.build()
.expect("footer has required fields");
assert_eq!(footer.schema, EXPORT_FOOTER_SCHEMA_V1);
assert_eq!(footer.export_id, "exp-001");
assert_eq!(footer.total_records, 100);
assert_eq!(footer.memory_count, 50);
assert_eq!(footer.artifact_count, 7);
assert!(footer.success);
assert_eq!(footer.checksum, Some("abc123".to_owned()));
}
#[test]
fn export_memory_record_builder() {
let memory = ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content("Always run tests before commit")
.content_hash("blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.importance(0.8)
.confidence(0.9)
.utility(0.7)
.pagerank_score(0.12)
.betweenness_score(0.34)
.hits_authority(0.56)
.hits_hub(0.78)
.onion_layer(3)
.k_truss_max(4)
.articulation_point(true)
.bayes_alpha(2.5)
.bayes_beta(1.5)
.created_at("2026-04-30T12:00:00Z")
.tombstoned_at("2026-05-01T12:00:00Z")
.tombstoned_reason("outdated release procedure")
.valid_from("2026-04-01T00:00:00Z")
.valid_to("2026-06-01T00:00:00Z")
.source_agent("claude-code")
.redacted(false)
.build()
.expect("memory has required fields");
assert_eq!(memory.schema, EXPORT_MEMORY_SCHEMA_V1);
assert_eq!(memory.memory_id, "mem-001");
assert_eq!(memory.level, "procedural");
assert_eq!(memory.kind, "rule");
assert_eq!(
memory.content_hash.as_deref(),
Some("blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
);
assert_eq!(memory.importance, Some(0.8));
assert_eq!(memory.utility, Some(0.7));
assert_eq!(memory.pagerank_score, Some(0.12));
assert_eq!(memory.betweenness_score, Some(0.34));
assert_eq!(memory.hits_authority, Some(0.56));
assert_eq!(memory.hits_hub, Some(0.78));
assert_eq!(memory.onion_layer, Some(3));
assert_eq!(memory.k_truss_max, Some(4));
assert_eq!(memory.articulation_point, Some(true));
assert_eq!(memory.bayes_alpha, Some(2.5));
assert_eq!(memory.bayes_beta, Some(1.5));
assert_eq!(
memory.tombstoned_at.as_deref(),
Some("2026-05-01T12:00:00Z")
);
assert_eq!(
memory.tombstoned_reason.as_deref(),
Some("outdated release procedure")
);
assert_eq!(memory.valid_from.as_deref(), Some("2026-04-01T00:00:00Z"));
assert_eq!(memory.valid_to.as_deref(), Some("2026-06-01T00:00:00Z"));
assert!(!memory.redacted);
}
#[test]
fn export_memory_builder_preserves_body_whitespace_and_normalizes_identifiers() {
let content = "\u{2003} indented evidence\n\tsecond line\r\n";
let memory = ExportMemoryRecord::builder()
.memory_id(" mem-001\n")
.workspace_id(" ws-123 ")
.level("procedural")
.kind("rule")
.content(content)
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields");
assert_eq!(memory.memory_id, "mem-001");
assert_eq!(memory.workspace_id, "ws-123");
assert_eq!(memory.content, content);
let encoded = serde_json::to_string(&memory).expect("memory serializes");
let decoded: ExportMemoryRecord =
serde_json::from_str(&encoded).expect("memory deserializes");
assert_eq!(decoded.content, content);
}
#[test]
fn export_artifact_record_builder() {
let artifact = ExportArtifactRecord::builder()
.artifact_id("art_01234567890123456789012345")
.workspace_id("ws-123")
.source_kind("file")
.artifact_type("log")
.original_path("logs/build.log")
.canonical_path("/workspace/logs/build.log")
.content_hash("blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.media_type("text/plain")
.size_bytes(42)
.redaction_status("checked")
.snippet("build ok")
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.expect("artifact has required fields");
assert_eq!(artifact.schema, EXPORT_ARTIFACT_SCHEMA_V1);
assert_eq!(artifact.artifact_id, "art_01234567890123456789012345");
assert_eq!(artifact.source_kind, "file");
assert_eq!(artifact.artifact_type, "log");
assert_eq!(artifact.redaction_status, "checked");
assert_eq!(artifact.snippet, Some("build ok".to_owned()));
}
#[test]
fn export_link_record_builder() {
let link = ExportLinkRecord::builder()
.link_id("lnk-001")
.source_memory_id("mem-001")
.target_memory_id("mem-002")
.link_type("supports")
.weight(0.7)
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("link has required fields");
assert_eq!(link.schema, EXPORT_LINK_SCHEMA_V1);
assert_eq!(link.link_id, "lnk-001");
assert_eq!(link.link_type, "supports");
assert_eq!(link.weight, Some(0.7));
}
#[test]
fn export_tag_record_builder() {
let tag = ExportTagRecord::builder()
.memory_id("mem-001")
.tag("important")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("tag has required fields");
assert_eq!(tag.schema, EXPORT_TAG_SCHEMA_V1);
assert_eq!(tag.memory_id, "mem-001");
assert_eq!(tag.tag, "important");
}
#[test]
fn export_audit_record_builder() {
let audit = ExportAuditRecord::builder()
.audit_id("aud-001")
.operation("create")
.target_type("memory")
.target_id("mem-001")
.performed_at("2026-04-30T12:00:00Z")
.performed_by("claude-code")
.build()
.expect("audit has required fields");
assert_eq!(audit.schema, EXPORT_AUDIT_SCHEMA_V1);
assert_eq!(audit.audit_id, "aud-001");
assert_eq!(audit.operation, "create");
assert_eq!(audit.target_type.as_deref(), Some("memory"));
assert_eq!(audit.target_id.as_deref(), Some("mem-001"));
assert_eq!(audit.performed_by, Some("claude-code".to_owned()));
}
#[test]
fn export_targetless_audit_round_trips_with_null_target_pair() -> TestResult {
let audit = ExportAuditRecord::builder()
.audit_id("aud-db-check-001")
.operation("db.check_integrity")
.performed_at("2026-04-30T12:00:00Z")
.performed_by("ee db check-integrity")
.details(serde_json::json!({ "passed": true }))
.build()
.map_err(|error| format!("targetless audit must build: {error}"))?;
ensure(
audit.target_type.as_deref(),
None,
"targetless audit target type",
)?;
ensure(
audit.target_id.as_deref(),
None,
"targetless audit target id",
)?;
let json = serde_json::to_value(&audit)
.map_err(|error| format!("targetless audit must serialize: {error}"))?;
ensure(
json.get("target_type"),
Some(&serde_json::Value::Null),
"targetless audit serializes target_type as null",
)?;
ensure(
json.get("target_id"),
Some(&serde_json::Value::Null),
"targetless audit serializes target_id as null",
)?;
let parsed: ExportAuditRecord = serde_json::from_value(json)
.map_err(|error| format!("targetless audit must deserialize: {error}"))?;
ensure(&parsed, &audit, "targetless audit JSON round trip")?;
let export_record = ExportRecord::Audit(audit.clone());
let jsonl = serde_json::to_string(&export_record)
.map_err(|error| format!("targetless audit record must render as JSONL: {error}"))?;
let parsed_record: ExportRecord = serde_json::from_str(&jsonl)
.map_err(|error| format!("targetless audit JSONL must parse: {error}"))?;
ensure_export_record_match(
&parsed_record,
&export_record,
"targetless audit ExportRecord round trip",
)?;
let parsed_without_target_fields: ExportAuditRecord =
serde_json::from_value(serde_json::json!({
"schema": EXPORT_AUDIT_SCHEMA_V1,
"audit_id": "aud-db-check-002",
"operation": "db.check_integrity",
"performed_at": "2026-04-30T12:01:00Z",
"performed_by": "ee db check-integrity",
"details": { "passed": true }
}))
.map_err(|error| format!("omitted target pair must deserialize: {error}"))?;
ensure(
parsed_without_target_fields.target_type,
None,
"omitted target_type parses as absent",
)?;
ensure(
parsed_without_target_fields.target_id,
None,
"omitted target_id parses as absent",
)
}
#[test]
fn export_audit_round_trips_independently_optional_target_fields() -> TestResult {
for (audit, expected_type, expected_id, ctx) in [
(
ExportAuditRecord::builder()
.audit_id("aud-search-completed")
.operation("search_completed")
.target_type("search")
.performed_at("2026-04-30T12:00:00Z")
.build()
.map_err(|error| format!("type-only audit must build: {error}"))?,
Some("search"),
None,
"type-only search audit",
),
(
ExportAuditRecord::builder()
.audit_id("aud-source-observed")
.operation("source_observed")
.target_id("source-001")
.performed_at("2026-04-30T12:01:00Z")
.build()
.map_err(|error| format!("id-only audit must build: {error}"))?,
None,
Some("source-001"),
"id-only source audit",
),
] {
ensure(
audit.target_type.as_deref(),
expected_type,
&format!("{ctx} target_type"),
)?;
ensure(
audit.target_id.as_deref(),
expected_id,
&format!("{ctx} target_id"),
)?;
ensure_json_round_trip(&audit, ctx)?;
}
Ok(())
}
#[test]
fn export_audit_rejects_blank_present_target_fields() -> TestResult {
for (builder, field, ctx) in [
(
ExportAuditRecord::builder()
.audit_id("aud-blank-type")
.operation("memory.inspect")
.target_type(" ")
.target_id("mem-001")
.performed_at("2026-04-30T12:00:00Z"),
"target_type",
"audit with blank target_type",
),
(
ExportAuditRecord::builder()
.audit_id("aud-blank-id")
.operation("memory.inspect")
.target_type("memory")
.target_id("\n\t")
.performed_at("2026-04-30T12:00:00Z"),
"target_id",
"audit with blank target_id",
),
] {
ensure_build_error(builder.build(), ExportRecordType::Audit, field, ctx)?;
}
for (target_fragment, expected_field) in [
(r#""target_type":" ","target_id":"mem-001""#, "target_type"),
(r#""target_type":"memory","target_id":"""#, "target_id"),
] {
let json = format!(
r#"{{"schema":"{EXPORT_AUDIT_SCHEMA_V1}","audit_id":"aud-invalid","operation":"memory.inspect",{target_fragment},"performed_at":"2026-04-30T12:00:00Z","performed_by":null,"details":null}}"#
);
let error = serde_json::from_str::<ExportAuditRecord>(&json)
.expect_err("malformed audit target pair must not deserialize");
ensure(
error.to_string().contains(expected_field),
true,
&format!("malformed audit error identifies {expected_field}"),
)?;
ensure(
serde_json::from_str::<ExportRecord>(&json).is_err(),
true,
"malformed audit must not pass through the untagged ExportRecord union",
)?;
}
Ok(())
}
#[test]
fn export_workspace_record_builder() {
let workspace = ExportWorkspaceRecord::builder()
.workspace_id("ws-123")
.path("/home/user/project")
.name("My Project")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("workspace has required fields");
assert_eq!(workspace.schema, EXPORT_WORKSPACE_SCHEMA_V1);
assert_eq!(workspace.workspace_id, "ws-123");
assert_eq!(workspace.path, "/home/user/project");
assert_eq!(workspace.name, Some("My Project".to_owned()));
}
#[test]
fn export_agent_record_builder() {
let agent = ExportAgentRecord::builder()
.agent_id("agt-001")
.name("claude-code")
.program("Claude Code")
.model("claude-opus-4-5-20251101")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("agent has required fields");
assert_eq!(agent.schema, EXPORT_AGENT_SCHEMA_V1);
assert_eq!(agent.agent_id, "agt-001");
assert_eq!(agent.name, "claude-code");
assert_eq!(agent.program, Some("Claude Code".to_owned()));
}
#[test]
fn export_record_builders_reject_missing_required_fields() -> TestResult {
ensure_build_error(
ExportHeader::builder()
.ee_version("0.1.0")
.export_id("exp-001")
.build(),
ExportRecordType::Header,
"created_at",
"header missing created_at",
)?;
ensure_build_error(
ExportHeader::builder()
.created_at(" ")
.ee_version("0.1.0")
.export_id("exp-001")
.build(),
ExportRecordType::Header,
"created_at",
"header blank created_at",
)?;
ensure_build_error(
ExportFooter::builder()
.completed_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Footer,
"export_id",
"footer missing export_id",
)?;
ensure_build_error(
ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Memory,
"content",
"memory missing content",
)?;
for content in ["", " \t\r\n", "\u{2003}"] {
ensure_build_error(
ExportMemoryRecord::builder()
.memory_id("mem-001")
.workspace_id("ws-123")
.level("procedural")
.kind("rule")
.content(content)
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Memory,
"content",
"memory blank content",
)?;
}
ensure_build_error(
ExportArtifactRecord::builder()
.artifact_id("art-001")
.workspace_id("ws-123")
.source_kind("file")
.artifact_type("log")
.content_hash("blake3:abc123")
.media_type("text/plain")
.redaction_status("checked")
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Artifact,
"size_bytes",
"artifact missing size_bytes",
)?;
ensure_build_error(
ExportLinkRecord::builder()
.link_id("lnk-001")
.source_memory_id("mem-001")
.link_type("supports")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Link,
"target_memory_id",
"link missing target_memory_id",
)?;
ensure_build_error(
ExportTagRecord::builder()
.memory_id("mem-001")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Tag,
"tag",
"tag record missing tag",
)?;
ensure_build_error(
ExportTagRecord::builder()
.memory_id(" ")
.tag("important")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Tag,
"memory_id",
"tag record blank memory_id",
)?;
ensure_build_error(
ExportAuditRecord::builder()
.audit_id("aud-001")
.operation("create")
.target_id("mem-001")
.build(),
ExportRecordType::Audit,
"performed_at",
"audit missing performed_at",
)?;
ensure_build_error(
ExportWorkspaceRecord::builder()
.workspace_id("ws-123")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Workspace,
"path",
"workspace missing path",
)?;
ensure_build_error(
ExportAgentRecord::builder()
.agent_id("agt-001")
.created_at("2026-04-30T12:00:00Z")
.build(),
ExportRecordType::Agent,
"name",
"agent missing name",
)
}
#[test]
fn export_record_union_type_detection() {
let header = ExportRecord::Header(
ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("exp-union")
.build()
.expect("header has required fields"),
);
assert_eq!(header.record_type(), ExportRecordType::Header);
assert_eq!(header.schema(), EXPORT_HEADER_SCHEMA_V1);
let memory = ExportRecord::Memory(Box::new(
ExportMemoryRecord::builder()
.memory_id("mem-union")
.workspace_id("ws-union")
.level("procedural")
.kind("rule")
.content("Union memory")
.created_at("2026-04-30T12:00:00Z")
.build()
.expect("memory has required fields"),
));
assert_eq!(memory.record_type(), ExportRecordType::Memory);
assert_eq!(memory.schema(), EXPORT_MEMORY_SCHEMA_V1);
let artifact = ExportRecord::Artifact(
ExportArtifactRecord::builder()
.artifact_id("art-union")
.workspace_id("ws-union")
.source_kind("file")
.artifact_type("log")
.content_hash("blake3:union")
.media_type("text/plain")
.size_bytes(0)
.redaction_status("checked")
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:00:00Z")
.build()
.expect("artifact has required fields"),
);
assert_eq!(artifact.record_type(), ExportRecordType::Artifact);
assert_eq!(artifact.schema(), EXPORT_ARTIFACT_SCHEMA_V1);
let footer = ExportRecord::Footer(
ExportFooter::builder()
.export_id("exp-union")
.completed_at("2026-04-30T12:00:00Z")
.build()
.expect("footer has required fields"),
);
assert_eq!(footer.record_type(), ExportRecordType::Footer);
assert_eq!(footer.schema(), EXPORT_FOOTER_SCHEMA_V1);
}
#[test]
fn concrete_export_records_round_trip_through_json() -> TestResult {
ensure_json_round_trip(
&ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.workspace_id("wsp_01234567890123456789012345")
.workspace_path("/workspace/project")
.export_scope(ExportScope::All)
.redaction_level(RedactionLevel::Standard)
.record_count(6)
.ee_version("0.1.0")
.hostname("agent-host")
.export_id("exp-round-trip")
.import_source(ImportSource::Native)
.trust_level(TrustLevel::Validated)
.checksum("blake3:export")
.signature("sigstore:fixture")
.source_schema_version("ee.export.v1")
.build()
.expect("header has required fields"),
"header round-trip",
)?;
ensure_json_round_trip(
&ExportMemoryRecord::builder()
.memory_id("mem_01234567890123456789012345")
.workspace_id("wsp_01234567890123456789012345")
.level("procedural")
.kind("rule")
.content("Run cargo fmt --check before release.")
.content_hash(
"blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)
.importance(0.8)
.confidence(0.9)
.utility(0.7)
.pagerank_score(0.12)
.betweenness_score(0.34)
.hits_authority(0.56)
.hits_hub(0.78)
.onion_layer(3)
.k_truss_max(4)
.articulation_point(false)
.bayes_alpha(2.5)
.bayes_beta(1.5)
.created_at("2026-04-30T12:00:00Z")
.updated_at("2026-04-30T12:01:00Z")
.expires_at("2026-05-30T12:00:00Z")
.source_agent("NobleCardinal")
.provenance_uri("ee-export://round-trip")
.supersedes("mem_00234567890123456789012345")
.superseded_by("mem_00334567890123456789012345")
.redacted(true)
.redaction_reason("standard_export")
.build()
.expect("memory has required fields"),
"memory round-trip",
)?;
ensure_json_round_trip(
&ExportArtifactRecord::builder()
.artifact_id("art_01234567890123456789012345")
.workspace_id("wsp_01234567890123456789012345")
.source_kind("file")
.artifact_type("log")
.original_path("logs/build.log")
.canonical_path("/workspace/project/logs/build.log")
.content_hash(
"blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)
.media_type("text/plain")
.size_bytes(256)
.redaction_status("checked")
.snippet("cargo fmt passed")
.snippet_hash(
"blake3:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
)
.provenance_uri("file:///workspace/project/logs/build.log")
.metadata(serde_json::json!({"title":"build log"}))
.created_at("2026-04-30T12:01:00Z")
.updated_at("2026-04-30T12:01:00Z")
.build()
.expect("artifact has required fields"),
"artifact round-trip",
)?;
ensure_json_round_trip(
&ExportLinkRecord::builder()
.link_id("lnk_01234567890123456789012345")
.source_memory_id("mem_01234567890123456789012345")
.target_memory_id("mem_00334567890123456789012345")
.link_type("supersedes")
.weight(0.75)
.created_at("2026-04-30T12:02:00Z")
.metadata(serde_json::json!({"reason":"round_trip"}))
.build()
.expect("link has required fields"),
"link round-trip",
)?;
ensure_json_round_trip(
&ExportTagRecord::builder()
.memory_id("mem_01234567890123456789012345")
.tag("release")
.created_at("2026-04-30T12:03:00Z")
.build()
.expect("tag has required fields"),
"tag round-trip",
)?;
ensure_json_round_trip(
&ExportAuditRecord::builder()
.audit_id("aud_01234567890123456789012345")
.operation("export")
.target_type("memory")
.target_id("mem_01234567890123456789012345")
.performed_at("2026-04-30T12:04:00Z")
.performed_by("NobleCardinal")
.details(serde_json::json!({"records":6}))
.build()
.expect("audit has required fields"),
"audit round-trip",
)?;
ensure_json_round_trip(
&ExportWorkspaceRecord::builder()
.workspace_id("wsp_01234567890123456789012345")
.path("/workspace/project")
.name("Round Trip")
.created_at("2026-04-30T11:00:00Z")
.last_accessed("2026-04-30T12:05:00Z")
.build()
.expect("workspace has required fields"),
"workspace round-trip",
)?;
ensure_json_round_trip(
&ExportAgentRecord::builder()
.agent_id("agt_01234567890123456789012345")
.name("NobleCardinal")
.program("codex-cli")
.model("gpt-5")
.created_at("2026-04-30T11:30:00Z")
.last_seen("2026-04-30T12:06:00Z")
.build()
.expect("agent has required fields"),
"agent round-trip",
)?;
ensure_json_round_trip(
&ExportFooter::builder()
.export_id("exp-round-trip")
.completed_at("2026-04-30T12:07:00Z")
.total_records(6)
.memory_count(1)
.artifact_count(1)
.link_count(1)
.tag_count(1)
.audit_count(1)
.checksum("blake3:footer")
.success(true)
.build()
.expect("footer has required fields"),
"footer round-trip",
)
}
#[test]
fn export_record_union_round_trips_line_delimited_jsonl() -> TestResult {
let records = [
ExportRecord::Header(
ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.workspace_id("wsp_01234567890123456789012345")
.export_scope(ExportScope::All)
.redaction_level(RedactionLevel::Minimal)
.record_count(6)
.ee_version("0.1.0")
.export_id("exp-jsonl-round-trip")
.import_source(ImportSource::Native)
.trust_level(TrustLevel::Validated)
.build()
.expect("header has required fields"),
),
ExportRecord::Workspace(
ExportWorkspaceRecord::builder()
.workspace_id("wsp_01234567890123456789012345")
.path("/workspace/project")
.name("Round Trip")
.created_at("2026-04-30T11:00:00Z")
.build()
.expect("workspace has required fields"),
),
ExportRecord::Agent(
ExportAgentRecord::builder()
.agent_id("agt_01234567890123456789012345")
.name("NobleCardinal")
.program("codex-cli")
.model("gpt-5")
.created_at("2026-04-30T11:30:00Z")
.build()
.expect("agent has required fields"),
),
ExportRecord::Memory(Box::new(
ExportMemoryRecord::builder()
.memory_id("mem_01234567890123456789012345")
.workspace_id("wsp_01234567890123456789012345")
.level("procedural")
.kind("rule")
.content("Run cargo fmt --check before release.")
.created_at("2026-04-30T12:00:00Z")
.source_agent("NobleCardinal")
.redacted(false)
.build()
.expect("memory has required fields"),
)),
ExportRecord::Artifact(
ExportArtifactRecord::builder()
.artifact_id("art_01234567890123456789012345")
.workspace_id("wsp_01234567890123456789012345")
.source_kind("file")
.artifact_type("log")
.original_path("logs/build.log")
.canonical_path("/workspace/project/logs/build.log")
.content_hash(
"blake3:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
)
.media_type("text/plain")
.size_bytes(256)
.redaction_status("checked")
.snippet("cargo fmt passed")
.created_at("2026-04-30T12:01:00Z")
.updated_at("2026-04-30T12:01:00Z")
.build()
.expect("artifact has required fields"),
),
ExportRecord::Tag(
ExportTagRecord::builder()
.memory_id("mem_01234567890123456789012345")
.tag("release")
.created_at("2026-04-30T12:03:00Z")
.build()
.expect("tag has required fields"),
),
ExportRecord::Link(
ExportLinkRecord::builder()
.link_id("lnk_01234567890123456789012345")
.source_memory_id("mem_01234567890123456789012345")
.target_memory_id("mem_00334567890123456789012345")
.link_type("supports")
.weight(0.75)
.created_at("2026-04-30T12:02:00Z")
.build()
.expect("link has required fields"),
),
ExportRecord::Audit(
ExportAuditRecord::builder()
.audit_id("aud_01234567890123456789012345")
.operation("export")
.target_type("memory")
.target_id("mem_01234567890123456789012345")
.performed_at("2026-04-30T12:04:00Z")
.performed_by("NobleCardinal")
.build()
.expect("audit has required fields"),
),
ExportRecord::Footer(
ExportFooter::builder()
.export_id("exp-jsonl-round-trip")
.completed_at("2026-04-30T12:07:00Z")
.total_records(6)
.memory_count(1)
.artifact_count(1)
.link_count(1)
.tag_count(1)
.audit_count(1)
.success(true)
.build()
.expect("footer has required fields"),
),
];
let jsonl = records
.iter()
.map(|record| serde_json::to_string(record).map_err(|error| error.to_string()))
.collect::<Result<Vec<_>, _>>()?
.join("\n");
let mut lines = jsonl.lines();
for (position, expected) in records.iter().enumerate() {
let line = lines
.next()
.ok_or_else(|| format!("missing JSONL record {position}"))?;
let parsed: ExportRecord = serde_json::from_str(line)
.map_err(|error| format!("JSONL record {position} must parse: {error}"))?;
ensure_export_record_match(&parsed, expected, &format!("JSONL record {position}"))?;
}
ensure(lines.next().is_none(), true, "no extra JSONL records")?;
Ok(())
}
#[test]
fn header_serializes_to_json() {
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("test-export")
.build()
.expect("header has required fields");
let json = serde_json::to_string(&header).expect("serialize");
assert!(json.contains(r#""schema":"ee.export.header.v1""#));
assert!(json.contains(r#""format_version":1"#));
assert!(json.contains(r#""created_at":"2026-04-30T12:00:00Z""#));
}
#[test]
fn memory_record_deserializes_from_json() {
let json = r#"{
"schema": "ee.export.memory.v1",
"memory_id": "mem-001",
"workspace_id": "ws-123",
"level": "procedural",
"kind": "rule",
"content": "Test content",
"importance": 0.8,
"confidence": 0.9,
"utility": 0.7,
"pagerank_score": 0.12,
"betweenness_score": 0.34,
"hits_authority": 0.56,
"hits_hub": 0.78,
"onion_layer": 3,
"k_truss_max": 4,
"articulation_point": true,
"bayes_alpha": 2.5,
"bayes_beta": 1.5,
"trust_class": "human_explicit",
"trust_subclass": "project-rule",
"created_at": "2026-04-30T12:00:00Z",
"tombstoned_at": "2026-05-01T12:00:00Z",
"tombstoned_reason": "outdated release procedure",
"valid_from": "2026-04-01T00:00:00Z",
"valid_to": "2026-06-01T00:00:00Z",
"redacted": false
}"#;
let memory: ExportMemoryRecord = serde_json::from_str(json).expect("deserialize");
assert_eq!(memory.schema, EXPORT_MEMORY_SCHEMA_V1);
assert_eq!(memory.memory_id, "mem-001");
assert_eq!(memory.importance, Some(0.8));
assert_eq!(memory.pagerank_score, Some(0.12));
assert_eq!(memory.betweenness_score, Some(0.34));
assert_eq!(memory.hits_authority, Some(0.56));
assert_eq!(memory.hits_hub, Some(0.78));
assert_eq!(memory.onion_layer, Some(3));
assert_eq!(memory.k_truss_max, Some(4));
assert_eq!(memory.articulation_point, Some(true));
assert_eq!(memory.bayes_alpha, Some(2.5));
assert_eq!(memory.bayes_beta, Some(1.5));
assert!(memory.content_hash.is_none());
assert_eq!(memory.trust_class.as_deref(), Some("human_explicit"));
assert_eq!(memory.trust_subclass.as_deref(), Some("project-rule"));
assert_eq!(
memory.tombstoned_at.as_deref(),
Some("2026-05-01T12:00:00Z")
);
assert_eq!(
memory.tombstoned_reason.as_deref(),
Some("outdated release procedure")
);
assert_eq!(memory.valid_from.as_deref(), Some("2026-04-01T00:00:00Z"));
assert_eq!(memory.valid_to.as_deref(), Some("2026-06-01T00:00:00Z"));
assert!(!memory.redacted);
}
#[test]
fn all_export_schemas_follow_naming_convention() {
for schema in ALL_EXPORT_SCHEMAS {
assert!(
schema.starts_with("ee.export.") && schema.ends_with(".v1"),
"schema {schema} should follow ee.export.<type>.v1 pattern"
);
}
}
#[test]
fn parse_invalid_export_record_type_error() {
let result: Result<ExportRecordType, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid export record type"));
assert!(err.to_string().contains("'invalid'"));
}
#[test]
fn parse_invalid_redaction_level_error() {
let result: Result<RedactionLevel, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid redaction level"));
}
#[test]
fn parse_invalid_export_scope_error() {
let result: Result<ExportScope, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid export scope"));
}
#[test]
fn import_source_roundtrip() -> TestResult {
for source in [
ImportSource::Native,
ImportSource::CassImport,
ImportSource::LegacyScan,
ImportSource::ExternalImport,
ImportSource::Unknown,
] {
let s = source.as_str();
let parsed: ImportSource = s
.parse()
.map_err(|e: ParseImportSourceError| e.to_string())?;
ensure(parsed, source, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn import_source_parse_normalizes_external_values() -> TestResult {
ensure(
" CASS-Import ".parse::<ImportSource>(),
Ok(ImportSource::CassImport),
"import source trims, lowercases, and accepts hyphen separator",
)?;
ensure(
"cassImport".parse::<ImportSource>(),
Ok(ImportSource::CassImport),
"import source accepts camelCase",
)?;
ensure(
"LegacyScan".parse::<ImportSource>(),
Ok(ImportSource::LegacyScan),
"import source accepts PascalCase",
)?;
ensure(
"externalImport".parse::<ImportSource>(),
Ok(ImportSource::ExternalImport),
"import source accepts camelCase for external imports",
)
}
#[test]
fn import_source_display() {
assert_eq!(ImportSource::Native.to_string(), "native");
assert_eq!(ImportSource::CassImport.to_string(), "cass_import");
assert_eq!(ImportSource::LegacyScan.to_string(), "legacy_scan");
assert_eq!(ImportSource::ExternalImport.to_string(), "external_import");
assert_eq!(ImportSource::Unknown.to_string(), "unknown");
}
#[test]
fn import_source_is_external() {
assert!(!ImportSource::Native.is_external());
assert!(ImportSource::CassImport.is_external());
assert!(ImportSource::LegacyScan.is_external());
assert!(ImportSource::ExternalImport.is_external());
assert!(ImportSource::Unknown.is_external());
}
#[test]
fn parse_invalid_import_source_error() {
let result: Result<ImportSource, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid import source"));
}
#[test]
fn trust_level_roundtrip() -> TestResult {
for level in [
TrustLevel::Untrusted,
TrustLevel::Validated,
TrustLevel::Verified,
TrustLevel::Quarantined,
] {
let s = level.as_str();
let parsed: TrustLevel = s.parse().map_err(|e: ParseTrustLevelError| e.to_string())?;
ensure(parsed, level, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn trust_level_parse_normalizes_external_values() -> TestResult {
ensure(
" Quarantined ".parse::<TrustLevel>(),
Ok(TrustLevel::Quarantined),
"trust level trims and lowercases",
)
}
#[test]
fn trust_level_display() {
assert_eq!(TrustLevel::Untrusted.to_string(), "untrusted");
assert_eq!(TrustLevel::Validated.to_string(), "validated");
assert_eq!(TrustLevel::Verified.to_string(), "verified");
assert_eq!(TrustLevel::Quarantined.to_string(), "quarantined");
}
#[test]
fn trust_level_is_trusted() {
assert!(!TrustLevel::Untrusted.is_trusted());
assert!(TrustLevel::Validated.is_trusted());
assert!(TrustLevel::Verified.is_trusted());
assert!(!TrustLevel::Quarantined.is_trusted());
}
#[test]
fn trust_level_is_quarantined() {
assert!(!TrustLevel::Untrusted.is_quarantined());
assert!(!TrustLevel::Validated.is_quarantined());
assert!(!TrustLevel::Verified.is_quarantined());
assert!(TrustLevel::Quarantined.is_quarantined());
}
#[test]
fn parse_invalid_trust_level_error() {
let result: Result<TrustLevel, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid trust level"));
}
#[test]
fn export_header_with_trust_metadata() {
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("trust-metadata")
.import_source(ImportSource::CassImport)
.trust_level(TrustLevel::Validated)
.checksum("abc123")
.source_schema_version("cass.session.v1")
.build()
.expect("header has required fields");
assert_eq!(header.import_source, ImportSource::CassImport);
assert_eq!(header.trust_level, TrustLevel::Validated);
assert_eq!(header.checksum, Some("abc123".to_owned()));
assert_eq!(
header.source_schema_version,
Some("cass.session.v1".to_owned())
);
}
#[test]
fn export_header_defaults_to_native_untrusted() {
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("native-untrusted")
.build()
.expect("header has required fields");
assert_eq!(header.import_source, ImportSource::Native);
assert_eq!(header.trust_level, TrustLevel::Untrusted);
assert!(header.checksum.is_none());
assert!(header.signature.is_none());
}
#[test]
fn export_header_serializes_trust_metadata() {
let header = ExportHeader::builder()
.created_at("2026-04-30T12:00:00Z")
.ee_version("0.1.0")
.export_id("quarantined-header")
.import_source(ImportSource::LegacyScan)
.trust_level(TrustLevel::Quarantined)
.build()
.expect("header has required fields");
let json = serde_json::to_string(&header).expect("serialize");
assert!(json.contains(r#""import_source":"legacy_scan""#));
assert!(json.contains(r#""trust_level":"quarantined""#));
}
}