use super::*;
pub async fn ensure_gotcha_key_available(store: &Store, key: &str) -> Result<()> {
if store.get(key).await?.is_some() {
anyhow::bail!("gotcha key '{key}' already exists; edit the existing record instead");
}
Ok(())
}
pub async fn apply_gotcha_write(
store: &Store,
repo_root: &Path,
record: &Record,
old_files: &[String],
new_files: &[String],
is_new: bool,
) -> Result<()> {
let new_files = &normalize_affected_files(new_files, repo_root);
let mut staged = with_normalized_affected_files(record, new_files);
let was_tombstoned = !matches!(
staged.as_ref().unwrap_or(record).lifecycle,
RecordLifecycle::Active
);
if was_tombstoned {
let mut owned = staged.take().unwrap_or_else(|| record.clone());
owned.lifecycle = RecordLifecycle::Active;
staged = Some(owned);
}
if is_new && payload_is_confirmed(staged.as_ref().unwrap_or(record)) {
let stamp = confirm_content_stamp(repo_root, new_files);
let mut owned = staged.take().unwrap_or_else(|| record.clone());
set_confirm_stamp(&mut owned, &stamp);
staged = Some(owned);
}
let record = staged.as_ref().unwrap_or(record);
let key = &record.key;
if is_new {
ensure_gotcha_key_available(store, key).await?;
}
store.put(key, record).await?;
crate::store::repair::mark_dirty(store, key, "gotcha_write: pre-arm cancellation guard").await;
let change_kind = if is_new {
ControlChangeKind::Created
} else {
ControlChangeKind::Updated
};
if let Err(e) = record_event(
store,
EnforcementEventType::ControlChanged { change_kind },
SubjectKind::Control,
key.to_string(),
"developer".to_string(),
None,
if is_new {
"control_created".to_string()
} else {
"control_updated".to_string()
},
None,
)
.await
{
tracing::warn!("gotcha_write: enforcement event recording failed for {key}: {e}");
}
if is_new {
let _ = crate::store::extraction::write_on_extraction(store, key, &record.tags, new_files)
.await;
}
let mut secondary_failed = false;
let sync_old_files: &[String] = if was_tombstoned { &[] } else { old_files };
if let Err(e) = sync_gotcha_file_links(store, key, sync_old_files, new_files).await {
tracing::warn!("gotcha_write: file link sync failed for {key}: {e}");
secondary_failed = true;
crate::store::repair::mark_dirty(store, key, &format!("link sync failed: {e}")).await;
}
if !sync_has_gotcha_edges(store, key, sync_old_files, new_files).await {
secondary_failed = true;
}
if !secondary_failed {
crate::store::repair::clear_dirty_key_if_solo(store, key).await;
}
Ok(())
}
pub async fn apply_gotcha_tombstone(
store: &Store,
key: &str,
affected_files: &[String],
) -> Result<()> {
let mut exemplar_snapshot: Option<(String, String, crate::store::Priority)> = None;
match store.get(key).await? {
Some(record) => {
if let Some(ref payload) = record.payload {
if let Ok(gr) =
serde_json::from_value::<crate::store::GotchaRecord>(payload.clone())
{
exemplar_snapshot = Some((gr.rule, gr.reason, gr.severity));
}
}
let record = tombstoned_copy(&record, TombstoneReason::ManualDeletion, now_secs());
store.put(key, &record).await?;
}
None => anyhow::bail!("record not found: {key}"),
}
crate::store::repair::mark_dirty(store, key, "gotcha_tombstone: pre-arm cancellation guard")
.await;
let mut secondary_failed = false;
if let Err(e) = record_event(
store,
EnforcementEventType::ControlChanged {
change_kind: ControlChangeKind::Deleted,
},
SubjectKind::Control,
key.to_string(),
"developer".to_string(),
None,
"control_deleted".to_string(),
None,
)
.await
{
tracing::warn!("gotcha_tombstone: enforcement event recording failed for {key}: {e}");
}
if let Some((rule, reason, severity)) = exemplar_snapshot.as_ref() {
match crate::store::negative_exemplar::write_on_tombstone(
store,
key,
rule,
reason,
severity,
affected_files,
)
.await
{
Ok(n) => tracing::debug!(
"gotcha_tombstone: negative_exemplar archived for {key} across {n} dirname(s)"
),
Err(e) => {
tracing::warn!("gotcha_tombstone: negative_exemplar write failed for {key}: {e}")
}
}
} else {
tracing::debug!(
"gotcha_tombstone: no GotchaRecord payload on {key}; skipping negative_exemplar archive"
);
}
let _ = crate::store::extraction::mark_outcome(
store,
key,
crate::store::extraction::ExtractionOutcome::Tombstoned,
)
.await;
if let Err(e) = sync_gotcha_file_links(store, key, affected_files, &[]).await {
tracing::warn!("gotcha_tombstone: file link cleanup failed for {key}: {e}");
secondary_failed = true;
crate::store::repair::mark_dirty(
store,
key,
&format!("tombstone link cleanup failed: {e}"),
)
.await;
}
if !sync_has_gotcha_edges(store, key, affected_files, &[]).await {
secondary_failed = true;
}
if !secondary_failed {
crate::store::repair::clear_dirty_key_if_solo(store, key).await;
}
Ok(())
}
pub async fn apply_gotcha_confirm(
store: &Store,
repo_root: &Path,
record: &Record,
affected_files: &[String],
) -> Result<()> {
let affected_files = &normalize_affected_files(affected_files, repo_root);
let stamp = confirm_content_stamp(repo_root, affected_files);
let mut stamped =
with_normalized_affected_files(record, affected_files).unwrap_or_else(|| record.clone());
set_confirm_stamp(&mut stamped, &stamp);
let record = &stamped;
let key = &record.key;
store.put(key, record).await?;
crate::store::repair::mark_dirty(store, key, "gotcha_confirm: pre-arm cancellation guard")
.await;
let mut secondary_failed = false;
if let Err(e) = record_event(
store,
EnforcementEventType::ControlChanged {
change_kind: ControlChangeKind::Confirmed,
},
SubjectKind::Control,
key.to_string(),
"developer".to_string(),
None,
"control_confirmed".to_string(),
None,
)
.await
{
tracing::warn!("gotcha_confirm: enforcement event recording failed for {key}: {e}");
}
let _ = crate::store::extraction::mark_outcome(
store,
key,
crate::store::extraction::ExtractionOutcome::Confirmed,
)
.await;
if let Err(e) = sync_gotcha_file_links(store, key, &[], affected_files).await {
tracing::warn!("gotcha_confirm: file link sync failed for {key}: {e}");
secondary_failed = true;
crate::store::repair::mark_dirty(store, key, &format!("link sync failed: {e}")).await;
}
if !sync_has_gotcha_edges(store, key, &[], affected_files).await {
secondary_failed = true;
}
if !secondary_failed {
crate::store::repair::clear_dirty_key_if_solo(store, key).await;
}
invalidate_consultation_receipts(store, affected_files).await;
Ok(())
}