use std::fmt;
use std::path::Path;
use std::thread;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use thiserror::Error;
use crate::workspace::Workspace;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FalseGreenVerdict {
Accepted,
Failed,
Incomplete,
InsufficientEvidence,
Invalid,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompletionAuthority {
#[serde(default)]
pub authority_ready: bool,
#[serde(default)]
pub may_claim_complete: bool,
}
impl CompletionAuthority {
#[must_use]
pub const fn permits_completion(self) -> bool {
self.authority_ready && self.may_claim_complete
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FalseGreenResult {
#[serde(alias = "verdict")]
pub verification: FalseGreenVerdict,
#[serde(alias = "canonical_status")]
pub verification_status: String,
#[serde(default)]
pub completion_authority: CompletionAuthority,
pub repairable: bool,
pub candidate_sha256: String,
pub authoritative_source_sha256: Option<String>,
pub run_id: Option<String>,
pub evidence: Value,
}
impl FalseGreenResult {
#[must_use]
pub fn accepted(candidate_sha256: impl Into<String>) -> Self {
Self {
verification: FalseGreenVerdict::Accepted,
verification_status: "accepted".to_owned(),
completion_authority: CompletionAuthority {
authority_ready: true,
may_claim_complete: true,
},
repairable: false,
candidate_sha256: candidate_sha256.into(),
authoritative_source_sha256: None,
run_id: None,
evidence: json!({"status": "accepted"}),
}
}
#[must_use]
pub fn accepted_awaiting_authority(candidate_sha256: impl Into<String>) -> Self {
Self {
verification: FalseGreenVerdict::Accepted,
verification_status: "accepted".to_owned(),
completion_authority: CompletionAuthority::default(),
repairable: false,
candidate_sha256: candidate_sha256.into(),
authoritative_source_sha256: None,
run_id: None,
evidence: json!({"status": "accepted"}),
}
}
#[must_use]
pub fn failed(candidate_sha256: impl Into<String>, evidence: Value) -> Self {
Self {
verification: FalseGreenVerdict::Failed,
verification_status: "failed".to_owned(),
completion_authority: CompletionAuthority::default(),
repairable: true,
candidate_sha256: candidate_sha256.into(),
authoritative_source_sha256: None,
run_id: None,
evidence,
}
}
#[must_use]
pub fn permits_completion(&self) -> bool {
self.verification == FalseGreenVerdict::Accepted
&& self.completion_authority.permits_completion()
}
}
#[derive(Debug, Error)]
pub enum FalseGreenError {
#[error(transparent)]
Client(#[from] falsegreen_core::ClientError),
#[error("canonical FalseGreen result has no string status")]
MissingStatus,
#[error("canonical FalseGreen pending result has no run identity")]
MissingRunIdentity,
#[error("canonical FalseGreen pending run identity changed")]
RunIdentityChanged,
#[error("canonical FalseGreen verification timed out")]
Timeout,
#[error("canonical FalseGreen repair evidence is unavailable")]
MissingRepairEvidence,
}
pub trait FalseGreenVerifier {
fn verify(
&mut self,
workspace: &Workspace,
candidate_sha256: &str,
) -> Result<FalseGreenResult, FalseGreenError>;
fn prepare_repair(&mut self, _workspace: &Workspace) -> Result<Value, FalseGreenError> {
Ok(Value::Null)
}
}
trait CanonicalClient {
fn check_completion(
&self,
workspace_root: &Path,
task_id: &str,
) -> Result<Value, FalseGreenError>;
fn authority_status(&self, task_id: &str) -> Result<Value, FalseGreenError>;
}
impl CanonicalClient for falsegreen_core::Client {
fn check_completion(
&self,
workspace_root: &Path,
task_id: &str,
) -> Result<Value, FalseGreenError> {
Ok(self
.check_completion(workspace_root, task_id)?
.into_payload())
}
fn authority_status(&self, task_id: &str) -> Result<Value, FalseGreenError> {
Ok(self.authority_status(task_id)?.into_payload())
}
}
pub struct EmbeddedFalseGreenVerifier {
client: Box<dyn CanonicalClient>,
task_id: String,
verification_timeout: Duration,
poll_interval: Duration,
repair_evidence: Option<Value>,
}
impl EmbeddedFalseGreenVerifier {
pub fn from_stored_session(
task_id: impl Into<String>,
verification_timeout: Duration,
) -> Result<Self, FalseGreenError> {
Ok(Self {
client: Box::new(falsegreen_core::Client::from_stored_session()?),
task_id: task_id.into(),
verification_timeout,
poll_interval: Duration::from_millis(250),
repair_evidence: None,
})
}
#[cfg(test)]
fn with_client(task_id: impl Into<String>, client: impl CanonicalClient + 'static) -> Self {
Self {
client: Box::new(client),
task_id: task_id.into(),
verification_timeout: Duration::from_secs(1),
poll_interval: Duration::ZERO,
repair_evidence: None,
}
}
fn completion_decision(&self, workspace_root: &Path) -> Result<Value, FalseGreenError> {
let started = Instant::now();
let mut pending_run_id: Option<String> = None;
loop {
let decision = self
.client
.check_completion(workspace_root, &self.task_id)?;
let status = decision
.get("status")
.and_then(Value::as_str)
.ok_or(FalseGreenError::MissingStatus)?;
if !pending_status(status) {
return Ok(decision);
}
let run_id = decision
.get("run_id")
.and_then(Value::as_str)
.ok_or(FalseGreenError::MissingRunIdentity)?;
if pending_run_id
.as_deref()
.is_some_and(|expected| expected != run_id)
{
return Err(FalseGreenError::RunIdentityChanged);
}
pending_run_id.get_or_insert_with(|| run_id.to_owned());
let elapsed = started.elapsed();
if elapsed >= self.verification_timeout {
return Err(FalseGreenError::Timeout);
}
thread::sleep(
self.poll_interval
.min(self.verification_timeout.saturating_sub(elapsed)),
);
}
}
}
impl fmt::Debug for EmbeddedFalseGreenVerifier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EmbeddedFalseGreenVerifier")
.field("task_id", &self.task_id)
.field("verification_timeout", &self.verification_timeout)
.field("repair_evidence", &self.repair_evidence)
.finish_non_exhaustive()
}
}
impl FalseGreenVerifier for EmbeddedFalseGreenVerifier {
fn verify(
&mut self,
workspace: &Workspace,
candidate_sha256: &str,
) -> Result<FalseGreenResult, FalseGreenError> {
let decision = self.completion_decision(workspace.root())?;
let status = decision
.get("status")
.and_then(Value::as_str)
.ok_or(FalseGreenError::MissingStatus)?
.to_owned();
let authority = self.client.authority_status(&self.task_id)?;
let repairable = explicit_true(&decision, "repair_authorized");
self.repair_evidence = repairable.then(|| decision.clone());
let verification = match status.as_str() {
"accepted" => FalseGreenVerdict::Accepted,
"failed" | "unsafe" => FalseGreenVerdict::Failed,
"incomplete" => FalseGreenVerdict::Incomplete,
"insufficient_evidence" => FalseGreenVerdict::InsufficientEvidence,
_ => FalseGreenVerdict::Invalid,
};
let completion_authority = CompletionAuthority {
authority_ready: explicit_true(&authority, "authority_ready"),
may_claim_complete: explicit_true(&authority, "may_claim_complete"),
};
Ok(FalseGreenResult {
verification,
verification_status: status,
completion_authority,
repairable,
candidate_sha256: candidate_sha256.to_owned(),
authoritative_source_sha256: source_sha256(&decision),
run_id: decision
.get("run_id")
.and_then(Value::as_str)
.map(str::to_owned),
evidence: json!({
"completion_decision": decision,
"authority_status": authority
}),
})
}
fn prepare_repair(&mut self, _workspace: &Workspace) -> Result<Value, FalseGreenError> {
self.repair_evidence
.take()
.ok_or(FalseGreenError::MissingRepairEvidence)
}
}
fn explicit_true(payload: &Value, name: &str) -> bool {
payload.get(name).and_then(Value::as_bool) == Some(true)
}
fn pending_status(status: &str) -> bool {
matches!(
status,
"QUEUED" | "CLAIMED" | "EXECUTING" | "RUNNING" | "queued" | "running"
)
}
fn source_sha256(payload: &Value) -> Option<String> {
payload
.get("source_sha256")
.or_else(|| payload.get("source_artifact_sha256"))
.and_then(Value::as_str)
.map(str::to_owned)
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::collections::VecDeque;
use std::path::Path;
use std::rc::Rc;
use serde_json::{Value, json};
use crate::workspace::Workspace;
use crate::workspace::tests::git_fixture;
use super::{
CanonicalClient, CompletionAuthority, EmbeddedFalseGreenVerifier, FalseGreenError,
FalseGreenVerdict, FalseGreenVerifier,
};
struct ScriptedClient {
checks: RefCell<VecDeque<Value>>,
statuses: RefCell<VecDeque<Value>>,
calls: Rc<RefCell<Vec<String>>>,
}
impl ScriptedClient {
fn new(check: Value, status: Value) -> (Self, Rc<RefCell<Vec<String>>>) {
Self::with_checks(vec![check], status)
}
fn with_checks(checks: Vec<Value>, status: Value) -> (Self, Rc<RefCell<Vec<String>>>) {
let calls = Rc::new(RefCell::new(Vec::new()));
(
Self {
checks: RefCell::new(VecDeque::from(checks)),
statuses: RefCell::new(VecDeque::from([status])),
calls: Rc::clone(&calls),
},
calls,
)
}
}
impl CanonicalClient for ScriptedClient {
fn check_completion(
&self,
workspace_root: &Path,
task_id: &str,
) -> Result<Value, FalseGreenError> {
assert!(workspace_root.is_dir());
self.calls.borrow_mut().push(format!("check:{task_id}"));
Ok(self.checks.borrow_mut().pop_front().expect("check result"))
}
fn authority_status(&self, task_id: &str) -> Result<Value, FalseGreenError> {
self.calls.borrow_mut().push(format!("status:{task_id}"));
Ok(self
.statuses
.borrow_mut()
.pop_front()
.expect("status result"))
}
}
fn verify_shape(
decision: Value,
authority: Value,
) -> (super::FalseGreenResult, EmbeddedFalseGreenVerifier) {
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
let (client, _) = ScriptedClient::new(decision, authority);
let mut verifier = EmbeddedFalseGreenVerifier::with_client("task_1", client);
let result = verifier.verify(&workspace, "candidate").expect("verify");
(result, verifier)
}
#[test]
fn canonical_check_and_status_are_called_in_process() {
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
let (client, calls) = ScriptedClient::new(
json!({"task_id": "task_1", "status": "accepted"}),
json!({
"task_id": "task_1",
"status": "accepted",
"authority_ready": true,
"may_claim_complete": true
}),
);
let mut verifier = EmbeddedFalseGreenVerifier::with_client("task_1", client);
let result = verifier.verify(&workspace, "candidate").expect("verify");
assert_eq!(&*calls.borrow(), &["check:task_1", "status:task_1"]);
assert!(result.permits_completion());
}
#[test]
fn hosted_pending_run_is_polled_without_changing_run_identity() {
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
let (client, calls) = ScriptedClient::with_checks(
vec![
json!({"task_id": "task_1", "status": "QUEUED", "run_id": "run_1"}),
json!({"task_id": "task_1", "status": "EXECUTING", "run_id": "run_1"}),
json!({"task_id": "task_1", "status": "accepted", "run_id": "result_1"}),
],
json!({
"task_id": "task_1",
"status": "accepted",
"authority_ready": true,
"may_claim_complete": true
}),
);
let mut verifier = EmbeddedFalseGreenVerifier::with_client("task_1", client);
let result = verifier.verify(&workspace, "candidate").expect("verify");
assert_eq!(
&*calls.borrow(),
&[
"check:task_1",
"check:task_1",
"check:task_1",
"status:task_1"
]
);
assert!(result.permits_completion());
assert_eq!(result.run_id.as_deref(), Some("result_1"));
}
#[test]
fn hosted_pending_run_identity_cannot_change_during_polling() {
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
let (client, _) = ScriptedClient::with_checks(
vec![
json!({"task_id": "task_1", "status": "QUEUED", "run_id": "run_1"}),
json!({"task_id": "task_1", "status": "EXECUTING", "run_id": "run_2"}),
],
json!({"task_id": "task_1", "status": "accepted"}),
);
let mut verifier = EmbeddedFalseGreenVerifier::with_client("task_1", client);
assert!(matches!(
verifier.verify(&workspace, "candidate"),
Err(FalseGreenError::RunIdentityChanged)
));
}
#[test]
fn accepted_verification_remains_accepted_while_awaiting_authority() {
let (result, _) = verify_shape(
json!({"task_id": "task_1", "status": "accepted"}),
json!({
"task_id": "task_1",
"status": "accepted",
"authority_ready": false,
"may_claim_complete": false
}),
);
assert_eq!(result.verification, FalseGreenVerdict::Accepted);
assert_eq!(result.verification_status, "accepted");
assert_eq!(result.completion_authority, CompletionAuthority::default());
assert!(!result.permits_completion());
}
#[test]
fn authority_matrix_requires_both_flags_and_accepted_verification() {
for authority_ready in [false, true] {
for may_claim_complete in [false, true] {
let (result, _) = verify_shape(
json!({"task_id": "task_1", "status": "accepted"}),
json!({
"task_id": "task_1",
"status": "accepted",
"authority_ready": authority_ready,
"may_claim_complete": may_claim_complete
}),
);
assert_eq!(
result.permits_completion(),
authority_ready && may_claim_complete
);
}
}
let (failed, _) = verify_shape(
json!({"task_id": "task_1", "status": "failed"}),
json!({
"task_id": "task_1",
"status": "failed",
"authority_ready": true,
"may_claim_complete": true
}),
);
assert_eq!(failed.verification, FalseGreenVerdict::Failed);
assert!(!failed.permits_completion());
}
#[test]
fn only_core_repair_authorization_opens_repair_path_without_double_consumption() {
let decision = json!({
"task_id": "task_1",
"status": "incomplete",
"repair_authorized": true,
"repair_cycles_remaining": 1,
"failures": [{"criterion": "AC-1", "summary": "failed"}]
});
let (result, mut verifier) = verify_shape(
decision.clone(),
json!({"task_id": "task_1", "status": "incomplete"}),
);
assert!(result.repairable);
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
assert_eq!(verifier.prepare_repair(&workspace).unwrap(), decision);
assert!(matches!(
verifier.prepare_repair(&workspace),
Err(FalseGreenError::MissingRepairEvidence)
));
}
#[test]
fn local_status_and_remaining_budget_cannot_infer_repair_authority() {
let (result, mut verifier) = verify_shape(
json!({
"task_id": "task_1",
"status": "incomplete",
"repair_cycles_remaining": 2
}),
json!({"task_id": "task_1", "status": "incomplete"}),
);
assert!(!result.repairable);
let directory = git_fixture();
let workspace = Workspace::open(directory.path()).expect("workspace");
assert!(matches!(
verifier.prepare_repair(&workspace),
Err(FalseGreenError::MissingRepairEvidence)
));
}
#[test]
fn unknown_status_is_invalid_even_with_authority_flags() {
let (result, _) = verify_shape(
json!({"task_id": "task_1", "status": "model_says_done"}),
json!({
"task_id": "task_1",
"authority_ready": true,
"may_claim_complete": true
}),
);
assert_eq!(result.verification, FalseGreenVerdict::Invalid);
assert!(!result.permits_completion());
}
}