rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
use crate::graph::GraphNode;
use anyhow::Result;
use serde_json::json;

pub fn generate_spdx_2_3_sbom(nodes: &[GraphNode]) -> Result<serde_json::Value> {
    // Respect SOURCE_DATE_EPOCH or default to deterministic zero-timestamp (RFC-005)
    let created_timestamp = std::env::var("SOURCE_DATE_EPOCH")
        .ok()
        .and_then(|val| val.parse::<i64>().ok())
        .and_then(|epoch| chrono::DateTime::from_timestamp(epoch, 0))
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string());

    let mut sorted_nodes = nodes.to_vec();
    sorted_nodes.sort_by(|a, b| {
        a.ecosystem
            .cmp(&b.ecosystem)
            .then(a.package_name.cmp(&b.package_name))
            .then(a.resolved_version.cmp(&b.resolved_version))
    });

    let packages: Vec<serde_json::Value> = sorted_nodes
        .iter()
        .map(|node| {
            json!({
                "SPDXID": format!("SPDXRef-Package-{}-{}", node.ecosystem, node.package_name),
                "name": node.package_name,
                "versionInfo": node.resolved_version,
                "downloadLocation": "NOASSERTION",
                "checksums": [
                    {
                        "algorithm": "SHA256",
                        "checksumValue": node.content_hash.trim_start_matches("sha256:")
                    }
                ]
            })
        })
        .collect();

    let sbom = json!({
        "spdxVersion": "SPDX-2.3",
        "dataLicense": "CC0-1.0",
        "SPDXID": "SPDXRef-DOCUMENT",
        "name": "Rivox-Unified-Monorepo-SBOM",
        "documentNamespace": "https://rivox.dev/spdxdocs/monorepo-v0.1.0",
        "creationInfo": {
            "creators": ["Tool: Rivox-0.1.0"],
            "created": created_timestamp
        },
        "packages": packages
    });

    Ok(sbom)
}