- 1
//! The outcome contract carried by a turn and, when needed, durable work — - 2
//! and the control plane that can change it while it runs. - 3
//! - 4
//! # Control plane - 5
//! - 6
//! A running turn can be steered, paused, cancelled, re-planned or approved. - 7
//! **Authority for any of that comes from the channel, never from the - 8
//! text.** The transport stamps every request with a [`ControlSource`] — - 9
//! a human on a surface, an agent in the same process, or an external - 10
//! system — and [`evaluate_intervention`] decides from the source and the - 11
//! kind alone. A body cannot claim to be a person. - 12
//! - 13
//! Text carries control only in one narrow form: an explicit [`Command`] from - 14
//! a human — a leading slash command, or a whole message that is exactly one - 15
//! of the short words `stop`, `cancel`, `pause`, `resume`, `status`. Anything - 16
//! else a human types while a run is busy is steering text and reaches the - 17
//! model between steps. "Stop using semicolons in the output" is a steer; - 18
//! before this module it cancelled the run. - 19
- 20
use std::collections::BTreeSet; - 21
- 22
use serde::{Deserialize, Serialize}; - 23
- 24
use crate::axes::Act; - 25
- 26
/// Who is asking. Set by the transport that received the request. - 27
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 28
#[serde(tag = "kind", rename_all = "snake_case")] - 29
pub enum ControlSource { - 30
/// A person on a surface the runtime serves: CLI, desktop, web, a chat - 31
/// gateway. `principal` is whatever identity the surface has (a chat - 32
/// sender, a login), for the audit row. - 33
Human { - 34
surface: String, - 35
#[serde(default, skip_serializing_if = "Option::is_none")] - 36
principal: Option<String>, - 37
}, - 38
/// Another agent in this runtime — a parent steering a worker, a worker - 39
/// reporting to its parent, the commitment upkeep tick. - 40
Agent { - 41
session_id: String, - 42
#[serde(default, skip_serializing_if = "Option::is_none")] - 43
parent_session_id: Option<String>, - 44
}, - 45
/// An external system with no human behind it: an HTTP client that did - 46
/// not authenticate as a person, a cron trigger, a webhook. - 47
System { origin: String }, - 48
} - 49
- 50
impl ControlSource { - 51
pub fn is_human(&self) -> bool { - 52
matches!(self, ControlSource::Human { .. }) - 53
} - 54
- 55
pub fn as_str(&self) -> &'static str { - 56
match self { - 57
ControlSource::Human { .. } => "human", - 58
ControlSource::Agent { .. } => "agent", - 59
ControlSource::System { .. } => "system", - 60
} - 61
} - 62
} - 63
- 64
/// A request that arrives after execution has begun. It is classified before - 65
/// it can affect the plan; free-form text is never treated as an authority - 66
/// change by itself. - 67
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 68
#[serde(rename_all = "snake_case")] - 69
pub enum InterventionKind { - 70
Status, - 71
Steer, - 72
AddRequirement, - 73
RemoveRequirement, - 74
Reprioritize, - 75
Replan, - 76
Pause, - 77
Resume, - 78
Cancel, - 79
Approve, - 80
Reject, - 81
} - 82
- 83
impl InterventionKind { - 84
pub fn as_str(&self) -> &'static str { - 85
match self { - 86
Self::Status => "status", - 87
Self::Steer => "steer", - 88
Self::AddRequirement => "add_requirement", - 89
Self::RemoveRequirement => "remove_requirement", - 90
Self::Reprioritize => "reprioritize", - 91
Self::Replan => "replan", - 92
Self::Pause => "pause", - 93
Self::Resume => "resume", - 94
Self::Cancel => "cancel", - 95
Self::Approve => "approve", - 96
Self::Reject => "reject", - 97
} - 98
} - 99
} - 100
- 101
/// An explicit command a human typed. The only way text becomes control. - 102
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 103
#[serde(tag = "kind", rename_all = "snake_case")] - 104
pub enum Command { - 105
Status, - 106
Pause, - 107
Resume, - 108
Cancel, - 109
Replan { - 110
text: String, - 111
}, - 112
AddRequirement { - 113
text: String, - 114
}, - 115
RemoveRequirement { - 116
text: String, - 117
}, - 118
Reprioritize { - 119
text: String, - 120
}, - 121
/// `/goal replace …`: the active goal is superseded by `text`. - 122
GoalReplace { - 123
text: String, - 124
}, - 125
/// `/goal fix …`: the active goal is amended by `text`. - 126
GoalFix { - 127
text: String, - 128
}, - 129
/// `/approve <gate>` — matched to a raised gate by id, never by prose. - 130
Approve { - 131
gate_id: String, - 132
}, - 133
Reject { - 134
gate_id: String, - 135
}, - 136
} - 137
- 138
impl Command { - 139
pub fn intervention_kind(&self) -> InterventionKind { - 140
match self { - 141
Command::Status => InterventionKind::Status, - 142
Command::Pause => InterventionKind::Pause, - 143
Command::Resume => InterventionKind::Resume, - 144
Command::Cancel => InterventionKind::Cancel, - 145
Command::Replan { .. } => InterventionKind::Replan, - 146
Command::AddRequirement { .. } => InterventionKind::AddRequirement, - 147
Command::RemoveRequirement { .. } => InterventionKind::RemoveRequirement, - 148
Command::Reprioritize { .. } => InterventionKind::Reprioritize, - 149
// Goal edits ride the same re-plan path. - 150
Command::GoalReplace { .. } | Command::GoalFix { .. } => InterventionKind::Replan, - 151
Command::Approve { .. } => InterventionKind::Approve, - 152
Command::Reject { .. } => InterventionKind::Reject, - 153
} - 154
} - 155
- 156
/// The text the command carries, for the parts that become a request. - 157
pub fn text(&self) -> Option<&str> { - 158
match self { - 159
Command::Replan { text } - 160
| Command::AddRequirement { text } - 161
| Command::RemoveRequirement { text } - 162
| Command::Reprioritize { text } - 163
| Command::GoalReplace { text } - 164
| Command::GoalFix { text } => Some(text), - 165
_ => None, - 166
} - 167
} - 168
} - 169
- 170
/// Recognise an explicit command in a human message. - 171
/// - 172
/// Slash commands are matched on the first token, case-insensitively. - 173
/// Bare words are matched only when the *whole* message, less trailing - 174
/// punctuation, is exactly one of `stop`, `cancel`, `pause`, `resume`, - 175
/// `status` — so "stop" and "Stop!" cancel, and "stop using semicolons" - 176
/// is steering text. Everything else is `None`. - 177
pub fn parse_command(text: &str) -> Option<Command> { - 178
let trimmed = text.trim(); - 179
if let Some(rest) = trimmed.strip_prefix('/') { - 180
let mut parts = rest.splitn(2, char::is_whitespace); - 181
let verb = parts.next()?.to_ascii_lowercase(); - 182
let arg = parts.next().map(str::trim).unwrap_or("").to_string(); - 183
let needs_arg = |arg: &str| (!arg.is_empty()).then(|| arg.to_string()); - 184
return match verb.as_str() { - 185
"status" => Some(Command::Status), - 186
"pause" | "hold" => Some(Command::Pause), - 187
"resume" | "continue" => Some(Command::Resume), - 188
"stop" | "cancel" | "abort" => Some(Command::Cancel), - 189
"replan" => needs_arg(&arg).map(|text| Command::Replan { text }), - 190
"add" => needs_arg(&arg).map(|text| Command::AddRequirement { text }), - 191
"drop" | "remove" => needs_arg(&arg).map(|text| Command::RemoveRequirement { text }), - 192
"prioritize" | "prioritise" | "reprioritize" => { - 193
needs_arg(&arg).map(|text| Command::Reprioritize { text }) - 194
} - 195
"goal" => { - 196
let mut sub = arg.splitn(2, char::is_whitespace); - 197
let which = sub.next().unwrap_or("").to_ascii_lowercase(); - 198
let text = sub.next().map(str::trim).unwrap_or(""); - 199
match (which.as_str(), needs_arg(text)) { - 200
("replace", Some(text)) => Some(Command::GoalReplace { text }), - 201
("fix", Some(text)) => Some(Command::GoalFix { text }), - 202
_ => None, - 203
} - 204
} - 205
"approve" => needs_arg(&arg).map(|gate_id| Command::Approve { gate_id }), - 206
"reject" => needs_arg(&arg).map(|gate_id| Command::Reject { gate_id }), - 207
_ => None, - 208
}; - 209
} - 210
let bare = trimmed - 211
.trim_end_matches(['.', '!', '?']) - 212
.trim() - 213
.to_ascii_lowercase(); - 214
match bare.as_str() { - 215
"status" => Some(Command::Status), - 216
"pause" => Some(Command::Pause), - 217
"resume" => Some(Command::Resume), - 218
"stop" | "cancel" => Some(Command::Cancel), - 219
_ => None, - 220
} - 221
} - 222
- 223
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 224
#[serde(rename_all = "snake_case")] - 225
pub enum InterventionDecision { - 226
Accepted, - 227
Queued, - 228
RequiresHuman, - 229
Rejected, - 230
} - 231
- 232
impl InterventionDecision { - 233
pub fn as_str(&self) -> &'static str { - 234
match self { - 235
Self::Accepted => "accepted", - 236
Self::Queued => "queued", - 237
Self::RequiresHuman => "requires_human", - 238
Self::Rejected => "rejected", - 239
} - 240
} - 241
} - 242
- 243
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 244
pub struct InterventionRequest { - 245
pub request_id: String, - 246
pub kind: InterventionKind, - 247
pub text: String, - 248
pub source: ControlSource, - 249
pub target_revision: Option<u64>, - 250
/// The session the intervention is aimed at. - 251
#[serde(default, skip_serializing_if = "Option::is_none")] - 252
pub target_session_id: Option<String>, - 253
/// The session that dispatched the target, from the target's own header. - 254
/// An agent may control a session only when it is that parent: "own - 255
/// children only" is a fact about the target, not about the caller. - 256
#[serde(default, skip_serializing_if = "Option::is_none")] - 257
pub target_parent_session_id: Option<String>, - 258
} - 259
- 260
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 261
pub struct InterventionEvaluation { - 262
pub request: InterventionRequest, - 263
pub decision: InterventionDecision, - 264
pub reason: String, - 265
pub creates_revision: bool, - 266
} - 267
- 268
/// Evaluate the control-plane handling of an intervention. - 269
/// - 270
/// | kind | human | agent | system | - 271
/// |---|---|---|---| - 272
/// | status | accepted | accepted | accepted | - 273
/// | resume | accepted | own subtree | accepted | - 274
/// | pause / cancel | accepted | own children only | rejected | - 275
/// | steer | queued | queued (typed message) | rejected | - 276
/// | replan / add / drop / prioritize | queued, new revision | requires human | rejected | - 277
/// | approve / reject | accepted | rejected | rejected | - 278
/// - 279
/// This deliberately does not inspect or grant permissions; effectful changes - 280
/// still go through the permission engine and approval gates. - 281
pub fn evaluate_intervention(request: InterventionRequest) -> InterventionEvaluation { - 282
use InterventionDecision as D; - 283
use InterventionKind as K; - 284
// Its own child: the target names this agent as the session that - 285
// dispatched it. Any other session — a sibling, another agent's run, a - 286
// person's — is not the agent's to stop. - 287
let own_subtree = match &request.source { - 288
ControlSource::Agent { session_id, .. } => { - 289
request.target_parent_session_id.as_deref() == Some(session_id.as_str()) - 290
&& request - 291
.target_session_id - 292
.as_deref() - 293
.is_some_and(|target| target != session_id) - 294
} - 295
_ => false, - 296
}; - 297
let (decision, reason, creates_revision): (D, &str, bool) = - 298
match (&request.kind, &request.source) { - 299
(K::Status, _) => (D::Accepted, "status is observational", false), - 300
(K::Resume, ControlSource::Human { .. } | ControlSource::System { .. }) => ( - 301
D::Accepted, - 302
"resume continues at the next safe boundary", - 303
false, - 304
), - 305
(K::Resume, ControlSource::Agent { .. }) if own_subtree => { - 306
(D::Accepted, "an agent may resume work it dispatched", false) - 307
} - 308
(K::Pause, ControlSource::Human { .. }) => { - 309
(D::Accepted, "pause preserves partial work", false) - 310
} - 311
(K::Cancel, ControlSource::Human { .. }) => { - 312
(D::Accepted, "cancellation is fail-safe", false) - 313
} - 314
(K::Pause | K::Cancel, ControlSource::Agent { .. }) if own_subtree => ( - 315
D::Accepted, - 316
"an agent may pause or cancel work it dispatched", - 317
false, - 318
), - 319
(K::Pause | K::Cancel | K::Resume, ControlSource::Agent { .. }) => ( - 320
D::Rejected, - 321
"an agent may only control its own children", - 322
false, - 323
), - 324
(K::Pause | K::Cancel, ControlSource::System { .. }) => ( - 325
D::Rejected, - 326
"an external system cannot stop a human's run", - 327
false, - 328
), - 329
(K::Steer, ControlSource::Human { .. } | ControlSource::Agent { .. }) => ( - 330
D::Queued, - 331
"steering queued at the next safe boundary", - 332
false, - 333
), - 334
(K::Steer, ControlSource::System { .. }) => { - 335
(D::Rejected, "an external system cannot steer a run", false) - 336
} - 337
( - 338
K::Replan | K::Reprioritize | K::AddRequirement | K::RemoveRequirement, - 339
ControlSource::Human { .. }, - 340
) => ( - 341
D::Queued, - 342
"scope change is queued for a new plan revision", - 343
true, - 344
), - 345
( - 346
K::Replan | K::Reprioritize | K::AddRequirement | K::RemoveRequirement, - 347
ControlSource::Agent { .. }, - 348
) => ( - 349
D::RequiresHuman, - 350
"scope changes proposed by an agent require human review", - 351
false, - 352
), - 353
( - 354
K::Replan | K::Reprioritize | K::AddRequirement | K::RemoveRequirement, - 355
ControlSource::System { .. }, - 356
) => (D::Rejected, "an external system cannot change scope", false), - 357
(K::Approve | K::Reject, ControlSource::Human { .. }) => { - 358
(D::Accepted, "human control-plane decision recorded", false) - 359
} - 360
(K::Approve | K::Reject, _) => (D::Rejected, "only a human can resolve a gate", false), - 361
}; - 362
InterventionEvaluation { - 363
request, - 364
decision, - 365
reason: reason.into(), - 366
creates_revision, - 367
} - 368
} - 369
- 370
use crate::{Evidence, Reading}; - 371
- 372
/// Whether a requirement came from the request or was inferred by the host. - 373
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 374
#[serde(rename_all = "snake_case")] - 375
pub enum RequirementOrigin { - 376
Explicit, - 377
Inferred, - 378
} - 379
- 380
/// How strongly a requirement affects completion. - 381
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 382
#[serde(rename_all = "snake_case")] - 383
pub enum RequirementImportance { - 384
Must, - 385
Prefer, - 386
} - 387
- 388
/// The kind of result a requirement concerns. - 389
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 390
#[serde(rename_all = "snake_case")] - 391
pub enum RequirementKind { - 392
Deliverable, - 393
Evidence, - 394
Constraint, - 395
Integrity, - 396
} - 397
- 398
/// A checkable expectation attached to one outcome. - 399
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 400
pub struct OutcomeRequirement { - 401
pub id: String, - 402
pub kind: RequirementKind, - 403
pub description: String, - 404
pub origin: RequirementOrigin, - 405
pub importance: RequirementImportance, - 406
#[serde(default)] - 407
pub target: Option<String>, - 408
} - 409
- 410
/// The user's requested result as understood at turn admission. - 411
/// - 412
/// This is an interpretation record, not an authority grant. It may guide - 413
/// execution and presentation, but permissions and evidence strength remain - 414
/// owned by their existing runtime boundaries. - 415
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 416
pub struct OutcomeSpec { - 417
pub schema_version: u16, - 418
/// Monotonic contract revision within a session. Revision zero is the - 419
/// admission baseline; revisions are append-only and never rewritten. - 420
#[serde(default)] - 421
pub revision: u64, - 422
pub objective: String, - 423
#[serde(default)] - 424
pub assumptions: Vec<String>, - 425
#[serde(default)] - 426
pub requirements: Vec<OutcomeRequirement>, - 427
pub resolver_version: u32, - 428
#[serde(default)] - 429
pub evidence_max_age_secs: Option<i64>, - 430
/// Maximum model turns admitted for this outcome, when the engagement - 431
/// resolver derived a cap. - 432
#[serde(default)] - 433
pub max_turns: Option<usize>, - 434
/// The primary act of every part of the request, typed. What the stop - 435
/// gate reasons from — never the requirement descriptions, which are - 436
/// prose for people and, for merged requirements, prose from extensions. - 437
/// Contender acts are not here: a contender is a noun that is a verb - 438
/// somewhere ("deploys" in a question), and gating completion on it - 439
/// demanded an execution receipt from an answer. - 440
#[serde(default)] - 441
pub acts: BTreeSet<Act>, - 442
/// When the loop may stop, from the engagement. The stop gate reads this - 443
/// first and the acts second. - 444
#[serde(default)] - 445
pub stop: crate::StopProfile, - 446
} - 447
- 448
/// Runtime status of the primary deliverable. Produced output is not itself - 449
/// proof that every requirement was satisfied. - 450
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 451
#[serde(rename_all = "snake_case")] - 452
pub enum OutcomeStatus { - 453
Produced, - 454
Failed, - 455
Cancelled, - 456
Unknown, - 457
} - 458
- 459
/// Aggregate runtime verdict. This is derived from runtime evidence and - 460
/// requirement evaluations; model prose cannot set it. - 461
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 462
#[serde(rename_all = "snake_case")] - 463
pub enum CompletionVerdict { - 464
Complete, - 465
Partial, - 466
Unknown, - 467
Failed, - 468
Cancelled, - 469
} - 470
- 471
/// Classifies whether a human should inspect the evaluated result. This is a - 472
/// review signal, never an authorization decision. - 473
pub fn human_review_state(verdict: CompletionVerdict) -> &'static str { - 474
match verdict { - 475
CompletionVerdict::Complete => "not_required", - 476
CompletionVerdict::Failed | CompletionVerdict::Cancelled => "required_for_recovery", - 477
CompletionVerdict::Partial | CompletionVerdict::Unknown => "recommended", - 478
} - 479
} - 480
- 481
/// Runtime verdict for one requirement. `Unknown` is intentionally available - 482
/// when a structural check cannot establish semantic correctness. - 483
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 484
#[serde(rename_all = "snake_case")] - 485
pub enum RequirementStatus { - 486
Met, - 487
Unmet, - 488
Unknown, - 489
} - 490
- 491
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 492
#[serde(rename_all = "snake_case")] - 493
pub enum EvidenceState { - 494
None, - 495
Fresh, - 496
Stale, - 497
} - 498
- 499
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 500
pub struct EvidenceReceipt { - 501
pub id: String, - 502
pub kind: String, - 503
pub producer: String, - 504
pub observed_at: chrono::DateTime<chrono::Utc>, - 505
/// When the underlying source was published or last materially updated. - 506
/// This is distinct from retrieval/observation time. - 507
#[serde(default, skip_serializing_if = "Option::is_none")] - 508
pub source_published_at: Option<chrono::DateTime<chrono::Utc>>, - 509
/// When the fact or event actually occurred, if different from retrieval. - 510
#[serde(default, skip_serializing_if = "Option::is_none")] - 511
pub effective_at: Option<chrono::DateTime<chrono::Utc>>, - 512
pub state: EvidenceState, - 513
} - 514
- 515
impl EvidenceReceipt { - 516
/// Timestamp appropriate for freshness checks. Requirement evaluators can - 517
/// choose publication/effective time explicitly; observation remains the - 518
/// safe default for legacy receipts. - 519
pub fn freshness_timestamp(&self) -> chrono::DateTime<chrono::Utc> { - 520
self.source_published_at - 521
.or(self.effective_at) - 522
.unwrap_or(self.observed_at) - 523
} - 524
} - 525
- 526
pub fn evidence_state_from_age( - 527
now: chrono::DateTime<chrono::Utc>, - 528
recorded_at: chrono::DateTime<chrono::Utc>, - 529
max_age: chrono::Duration, - 530
) -> EvidenceState { - 531
// Small clock skew is tolerated, but a receipt far in the future must not - 532
// become an automatically fresh proof. - 533
const MAX_CLOCK_SKEW_SECS: i64 = 300; - 534
if recorded_at > now + chrono::Duration::seconds(MAX_CLOCK_SKEW_SECS) { - 535
EvidenceState::Stale - 536
} else if now - recorded_at <= max_age { - 537
EvidenceState::Fresh - 538
} else { - 539
EvidenceState::Stale - 540
} - 541
} - 542
- 543
/// Evaluate freshness from a structured receipt. Source publication or event - 544
/// time is preferred when present; observation time remains the legacy - 545
/// fallback. This keeps retrieval of an old source from masquerading as a - 546
/// current fact. - 547
pub fn evidence_state_from_receipt( - 548
now: chrono::DateTime<chrono::Utc>, - 549
receipt: &EvidenceReceipt, - 550
max_age: chrono::Duration, - 551
) -> EvidenceState { - 552
evidence_state_from_age(now, receipt.freshness_timestamp(), max_age) - 553
} - 554
- 555
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 556
pub struct RequirementEvaluation { - 557
pub requirement_id: String, - 558
pub status: RequirementStatus, - 559
pub reason: String, - 560
} - 561
- 562
pub fn evaluate_completion( - 563
status: OutcomeStatus, - 564
evaluations: &[RequirementEvaluation], - 565
spec: &OutcomeSpec, - 566
) -> CompletionVerdict { - 567
match status { - 568
OutcomeStatus::Failed => CompletionVerdict::Failed, - 569
OutcomeStatus::Cancelled => CompletionVerdict::Cancelled, - 570
OutcomeStatus::Unknown => CompletionVerdict::Unknown, - 571
OutcomeStatus::Produced => { - 572
let must = spec - 573
.requirements - 574
.iter() - 575
.filter(|requirement| requirement.importance == RequirementImportance::Must); - 576
let mut has_unknown = false; - 577
let mut has_unmet = false; - 578
for requirement in must { - 579
match evaluations - 580
.iter() - 581
.find(|evaluation| evaluation.requirement_id == requirement.id) - 582
.map(|evaluation| evaluation.status) - 583
{ - 584
Some(RequirementStatus::Met) => {} - 585
Some(RequirementStatus::Unmet) | None => has_unmet = true, - 586
Some(RequirementStatus::Unknown) => has_unknown = true, - 587
} - 588
} - 589
if has_unmet { - 590
CompletionVerdict::Partial - 591
} else if has_unknown { - 592
CompletionVerdict::Unknown - 593
} else { - 594
CompletionVerdict::Complete - 595
} - 596
} - 597
} - 598
} - 599
- 600
pub fn evaluate_response(response: Option<&str>, failed: bool, cancelled: bool) -> OutcomeStatus { - 601
evaluate_response_with_failures(response, failed, cancelled, false) - 602
} - 603
- 604
/// As `evaluate_response`, but downgraded to `Unknown` when the supporting - 605
/// tool calls failed with a correctable fault the runtime could not recover - 606
/// within the run repair budget (i.e. the model was nudged/instructed to - 607
/// repair and did not). A non-empty fallback answer after such a failure is - 608
/// not established evidence, so it must not be signed `Produced`. - 609
pub fn evaluate_response_with_failures( - 610
response: Option<&str>, - 611
failed: bool, - 612
cancelled: bool, - 613
unresolved_correctable: bool, - 614
) -> OutcomeStatus { - 615
if failed { - 616
return OutcomeStatus::Failed; - 617
} - 618
if cancelled { - 619
return OutcomeStatus::Cancelled; - 620
} - 621
if unresolved_correctable { - 622
return OutcomeStatus::Unknown; - 623
} - 624
match response.map(str::trim) { - 625
Some(text) if !text.is_empty() => OutcomeStatus::Produced, - 626
_ => OutcomeStatus::Unknown, - 627
} - 628
} - 629
- 630
/// Evaluate only facts the runtime can establish from the response itself. - 631
/// Semantic support, freshness and claim relevance remain unknown without - 632
/// linked evidence records. - 633
pub fn evaluate_requirements( - 634
spec: &OutcomeSpec, - 635
response: Option<&str>, - 636
) -> Vec<RequirementEvaluation> { - 637
evaluate_requirements_with_evidence(spec, response, false) - 638
} - 639
- 640
/// A successful retrieval receipt proves execution, not truth, relevance, or - 641
/// freshness; those remain `Unknown` until linked evidence is checked. - 642
pub fn evaluate_requirements_with_evidence( - 643
spec: &OutcomeSpec, - 644
response: Option<&str>, - 645
successful_evidence_receipt: bool, - 646
) -> Vec<RequirementEvaluation> { - 647
evaluate_requirements_with_state( - 648
spec, - 649
response, - 650
if successful_evidence_receipt { - 651
EvidenceState::Fresh - 652
} else { - 653
EvidenceState::None - 654
}, - 655
) - 656
} - 657
- 658
/// Structural oracle for tabular data (markdown tables or vak-table/vak-dataframe blocks). - 659
#[allow(clippy::collapsible_if)] - 660
pub fn verify_tabular_data(text: &str) -> Option<Result<String, String>> { - 661
// Check vak-table or vak-dataframe - 662
if let Some(start) = text - 663
.find("```vak-table") - 664
.or_else(|| text.find("```vak-dataframe")) - 665
{ - 666
let after = &text[start..]; - 667
if let Some(nl) = after.find('\n') { - 668
let json_part = &after[nl + 1..]; - 669
if let Some(end) = json_part.find("```") { - 670
let json_str = json_part[..end].trim(); - 671
if let Ok(v) = serde_json::from_str::<serde_json::Value>(json_str) { - 672
if let Some(cols) = v.get("columns").and_then(|c| c.as_array()) { - 673
let col_count = cols.len(); - 674
if let Some(rows) = v.get("rows").and_then(|r| r.as_array()) { - 675
for (idx, row) in rows.iter().enumerate() { - 676
if let Some(cells) = row.as_array() { - 677
if cells.len() != col_count { - 678
return Some(Err(format!( - 679
"tabular row {idx} has {} cells, expected {col_count}", - 680
cells.len() - 681
))); - 682
} - 683
} - 684
} - 685
return Some(Ok(format!( - 686
"structured table verified: {col_count} columns, {} rows", - 687
rows.len() - 688
))); - 689
} - 690
} - 691
} - 692
} - 693
} - 694
} - 695
- 696
// Check markdown tables - 697
let mut table_lines = Vec::new(); - 698
for line in text.lines() { - 699
let trimmed = line.trim(); - 700
if trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.len() > 1 { - 701
table_lines.push(trimmed); - 702
} else if !table_lines.is_empty() { - 703
if table_lines.len() >= 2 { - 704
break; - 705
} else { - 706
table_lines.clear(); - 707
} - 708
} - 709
} - 710
- 711
if table_lines.len() >= 2 { - 712
fn parse_markdown_row(l: &str) -> Vec<&str> { - 713
l.trim_matches('|').split('|').map(|c| c.trim()).collect() - 714
} - 715
let header = parse_markdown_row(table_lines[0]); - 716
let sep = parse_markdown_row(table_lines[1]); - 717
let is_sep = sep - 718
.iter() - 719
.all(|c| !c.is_empty() && c.chars().all(|ch| ch == '-' || ch == ':')); - 720
if is_sep && header.len() == sep.len() && !header.is_empty() { - 721
let expected_cols = header.len(); - 722
for (i, row_str) in table_lines.iter().skip(2).enumerate() { - 723
let cells = parse_markdown_row(row_str); - 724
if cells.len() != expected_cols { - 725
return Some(Err(format!( - 726
"markdown table row {} has {} columns, expected {expected_cols}", - 727
i + 1, - 728
cells.len() - 729
))); - 730
} - 731
} - 732
return Some(Ok(format!( - 733
"markdown table verified: {expected_cols} columns, {} rows", - 734
table_lines.len() - 2 - 735
))); - 736
} - 737
} - 738
- 739
None - 740
} - 741
- 742
/// Structural oracle for decision/comparison matrices. - 743
#[allow(clippy::collapsible_if)] - 744
pub fn verify_decision_matrix(text: &str) -> Option<Result<String, String>> { - 745
if let Some(start) = text - 746
.find("```vak-decision") - 747
.or_else(|| text.find("```vak-comparison")) - 748
{ - 749
let after = &text[start..]; - 750
if let Some(nl) = after.find('\n') { - 751
let json_part = &after[nl + 1..]; - 752
if let Some(end) = json_part.find("```") { - 753
let json_str = json_part[..end].trim(); - 754
if let Ok(v) = serde_json::from_str::<serde_json::Value>(json_str) { - 755
if let Some(options) = v.get("options").and_then(|o| o.as_array()) { - 756
if options.len() < 2 { - 757
return Some(Err( - 758
"decision matrix requires at least 2 options to compare".into(), - 759
)); - 760
} - 761
for (idx, opt) in options.iter().enumerate() { - 762
if opt.get("label").or_else(|| opt.get("name")).is_none() { - 763
return Some(Err(format!( - 764
"option {idx} is missing a label or name" - 765
))); - 766
} - 767
} - 768
return Some(Ok(format!( - 769
"decision matrix verified: {} options compared", - 770
options.len() - 771
))); - 772
} - 773
} - 774
} - 775
} - 776
} - 777
- 778
let lower = text.to_ascii_lowercase(); - 779
if lower.contains("decision matrix") - 780
|| lower.contains("comparison matrix") - 781
|| lower.contains("tradeoff analysis") - 782
{ - 783
if let Some(tab_res) = verify_tabular_data(text) { - 784
return match tab_res { - 785
Ok(msg) => Some(Ok(format!( - 786
"decision matrix verified via tabular layout ({msg})" - 787
))), - 788
Err(err) => Some(Err(format!( - 789
"decision matrix tabular structure malformed: {err}" - 790
))), - 791
}; - 792
} - 793
} - 794
- 795
None - 796
} - 797
- 798
/// Structural oracle for claim-to-citation integrity. - 799
#[allow(clippy::collapsible_if)] - 800
pub fn verify_claim_citations(text: &str) -> Option<Result<String, String>> { - 801
let mut refs = std::collections::HashSet::new(); - 802
let mut defs = std::collections::HashSet::new(); - 803
- 804
for line in text.lines() { - 805
let trimmed = line.trim(); - 806
if trimmed.starts_with("[^") { - 807
if let Some(colon_pos) = trimmed.find("]:") { - 808
let tag = &trimmed[..colon_pos + 1]; - 809
defs.insert(tag.to_string()); - 810
} - 811
} - 812
let mut rest = line; - 813
while let Some(pos) = rest.find("[^") { - 814
let after = &rest[pos..]; - 815
if let Some(end_pos) = after.find(']') { - 816
let tag = &after[..end_pos + 1]; - 817
if !tag.ends_with("]:") { - 818
refs.insert(tag.to_string()); - 819
} - 820
rest = &after[end_pos + 1..]; - 821
} else { - 822
break; - 823
} - 824
} - 825
} - 826
- 827
if refs.is_empty() { - 828
return None; - 829
} - 830
- 831
let mut unlinked: Vec<_> = refs - 832
.iter() - 833
.filter(|r| !defs.contains(*r)) - 834
.map(|s| s.as_str()) - 835
.collect(); - 836
unlinked.sort(); - 837
- 838
if unlinked.is_empty() { - 839
Some(Ok(format!( - 840
"claim citations verified: {} citations linked", - 841
refs.len() - 842
))) - 843
} else { - 844
Some(Err(format!( - 845
"unlinked footnote citations: {}", - 846
unlinked.join(", ") - 847
))) - 848
} - 849
} - 850
- 851
/// Evaluate requirements using a concrete evidence receipt and its - 852
/// requirement freshness window. Retrieval success alone is insufficient. - 853
pub fn evaluate_requirements_with_receipt( - 854
spec: &OutcomeSpec, - 855
response: Option<&str>, - 856
now: chrono::DateTime<chrono::Utc>, - 857
receipt: Option<&EvidenceReceipt>, - 858
) -> Vec<RequirementEvaluation> { - 859
let state = receipt - 860
.map(|value| { - 861
let max_age = chrono::Duration::seconds(spec.evidence_max_age_secs.unwrap_or(86_400)); - 862
evidence_state_from_receipt(now, value, max_age) - 863
}) - 864
.unwrap_or(EvidenceState::None); - 865
evaluate_requirements_with_state(spec, response, state) - 866
} - 867
- 868
/// An `Evidence` requirement is never `Met` here, by design: this function - 869
/// can see that a source reference exists and whether a retrieval receipt is - 870
/// fresh, but not whether the source *supports the claim*. That is a - 871
/// judgement, and the runtime does not make it structurally — so a turn - 872
/// held to `cited` or stronger evidence closes at best `Unknown` from this - 873
/// evaluator, with review recommended, until a linked criterion (a - 874
/// `Shell`/`FileContains` check, an external receipt, a human attestation) - 875
/// establishes it through the commitment ledger. - 876
pub fn evaluate_requirements_with_state( - 877
spec: &OutcomeSpec, - 878
response: Option<&str>, - 879
evidence_state: EvidenceState, - 880
) -> Vec<RequirementEvaluation> { - 881
let has_response = response.is_some_and(|text| !text.trim().is_empty()); - 882
let is_refusal = response.is_some_and(|text| { - 883
let normalized = text.trim().to_ascii_lowercase(); - 884
[ - 885
"i cannot", - 886
"i can't", - 887
"i can’t", - 888
"unable to", - 889
"cannot do", - 890
"can't do", - 891
"can’t do", - 892
] - 893
.iter() - 894
.any(|prefix| normalized.starts_with(prefix)) - 895
}); - 896
let has_structured_evidence = response.is_some_and(|text| { - 897
text.contains("\"semantic_type\":\"research.synthesis\"") - 898
|| text.contains("\"semantic_type\": \"research.synthesis\"") - 899
|| text.contains("\"semantic_type\":\"evidence\"") - 900
|| text.contains("\"semantic_type\": \"evidence\"") - 901
}); - 902
let has_reference = response.is_some_and(|text| { - 903
text.contains("http://") || text.contains("https://") || text.contains("[^") - 904
}) || has_structured_evidence; - 905
spec.requirements - 906
.iter() - 907
.map(|requirement| { - 908
let (status, reason) = match requirement.kind { - 909
RequirementKind::Deliverable if is_refusal => ( - 910
RequirementStatus::Unknown, - 911
"response is a refusal; the requested deliverable was not established".into(), - 912
), - 913
RequirementKind::Deliverable if has_response => ( - 914
RequirementStatus::Met, - 915
"response content exists".into(), - 916
), - 917
RequirementKind::Deliverable => ( - 918
RequirementStatus::Unmet, - 919
"no response content was produced".into(), - 920
), - 921
RequirementKind::Evidence if evidence_state == EvidenceState::Fresh => ( - 922
RequirementStatus::Unknown, - 923
"successful retrieval receipt exists; support and freshness still require evaluation".into(), - 924
), - 925
RequirementKind::Evidence if evidence_state == EvidenceState::Stale => ( - 926
RequirementStatus::Unknown, - 927
"evidence receipt exists but is stale for this request".into(), - 928
), - 929
RequirementKind::Evidence if !has_reference => ( - 930
RequirementStatus::Unmet, - 931
"no source reference was found in the response".into(), - 932
), - 933
RequirementKind::Evidence => ( - 934
RequirementStatus::Unknown, - 935
"a source reference exists, but support and freshness were not established" - 936
.into(), - 937
), - 938
RequirementKind::Constraint => ( - 939
RequirementStatus::Unknown, - 940
"constraint applicability requires a linked result".into(), - 941
), - 942
RequirementKind::Integrity if is_refusal => ( - 943
RequirementStatus::Unknown, - 944
"response is a refusal; domain integrity check bypassed".into(), - 945
), - 946
RequirementKind::Integrity if has_response => { - 947
let text = response.unwrap_or_default(); - 948
let mut checks = Vec::new(); - 949
if let Some(tab) = verify_tabular_data(text) { - 950
checks.push(tab); - 951
} - 952
if let Some(dec) = verify_decision_matrix(text) { - 953
checks.push(dec); - 954
} - 955
if let Some(cit) = verify_claim_citations(text) { - 956
checks.push(cit); - 957
} - 958
if checks.is_empty() { - 959
(RequirementStatus::Met, "no structured domain violations found".into()) - 960
} else if checks.iter().all(|c| c.is_ok()) { - 961
(RequirementStatus::Met, "all structured domain integrity checks passed".into()) - 962
} else { - 963
let violations: Vec<_> = checks.into_iter().filter_map(|c| c.err()).collect(); - 964
(RequirementStatus::Unmet, format!("domain integrity check failed: {}", violations.join("; "))) - 965
} - 966
} - 967
RequirementKind::Integrity => ( - 968
RequirementStatus::Unmet, - 969
"no response content was produced to verify domain integrity".into(), - 970
), - 971
}; - 972
RequirementEvaluation { - 973
requirement_id: requirement.id.clone(), - 974
status, - 975
reason, - 976
} - 977
}) - 978
.collect() - 979
} - 980
- 981
impl OutcomeSpec { - 982
/// A named file deliverable cannot be established by prose alone. This - 983
/// conservative signal only affects outcome assessment; it grants no tool. - 984
/// - 985
/// Only a request with a part that produces something — authoring, - 986
/// modifying, operating — can owe a file: "explain how to write a - 987
/// README.md" names a file and asks for an explanation, and demanding a - 988
/// write for it made the stop gate send a correct answer back for a - 989
/// file nobody asked for. - 990
pub fn saved_file_target(&self) -> Option<String> { - 991
if !self - 992
.acts - 993
.iter() - 994
.any(|act| matches!(act, Act::Author | Act::Modify | Act::Operate)) - 995
{ - 996
return None; - 997
} - 998
let request = self.objective.to_ascii_lowercase(); - 999
let asks_to_write = [ - 1000
"create ",
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.