1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::audit::SpecItemKind;
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
15pub struct LockFile {
16 pub module: String,
17 pub claims: HashMap<String, Claim>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Claim {
23 pub agent: String,
24 pub kind: SpecItemKind,
25 pub claimed_at: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum LockError {
31 AlreadyClaimed {
33 item: String,
34 owner: String,
35 requester: String,
36 },
37 NotClaimed { item: String },
39 NotOwner {
41 item: String,
42 owner: String,
43 requester: String,
44 },
45 UnknownItem { item: String },
47}
48
49impl std::fmt::Display for LockError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 LockError::AlreadyClaimed {
53 item,
54 owner,
55 requester,
56 } => write!(
57 f,
58 "'{item}' is already claimed by agent '{owner}' (requested by '{requester}')"
59 ),
60 LockError::NotClaimed { item } => write!(f, "'{item}' is not claimed"),
61 LockError::NotOwner {
62 item,
63 owner,
64 requester,
65 } => write!(f, "'{item}' is claimed by '{owner}', not '{requester}'"),
66 LockError::UnknownItem { item } => {
67 write!(f, "no spec item named '{item}' found")
68 }
69 }
70 }
71}
72
73pub type SpecItems = HashMap<String, SpecItemKind>;
75
76pub fn extract_spec_items(report: &crate::audit::AuditReport) -> SpecItems {
78 report
79 .entries
80 .iter()
81 .map(|e| (e.name.clone(), e.kind))
82 .collect()
83}
84
85pub fn lock_item(
87 lockfile: &mut LockFile,
88 items: &SpecItems,
89 item: &str,
90 agent: &str,
91 timestamp: &str,
92) -> Result<(), LockError> {
93 let kind = items.get(item).ok_or_else(|| LockError::UnknownItem {
95 item: item.to_string(),
96 })?;
97
98 if let Some(claim) = lockfile.claims.get(item)
100 && claim.agent != agent
101 {
102 return Err(LockError::AlreadyClaimed {
103 item: item.to_string(),
104 owner: claim.agent.clone(),
105 requester: agent.to_string(),
106 });
107 }
108
109 lockfile.claims.insert(
110 item.to_string(),
111 Claim {
112 agent: agent.to_string(),
113 kind: *kind,
114 claimed_at: timestamp.to_string(),
115 },
116 );
117 Ok(())
118}
119
120pub fn unlock_item(lockfile: &mut LockFile, item: &str, agent: &str) -> Result<(), LockError> {
122 match lockfile.claims.get(item) {
123 None => Err(LockError::NotClaimed {
124 item: item.to_string(),
125 }),
126 Some(claim) if claim.agent != agent => Err(LockError::NotOwner {
127 item: item.to_string(),
128 owner: claim.agent.clone(),
129 requester: agent.to_string(),
130 }),
131 Some(_) => {
132 lockfile.claims.remove(item);
133 Ok(())
134 }
135 }
136}
137
138pub fn format_status(lockfile: &LockFile, items: &SpecItems) -> String {
140 let mut out = format!("Lock status for module: {}\n\n", lockfile.module);
141
142 if lockfile.claims.is_empty() {
143 out.push_str(" No items are currently claimed.\n");
144 return out;
145 }
146
147 let mut claims: Vec<_> = lockfile.claims.iter().collect();
149 claims.sort_by_key(|(name, _)| (*name).clone());
150
151 for (name, claim) in &claims {
152 out.push_str(&format!(
153 " {} {} — claimed by '{}' at {}\n",
154 claim.kind, name, claim.agent, claim.claimed_at,
155 ));
156 }
157
158 let mut unclaimed: Vec<_> = items
160 .iter()
161 .filter(|(name, _)| !lockfile.claims.contains_key(*name))
162 .collect();
163 unclaimed.sort_by_key(|(name, _)| (*name).clone());
164
165 if !unclaimed.is_empty() {
166 out.push_str("\n Unclaimed:\n");
167 for (name, kind) in &unclaimed {
168 out.push_str(&format!(" {} {}\n", kind, name));
169 }
170 }
171
172 out
173}