use camino::Utf8PathBuf;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceClass {
EngineeringDoctrinePrinciple,
EngineeringDoctrinePattern,
EngineeringDoctrineChecklist,
EngineeringDoctrineTooling,
EngineeringDoctrineGlossary,
EngineeringDoctrineEvolution,
ProjectDoctrine,
ProjectAdr,
ProjectSchema,
ProjectContract,
ProjectTest,
ProjectCi,
ProjectReleaseGate,
ProjectSourceCode,
ProjectReadme,
ProjectAgentFile,
AxiomAlgorithm,
AxiomPolicy,
AxiomSchema,
AxiomTool,
AxiomTemplate,
AxiomWorkflow,
AxiomSkill,
CortexReceipt,
CortexFixture,
GeneratedManaged,
BlockedSurface,
Unclassified,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceCategory {
ProductSpec,
Adrs,
Doctrine,
TestsOrEvals,
RuntimeRoots,
ReleaseGates,
}
#[allow(clippy::use_self)]
impl SourceClass {
#[must_use]
pub const fn surface_category(self) -> Option<SurfaceCategory> {
use SourceClass as S;
use SurfaceCategory as C;
match self {
S::ProjectReadme | S::ProjectDoctrine => Some(C::ProductSpec),
S::ProjectAdr => Some(C::Adrs),
S::EngineeringDoctrinePrinciple
| S::EngineeringDoctrinePattern
| S::EngineeringDoctrineChecklist
| S::EngineeringDoctrineTooling
| S::EngineeringDoctrineGlossary
| S::EngineeringDoctrineEvolution => Some(C::Doctrine),
S::ProjectTest | S::ProjectSchema | S::ProjectContract => Some(C::TestsOrEvals),
S::ProjectCi | S::ProjectReleaseGate => Some(C::ReleaseGates),
_ => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SourceRecord {
pub id: String,
pub path: Utf8PathBuf,
pub class: SourceClass,
pub sha256: String,
pub size_bytes: u64,
#[serde(skip)]
pub modified: Option<DateTime<Utc>>,
#[serde(default)]
pub blocked: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub blocked_reason: Option<String>,
}
impl SourceRecord {
#[must_use]
pub fn stable_id(class: SourceClass, path: &Utf8PathBuf) -> String {
let class_str =
serde_json::to_string(&class).unwrap_or_else(|_| "\"unclassified\"".to_string());
format!("{}:{}", class_str.trim_matches('"'), path.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn doctrine_classes_map_to_doctrine_bucket() {
assert_eq!(
SourceClass::EngineeringDoctrinePrinciple.surface_category(),
Some(SurfaceCategory::Doctrine)
);
assert_eq!(
SourceClass::EngineeringDoctrineChecklist.surface_category(),
Some(SurfaceCategory::Doctrine)
);
}
#[test]
fn adr_maps_to_adrs() {
assert_eq!(
SourceClass::ProjectAdr.surface_category(),
Some(SurfaceCategory::Adrs)
);
}
#[test]
fn source_record_roundtrips() {
let r = SourceRecord {
id: "project_adr:docs/adr/0001.md".into(),
path: "docs/adr/0001.md".into(),
class: SourceClass::ProjectAdr,
sha256: "deadbeef".repeat(8),
size_bytes: 1234,
modified: None,
blocked: false,
blocked_reason: None,
};
let s = serde_json::to_string(&r).expect("serialize");
let back: SourceRecord = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back.path, r.path);
}
}