use serde::{Deserialize, Serialize};
pub const DEFAULT_COMMIT_WARN_THRESHOLD: u32 = 5;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VersionSurface {
pub path: String,
pub version: Option<String>,
pub is_lockfile: bool,
}
impl VersionSurface {
pub fn manifest(path: impl Into<String>, version: Option<String>) -> Self {
Self {
path: path.into(),
version,
is_lockfile: false,
}
}
pub fn lockfile(path: impl Into<String>, version: Option<String>) -> Self {
Self {
path: path.into(),
version,
is_lockfile: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReleaseDebtInput {
pub surfaces: Vec<VersionSurface>,
pub latest_tag: Option<String>,
pub commits_since_tag: u32,
pub commit_warn_threshold: u32,
pub inventory_errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebtFinding {
pub kind: String,
pub status: String,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReleaseDebtReport {
pub status: String,
pub version: Option<String>,
pub latest_tag: Option<String>,
pub commits_since_tag: u32,
pub findings: Vec<DebtFinding>,
}
fn finding(kind: &str, status: &str, detail: String) -> DebtFinding {
DebtFinding {
kind: kind.to_string(),
status: status.to_string(),
detail,
}
}
fn normalize_tag(tag: &str) -> &str {
tag.strip_prefix('v').unwrap_or(tag)
}
pub fn evaluate(input: &ReleaseDebtInput) -> ReleaseDebtReport {
let mut findings = Vec::new();
for problem in &input.inventory_errors {
findings.push(finding("inventory-error", "FAIL", problem.clone()));
}
let (manifests, lockfiles): (Vec<_>, Vec<_>) =
input.surfaces.iter().partition(|s| !s.is_lockfile);
for surface in &input.surfaces {
if surface.version.is_none() {
findings.push(finding(
"version-unreadable",
"FAIL",
format!("{}: no version field found", surface.path),
));
}
}
let mut manifest_versions: Vec<(&str, &str)> = manifests
.iter()
.filter_map(|s| s.version.as_deref().map(|v| (s.path.as_str(), v)))
.collect();
manifest_versions.sort_by(|a, b| a.0.cmp(b.0));
let mut distinct: Vec<&str> = manifest_versions.iter().map(|(_, v)| *v).collect();
distinct.sort_unstable();
distinct.dedup();
let agreed_version = match distinct.len() {
0 => None,
1 => Some(distinct[0].to_string()),
_ => {
let detail = manifest_versions
.iter()
.map(|(path, version)| format!("{path}={version}"))
.collect::<Vec<_>>()
.join(", ");
findings.push(finding(
"multi-surface-drift",
"FAIL",
format!("version surfaces disagree: {detail}"),
));
None
}
};
if let Some(expected) = agreed_version.as_deref() {
for lock in &lockfiles {
if let Some(actual) = lock.version.as_deref()
&& actual != expected
{
findings.push(finding(
"lockfile-desync",
"FAIL",
format!(
"{} declares {} but manifest declares {} — a --locked build fails on this state",
lock.path, actual, expected
),
));
}
}
}
match (&input.latest_tag, agreed_version.as_deref()) {
(Some(tag), Some(version)) if normalize_tag(tag) != version => {
findings.push(finding(
"tag-drift",
"WARN",
format!("manifest is {version} but latest tag is {tag} — release not cut"),
));
}
(None, Some(version)) => {
findings.push(finding(
"untagged",
"WARN",
format!("manifest is {version} but the repo has no release tag"),
));
}
_ => {}
}
if input.commits_since_tag > input.commit_warn_threshold {
findings.push(finding(
"unreleased-commits",
"WARN",
format!(
"{} commits since {} (threshold {})",
input.commits_since_tag,
input.latest_tag.as_deref().unwrap_or("the initial commit"),
input.commit_warn_threshold
),
));
}
let status = if findings.iter().any(|f| f.status == "FAIL") {
"FAIL"
} else if findings.iter().any(|f| f.status == "WARN") {
"WARN"
} else {
"OK"
};
ReleaseDebtReport {
status: status.to_string(),
version: agreed_version,
latest_tag: input.latest_tag.clone(),
commits_since_tag: input.commits_since_tag,
findings,
}
}
pub fn parse_cargo_toml_version(text: &str) -> Option<String> {
let mut in_package = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_package = trimmed == "[package]" || trimmed.starts_with("[package.");
if trimmed == "[package]" {
in_package = true;
continue;
}
}
if in_package && let Some(value) = parse_toml_string_value(trimmed, "version") {
return Some(value);
}
}
None
}
pub fn parse_cargo_lock_version(text: &str, package_name: &str) -> Option<String> {
let mut in_target_block = false;
let mut block_version: Option<String> = None;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "[[package]]" {
if in_target_block {
return block_version;
}
in_target_block = false;
block_version = None;
continue;
}
if let Some(name) = parse_toml_string_value(trimmed, "name") {
in_target_block = name == package_name;
continue;
}
if in_target_block && let Some(version) = parse_toml_string_value(trimmed, "version") {
block_version = Some(version);
}
}
if in_target_block { block_version } else { None }
}
fn parse_toml_string_value(line: &str, key: &str) -> Option<String> {
let rest = line.strip_prefix(key)?.trim_start();
let rest = rest.strip_prefix('=')?.trim();
let rest = rest.strip_prefix('"')?;
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
pub fn parse_json_version(text: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(text).ok()?;
value
.get("version")
.and_then(|v| v.as_str())
.map(|v| v.to_string())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarketplaceParse {
Entries(Vec<(String, Option<String>)>),
Malformed(String),
}
pub fn parse_marketplace(text: &str) -> MarketplaceParse {
let value: serde_json::Value = match serde_json::from_str(text) {
Ok(value) => value,
Err(e) => return MarketplaceParse::Malformed(format!("not valid JSON: {e}")),
};
if !value.is_object() {
return MarketplaceParse::Malformed("document is not a JSON object".to_string());
}
let Some(plugins) = value.get("plugins") else {
return MarketplaceParse::Malformed("no `plugins` array".to_string());
};
let Some(plugins) = plugins.as_array() else {
return MarketplaceParse::Malformed("`plugins` is not an array".to_string());
};
let mut entries = Vec::new();
for (i, entry) in plugins.iter().enumerate() {
if !entry.is_object() {
return MarketplaceParse::Malformed(format!("plugins[{i}] is not an object"));
}
let name = entry
.get("name")
.and_then(|n| n.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| format!("<unnamed #{i}>"));
let version = entry
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
entries.push((name, version));
}
MarketplaceParse::Entries(entries)
}
#[cfg(test)]
mod tests {
use super::*;
fn input(surfaces: Vec<VersionSurface>) -> ReleaseDebtInput {
ReleaseDebtInput {
surfaces,
latest_tag: Some("v1.5.2".into()),
commits_since_tag: 0,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
}
}
#[test]
fn clean_repo_is_ok() {
let report = evaluate(&input(vec![
VersionSurface::manifest("Cargo.toml", Some("1.5.2".into())),
VersionSurface::lockfile("Cargo.lock", Some("1.5.2".into())),
]));
assert_eq!(report.status, "OK");
assert!(report.findings.is_empty());
assert_eq!(report.version.as_deref(), Some("1.5.2"));
}
#[test]
fn v1_5_1_cargo_lock_desync_is_detected() {
let report = evaluate(&ReleaseDebtInput {
surfaces: vec![
VersionSurface::manifest("Cargo.toml", Some("1.5.1".into())),
VersionSurface::lockfile("Cargo.lock", Some("1.5.0".into())),
],
latest_tag: Some("v1.5.0".into()),
commits_since_tag: 2,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
});
assert_eq!(report.status, "FAIL");
let desync = report
.findings
.iter()
.find(|f| f.kind == "lockfile-desync")
.expect("lockfile desync must be reported");
assert_eq!(desync.status, "FAIL");
assert!(desync.detail.contains("1.5.0"), "{}", desync.detail);
assert!(desync.detail.contains("1.5.1"), "{}", desync.detail);
}
#[test]
fn multi_surface_disagreement_fails() {
let report = evaluate(&input(vec![
VersionSurface::manifest(".claude-plugin/plugin.json", Some("3.10.2".into())),
VersionSurface::manifest(".claude-plugin/marketplace.json", Some("3.10.2".into())),
VersionSurface::manifest(
"plugins/nxtg-forge/.claude-plugin/plugin.json",
Some("3.10.2".into()),
),
VersionSurface::manifest(
"plugins/nxtg-forge/servers/governance-mcp/package.json",
Some("3.7.0".into()),
),
VersionSurface::lockfile(
"plugins/nxtg-forge/servers/governance-mcp/package-lock.json",
Some("3.10.2".into()),
),
]));
assert_eq!(report.status, "FAIL");
let drift = report
.findings
.iter()
.find(|f| f.kind == "multi-surface-drift")
.expect("multi-surface drift must be reported");
assert!(drift.detail.contains("3.7.0"), "{}", drift.detail);
assert!(drift.detail.contains("package.json"), "{}", drift.detail);
assert!(report.version.is_none());
}
#[test]
fn single_manifest_port_would_pass_the_plugin_case() {
let surfaces = vec![
VersionSurface::manifest(".claude-plugin/plugin.json", Some("3.10.2".into())),
VersionSurface::manifest("governance-mcp/package.json", Some("3.7.0".into())),
];
let single_surface_only = evaluate(&ReleaseDebtInput {
surfaces: vec![surfaces[0].clone()],
latest_tag: Some("v3.10.2".into()),
commits_since_tag: 0,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
});
assert_eq!(single_surface_only.status, "OK");
let all_surfaces = evaluate(&ReleaseDebtInput {
surfaces,
latest_tag: Some("v3.10.2".into()),
commits_since_tag: 0,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
});
assert_eq!(all_surfaces.status, "FAIL");
}
#[test]
fn tag_drift_warns() {
let report = evaluate(&ReleaseDebtInput {
surfaces: vec![VersionSurface::manifest("Cargo.toml", Some("1.6.0".into()))],
latest_tag: Some("v1.5.2".into()),
commits_since_tag: 1,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
});
assert_eq!(report.status, "WARN");
assert!(report.findings.iter().any(|f| f.kind == "tag-drift"));
}
#[test]
fn unreleased_commits_warn_above_threshold() {
let mut base = input(vec![VersionSurface::manifest(
"Cargo.toml",
Some("1.5.2".into()),
)]);
base.commits_since_tag = 6;
let report = evaluate(&base);
assert_eq!(report.status, "WARN");
let f = report
.findings
.iter()
.find(|f| f.kind == "unreleased-commits")
.expect("unreleased commits must be reported");
assert!(f.detail.contains('6'), "{}", f.detail);
base.commits_since_tag = 5;
assert_eq!(evaluate(&base).status, "OK", "threshold is exclusive");
}
#[test]
fn untagged_repo_warns() {
let report = evaluate(&ReleaseDebtInput {
surfaces: vec![VersionSurface::manifest("Cargo.toml", Some("0.1.0".into()))],
latest_tag: None,
commits_since_tag: 3,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: Vec::new(),
});
assert_eq!(report.status, "WARN");
assert!(report.findings.iter().any(|f| f.kind == "untagged"));
}
#[test]
fn unreadable_version_fails() {
let report = evaluate(&input(vec![
VersionSurface::manifest("Cargo.toml", Some("1.5.2".into())),
VersionSurface::manifest("plugin.json", None),
]));
assert_eq!(report.status, "FAIL");
assert!(
report
.findings
.iter()
.any(|f| f.kind == "version-unreadable")
);
}
#[test]
fn tag_v_prefix_is_normalized() {
let report = evaluate(&input(vec![VersionSurface::manifest(
"Cargo.toml",
Some("1.5.2".into()),
)]));
assert_eq!(report.status, "OK", "v1.5.2 must equal 1.5.2");
}
#[test]
fn parses_cargo_toml_package_version() {
let text = r#"
[package]
name = "forge-orchestrator"
version = "1.5.2"
edition = "2024"
[dependencies]
clap = { version = "4", features = ["derive"] }
"#;
assert_eq!(parse_cargo_toml_version(text).as_deref(), Some("1.5.2"));
}
#[test]
fn cargo_toml_parser_ignores_dependency_versions() {
let text = r#"
[workspace]
[dependencies]
serde = { version = "9.9.9" }
[package]
name = "forge-orchestrator"
version = "1.5.2"
"#;
assert_eq!(parse_cargo_toml_version(text).as_deref(), Some("1.5.2"));
}
#[test]
fn parses_cargo_lock_block_for_the_named_package() {
let text = r#"
[[package]]
name = "clap"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clap_builder",
]
[[package]]
name = "forge-orchestrator"
version = "1.5.2"
dependencies = [
"anyhow",
"clap",
]
[[package]]
name = "serde"
version = "1.0.200"
"#;
assert_eq!(
parse_cargo_lock_version(text, "forge-orchestrator").as_deref(),
Some("1.5.2")
);
assert_eq!(
parse_cargo_lock_version(text, "clap").as_deref(),
Some("4.5.0")
);
assert_eq!(
parse_cargo_lock_version(text, "serde").as_deref(),
Some("1.0.200")
);
assert_eq!(parse_cargo_lock_version(text, "absent"), None);
}
#[test]
fn cargo_lock_parser_does_not_leak_into_the_next_block() {
let text = r#"
[[package]]
name = "forge-orchestrator"
dependencies = [
"anyhow",
]
[[package]]
name = "other"
version = "9.9.9"
"#;
assert_eq!(parse_cargo_lock_version(text, "forge-orchestrator"), None);
}
#[test]
fn marketplace_entry_without_a_version_is_surfaced_not_dropped() {
let parsed = parse_marketplace(
r#"{"plugins":[
{"name":"a","version":"1.0.0"},
{"name":"b","source":"./b"}
]}"#,
);
let MarketplaceParse::Entries(entries) = parsed else {
panic!("readable manifest must parse: {parsed:?}");
};
assert_eq!(entries.len(), 2, "every entry is reported: {entries:?}");
assert_eq!(entries[1], ("b".to_string(), None));
}
#[test]
fn marketplace_entry_version_is_surfaced_and_fails_the_evaluator() {
let report = evaluate(&input(vec![
VersionSurface::manifest("marketplace.json#plugins[a]", Some("1.0.0".into())),
VersionSurface::manifest("marketplace.json#plugins[b]", None),
]));
assert_eq!(report.status, "FAIL");
assert!(
report
.findings
.iter()
.any(|f| f.kind == "version-unreadable" && f.detail.contains("plugins[b]")),
"{:?}",
report.findings
);
}
#[test]
fn valid_empty_marketplace_is_not_malformed() {
assert_eq!(
parse_marketplace(r#"{"name":"m","plugins":[]}"#),
MarketplaceParse::Entries(vec![])
);
}
#[test]
fn malformed_marketplace_shapes_are_reported_not_emptied() {
for (text, needle) in [
("not json at all", "not valid JSON"),
("[]", "not a JSON object"),
(r#"{"name":"m"}"#, "no `plugins` array"),
(r#"{"plugins":{}}"#, "not an array"),
(r#"{"plugins":["a string"]}"#, "plugins[0] is not an object"),
] {
match parse_marketplace(text) {
MarketplaceParse::Malformed(reason) => {
assert!(reason.contains(needle), "{text} -> {reason}");
}
other => panic!("{text} must be malformed, got {other:?}"),
}
}
}
#[test]
fn a_non_string_entry_version_is_treated_as_missing() {
let MarketplaceParse::Entries(entries) =
parse_marketplace(r#"{"plugins":[{"name":"a","version":3}]}"#)
else {
panic!("structurally fine, just a bad version type");
};
assert_eq!(entries[0], ("a".to_string(), None));
}
#[test]
fn inventory_errors_fail_the_evaluation_loudly() {
let report = evaluate(&ReleaseDebtInput {
surfaces: vec![VersionSurface::manifest("Cargo.toml", Some("1.5.2".into()))],
latest_tag: Some("v1.5.2".into()),
commits_since_tag: 0,
commit_warn_threshold: DEFAULT_COMMIT_WARN_THRESHOLD,
inventory_errors: vec!["plugin.json: unreadable (permission denied)".into()],
});
assert_eq!(
report.status, "FAIL",
"an unreadable surface cannot yield a passing verdict"
);
let f = report
.findings
.iter()
.find(|f| f.kind == "inventory-error")
.expect("inventory error must be reported");
assert!(f.detail.contains("permission denied"), "{}", f.detail);
}
#[test]
fn parses_json_version() {
assert_eq!(
parse_json_version(r#"{"name":"nxtg-forge","version":"3.10.2"}"#).as_deref(),
Some("3.10.2")
);
assert_eq!(parse_json_version(r#"{"name":"x"}"#), None);
assert_eq!(parse_json_version("not json"), None);
}
}