- 982
.map(normalize) - 983
.filter(|domain| !domain.is_empty()) - 984
.collect(), - 985
Some(serde_json::Value::String(items)) => items - 986
.split(',') - 987
.map(normalize) - 988
.filter(|domain| !domain.is_empty()) - 989
.collect(), - 990
_ => Vec::new(), - 991
}; - 992
let confidence = match object.get("confidence") { - 993
Some(serde_json::Value::Number(number)) => number.as_f64(), - 994
Some(serde_json::Value::String(number)) => number.trim().parse::<f64>().ok(), - 995
_ => None, - 996
} - 997
.filter(|confidence| confidence.is_finite()); - 998
Some(Classification { - 999
act: text("act"), - 1000
horizon: text("horizon"), - 1001
stakes: text("stakes"), - 1002
evidence: text("evidence"), - 1003
clarity: text("clarity"), - 1004
domains, - 1005
confidence, - 1006
}) - 1007
} - 1008
- 1009
/// Parse a model tier's answer: a JSON array of objects, or a single object - 1010
/// (which then applies to every part). The first JSON value in the answer is - 1011
/// read and anything around it — a fence, a sentence of preamble, trailing - 1012
/// prose — is ignored. Field types are read leniently: a domain list may be - 1013
/// one comma-separated string, a confidence may be a quoted number. - 1014
pub fn parse_classifications(text: &str) -> Result<Vec<Classification>, String> { - 1015
let start = text - 1016
.find(['[', '{']) - 1017
.ok_or("no JSON in classifier answer")?; - 1018
let mut values = - 1019
serde_json::Deserializer::from_str(&text[start..]).into_iter::<serde_json::Value>(); - 1020
let value = match values.next() { - 1021
Some(Ok(value)) => value, - 1022
Some(Err(error)) => return Err(error.to_string()), - 1023
None => return Err("no JSON in classifier answer".into()), - 1024
}; - 1025
match value { - 1026
serde_json::Value::Array(items) => { - 1027
Ok(items.iter().filter_map(classification_from_value).collect()) - 1028
} - 1029
object @ serde_json::Value::Object(_) => { - 1030
Ok(classification_from_value(&object).into_iter().collect()) - 1031
} - 1032
_ => Err("classifier answer is neither an object nor an array".into()), - 1033
} - 1034
} - 1035
- 1036
/// Fold a model tier's answer into a partial intent. - 1037
/// - 1038
/// `classifications` line up with the partial's strands; a single object - 1039
/// applies to every strand. Only axes the model actually returned are - 1040
/// overwritten, and the result is always marked non-reproducible with the - 1041
/// model id and prompt digest that produced it. A malformed or empty answer - 1042
/// leaves the partial untouched, so a bad model response degrades to the free - 1043
/// tiers rather than to nonsense. - 1044
/// - 1045
/// A model may raise stakes or evidence freely; it may not lower either - 1046
/// below what the free tiers concluded, nor lower stakes below what the act - 1047
/// it chose implies. Authority-bearing limits are met with the partial's, and - 1048
/// the posture it derives is met with the free tier's where the free tier - 1049
/// read the part at all — the stop rule, the checkpoint and the human - 1050
/// involvement stay at least as strict — so a classifier can change what a - 1051
/// turn *reaches for* but never what it is *allowed to do*. - 1052
#[allow(clippy::too_many_arguments)] - 1053
pub fn apply_classification( - 1054
partial: Intent, - 1055
classifications: &[Classification], - 1056
model: &str, - 1057
prompt_digest: &str, - 1058
authority: &Authority, - 1059
config: &ResolverConfig, - 1060
cloud: bool, - 1061
) -> Intent { - 1062
let mut strands = partial.strands.clone(); - 1063
let mut applied = 0usize; - 1064
if classifications.len() == strands.len() { - 1065
for (strand, classification) in strands.iter_mut().zip(classifications) { - 1066
applied += apply_to_strand(strand, classification, authority, config); - 1067
} - 1068
} else if !classifications.is_empty() { - 1069
// The model split the request differently than the segmenter did. - 1070
// The answer is still evidence; fold it into one classification on - 1071
// the cautious side and apply it to every strand. - 1072
let folded = fold_classifications(classifications); - 1073
for strand in strands.iter_mut() { - 1074
applied += apply_to_strand(strand, &folded, authority, config); - 1075
} - 1076
} - 1077
- 1078
if applied == 0 { - 1079
let mut provenance = partial.provenance; - 1080
provenance.escalation_note = Some(format!( - 1081
"{model} returned no usable axes; free-tier reading retained" - 1082
)); - 1083
return Intent { - 1084
reading: partial.reading, - 1085
strands: partial.strands, - 1086
engagement: partial.engagement, - 1087
provenance, - 1088
}; - 1089
} - 1090
- 1091
let reading = composite_reading(&strands); - 1092
let engagement = recompose(&strands); - 1093
- 1094
let mut provenance = partial.provenance; - 1095
provenance.tier = if cloud { - 1096
Tier::CloudModel - 1097
} else { - 1098
Tier::LocalModel - 1099
}; - 1100
provenance.reproducible = false; - 1101
provenance.model = Some(model.to_string()); - 1102
provenance.prompt_digest = Some(prompt_digest.to_string()); - 1103
provenance.escalation_note = Some(format!("{applied} axis/axes set by {model}")); - 1104
- 1105
Intent { - 1106
reading, - 1107
strands, - 1108
engagement, - 1109
provenance, - 1110
} - 1111
} - 1112
- 1113
/// Several classifications folded into one, on the cautious side. - 1114
fn fold_classifications(classifications: &[Classification]) -> Classification { - 1115
fn highest<T: Copy>( - 1116
values: impl Iterator<Item = Option<T>>, - 1117
rank: impl Fn(T) -> u8, - 1118
name: impl Fn(T) -> &'static str, - 1119
) -> Option<String> { - 1120
values - 1121
.flatten() - 1122
.max_by_key(|value| rank(*value)) - 1123
.map(|value| name(value).to_string()) - 1124
} - 1125
let act = classifications - 1126
.iter() - 1127
.filter_map(|c| c.act.as_deref().and_then(Act::parse)) - 1128
// The most consequential act the model named. - 1129
.max_by_key(|act| (act.is_effectful(), act.requires_execution())) - 1130
.map(|act| act.as_str().to_string()); - 1131
let horizon = highest( - 1132
classifications - 1133
.iter() - 1134
.map(|c| c.horizon.as_deref().and_then(Horizon::parse)), - 1135
Horizon::rank, - 1136
Horizon::as_str, - 1137
); - 1138
let stakes = highest( - 1139
classifications - 1140
.iter() - 1141
.map(|c| c.stakes.as_deref().and_then(Stakes::parse)), - 1142
Stakes::rank, - 1143
Stakes::as_str, - 1144
); - 1145
let evidence = highest( - 1146
classifications - 1147
.iter() - 1148
.map(|c| c.evidence.as_deref().and_then(Evidence::parse)), - 1149
Evidence::rank, - 1150
Evidence::as_str, - 1151
); - 1152
let clarity = highest( - 1153
classifications - 1154
.iter() - 1155
.map(|c| c.clarity.as_deref().and_then(Clarity::parse)), - 1156
Clarity::rank, - 1157
Clarity::as_str, - 1158
); - 1159
let mut domains: Vec<String> = classifications - 1160
.iter() - 1161
.flat_map(|c| c.domains.iter().cloned()) - 1162
.collect(); - 1163
domains.sort(); - 1164
domains.dedup(); - 1165
let confidence = classifications - 1166
.iter() - 1167
.filter_map(|c| c.confidence) - 1168
.fold(None, |acc: Option<f64>, c| { - 1169
Some(acc.map_or(c, |a| a.min(c))) - 1170
}); - 1171
Classification { - 1172
act, - 1173
horizon, - 1174
stakes, - 1175
evidence, - 1176
clarity, - 1177
domains, - 1178
confidence, - 1179
} - 1180
} - 1181
- 1182
/// Apply one classification to one strand. Returns how many axes it set. - 1183
fn apply_to_strand( - 1184
strand: &mut Strand, - 1185
classification: &Classification, - 1186
authority: &Authority, - 1187
config: &ResolverConfig, - 1188
) -> usize { - 1189
let before = strand.reading.clone(); - 1190
// A part the free tiers could not read at all carries the general - 1191
// reading and the orienting posture; that is not a judgement to be - 1192
// cautious about, so the classifier's posture replaces it. - 1193
let free_tier_read_it = before.confidence >= config.provisional_confidence; - 1194
let mut reading = strand.reading.clone(); - 1195
let mut applied = 0usize; - 1196
- 1197
if let Some(act) = classification.act.as_deref().and_then(Act::parse) { - 1198
if act != reading.act { - 1199
// The free tier's act stays in the slice as an alternate: the - 1200
// model may refine what the part *is*, and the earlier reading - 1201
// was evidence too. - 1202
if free_tier_read_it { - 1203
reading.alternate_acts.insert(reading.act); - 1204
} - 1205
reading.alternate_acts.remove(&act); - 1206
} - 1207
reading.act = act; - 1208
applied += 1; - 1209
} - 1210
if let Some(horizon) = classification.horizon.as_deref().and_then(Horizon::parse) { - 1211
reading.horizon = horizon; - 1212
applied += 1; - 1213
} - 1214
if let Some(stakes) = classification.stakes.as_deref().and_then(Stakes::parse) { - 1215
reading.stakes = stakes; - 1216
applied += 1; - 1217
} - 1218
if let Some(evidence) = classification.evidence.as_deref().and_then(Evidence::parse) { - 1219
reading.evidence = evidence; - 1220
applied += 1; - 1221
} - 1222
if let Some(clarity) = classification.clarity.as_deref().and_then(Clarity::parse) { - 1223
reading.clarity = clarity; - 1224
applied += 1; - 1225
} - 1226
// Floors: the act the model chose implies stakes of its own, and the - 1227
// free tier's stakes and evidence are never lowered. - 1228
let act_floor = implied_stakes(reading.act); - 1229
if reading.stakes.rank() < act_floor.rank() { - 1230
reading.stakes = act_floor; - 1231
} - 1232
if reading.stakes.rank() < before.stakes.rank() { - 1233
reading.stakes = before.stakes; - 1234
} - 1235
if reading.evidence.rank() < before.evidence.rank() { - 1236
reading.evidence = before.evidence; - 1237
} - 1238
// Only names capabilities can declare: a free-form subject tag - 1239
// ("weather") matches no capability and would key behaviour on a topic. - 1240
let added = classification - 1241
.domains - 1242
.iter() - 1243
.filter(|domain| crate::engage::DOMAIN_VOCABULARY.contains(&domain.as_str())) - 1244
.filter(|domain| reading.domains.insert((*domain).clone())) - 1245
.count(); - 1246
if added > 0 { - 1247
applied += 1; - 1248
} - 1249
- 1250
if applied == 0 { - 1251
return 0; - 1252
} - 1253
- 1254
// A classifier's confidence covers every axis it actually answered; axes - 1255
// it left alone keep whatever the free tiers concluded. An answer with - 1256
// no confidence at all is provisional — enough to raise a floor, not - 1257
// enough to remove a tool. - 1258
let stated = classification - 1259
.confidence - 1260
.unwrap_or(config.provisional_confidence) - 1261
.clamp(0.0, 1.0); - 1262
if classification.act.is_some() { - 1263
reading.axis_confidence.act = stated; - 1264
} - 1265
if classification.horizon.is_some() { - 1266
reading.axis_confidence.horizon = stated; - 1267
} - 1268
if classification.stakes.is_some() { - 1269
reading.axis_confidence.stakes = stated; - 1270
} - 1271
if classification.evidence.is_some() { - 1272
reading.axis_confidence.evidence = stated; - 1273
} - 1274
reading.confidence = reading.axis_confidence.overall(); - 1275
- 1276
let may_slice = - 1277
config.slice_capabilities && reading.may_slice_capabilities(config.accept_confidence); - 1278
let fresh = derive(&reading, authority, may_slice); - 1279
// What the part reaches for is the model's to refine; what it is allowed - 1280
// to do is not. - 1281
let previous = &strand.engagement; - 1282
let mut limits = fresh.limits.clone(); - 1283
limits.approval_ceiling = limits - 1284
.approval_ceiling - 1285
.meet(previous.limits.approval_ceiling); - 1286
limits.permission_ceiling = limits - 1287
.permission_ceiling - 1288
.meet(previous.limits.permission_ceiling); - 1289
if previous.limits.min_satisfaction.rank() > limits.min_satisfaction.rank() { - 1290
limits.min_satisfaction = previous.limits.min_satisfaction; - 1291
} - 1292
limits.required_modalities = limits - 1293
.required_modalities - 1294
.union(&previous.limits.required_modalities) - 1295
.copied() - 1296
.collect(); - 1297
limits.spend_ceiling_usd = match (limits.spend_ceiling_usd, previous.limits.spend_ceiling_usd) { - 1298
(Some(a), Some(b)) => Some(a.min(b)), - 1299
(a, b) => a.or(b), - 1300
}; - 1301
let posture = if free_tier_read_it { - 1302
let mut posture = fresh.posture.meet(&previous.posture); - 1303
posture.note = fresh.posture.note.clone(); - 1304
posture - 1305
} else { - 1306
fresh.posture - 1307
}; - 1308
strand.reading = reading; - 1309
strand.engagement = Engagement { limits, posture }; - 1310
applied - 1311
} - 1312
- 1313
/// Digest of a classification prompt, so a later change to it is visible in - 1314
/// old ledger entries rather than silent. - 1315
pub fn prompt_digest(prompt: &str) -> String { - 1316
use sha2::{Digest, Sha256}; - 1317
format!("{:x}", Sha256::digest(prompt.as_bytes())) - 1318
} - 1319
- 1320
#[cfg(test)] - 1321
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 1322
mod tests { - 1323
use super::*; - 1324
- 1325
fn now() -> chrono::DateTime<chrono::Utc> { - 1326
chrono::DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z") - 1327
.unwrap() - 1328
.with_timezone(&chrono::Utc) - 1329
} - 1330
- 1331
fn request<'a>(text: &'a str) -> Request<'a> { - 1332
Request { - 1333
text, - 1334
..Request::default() - 1335
} - 1336
} - 1337
- 1338
fn resolve_text(text: &str) -> Resolution { - 1339
resolve( - 1340
&request(text), - 1341
&Declared::default(), - 1342
&Authority::default(), - 1343
&ResolverConfig::default(), - 1344
) - 1345
} - 1346
- 1347
/// `RESOLVER_VERSION` and the lexicon move together. When this fails: - 1348
/// bump `RESOLVER_VERSION`, add a line to its history, and replace the - 1349
/// pinned digest with the one in the message. - 1350
#[test] - 1351
fn lexicon_digest_matches_resolver_version() { - 1352
const PINNED: (u32, &str) = ( - 1353
5, - 1354
"b4a8e4771afdfa16f21afc993fbfe8864723a4a5a714c8f438faa01d583748f9", - 1355
); - 1356
let digest = crate::signals::lexicon_digest(); - 1357
assert_eq!( - 1358
(RESOLVER_VERSION, digest.as_str()), - 1359
PINNED, - 1360
"the tier-1 lexicon changed: bump RESOLVER_VERSION and pin digest {digest}" - 1361
); - 1362
} - 1363
- 1364
#[test] - 1365
fn resolution_is_deterministic() { - 1366
let a = resolve_text("refactor the parser"); - 1367
let b = resolve_text("refactor the parser"); - 1368
assert_eq!(a, b); - 1369
} - 1370
- 1371
#[test] - 1372
fn disabling_the_kernel_reproduces_the_general_engagement() { - 1373
let config = ResolverConfig { - 1374
enabled: false, - 1375
..ResolverConfig::default() - 1376
}; - 1377
let resolution = resolve( - 1378
&request("deploy everything to production right now"), - 1379
&Declared::default(), - 1380
&Authority::default(), - 1381
&config, - 1382
); - 1383
let intent = resolution.intent(); - 1384
assert_eq!(intent.engagement, Engagement::general()); - 1385
assert!(intent.engagement.limits.required_domains.is_unconstrained()); - 1386
assert_eq!(intent.provenance.tier, Tier::General); - 1387
} - 1388
- 1389
#[test] - 1390
fn a_fully_declared_reading_is_tier_zero_and_reproducible() { - 1391
let declared = Declared { - 1392
act: Some(Act::Modify), - 1393
horizon: Some(Horizon::Session), - 1394
stakes: Some(Stakes::Reversible), - 1395
evidence: Some(Evidence::Verified), - 1396
..Declared::default() - 1397
}; - 1398
let resolution = resolve( - 1399
&request("whatever"), - 1400
&declared, - 1401
&Authority::default(), - 1402
&ResolverConfig::default(), - 1403
); - 1404
let intent = resolution.intent(); - 1405
assert_eq!(intent.provenance.tier, Tier::Declared); - 1406
assert!(intent.provenance.reproducible); - 1407
assert_eq!(intent.reading.act, Act::Modify); - 1408
assert_eq!(intent.reading.evidence, Evidence::Verified); - 1409
} - 1410
- 1411
#[test] - 1412
fn confidence_tracks_the_weakest_axis() { - 1413
let resolution = resolve_text("refactor the parser"); - 1414
let intent = resolution.peek(); - 1415
assert!(intent.reading.confidence <= 1.0); - 1416
let declared = Declared { - 1417
act: Some(Act::Modify), - 1418
horizon: Some(Horizon::Turn), - 1419
stakes: Some(Stakes::Reversible), - 1420
evidence: Some(Evidence::None), - 1421
..Declared::default() - 1422
}; - 1423
let full = resolve( - 1424
&request("refactor the parser"), - 1425
&declared, - 1426
&Authority::default(), - 1427
&ResolverConfig::default(), - 1428
); - 1429
assert!(full.peek().reading.confidence > intent.reading.confidence); - 1430
} - 1431
- 1432
/// Nothing recognisable: the orienting engagement (floor domains, general - 1433
/// posture) and a recommendation to escalate. - 1434
#[test] - 1435
fn unknown_input_falls_back_to_orienting_and_recommends_escalation() { - 1436
let resolution = resolve_text("zorble the frobnicator immediately"); - 1437
match resolution { - 1438
Resolution::Escalate { partial, reason } => { - 1439
assert_eq!(partial.provenance.tier, Tier::General); - 1440
assert_eq!( - 1441
partial.engagement.limits.required_domains, - 1442
Engagement::orienting().limits.required_domains - 1443
); - 1444
assert!(!reason.is_empty()); - 1445
} - 1446
Resolution::Settled(intent) => { - 1447
assert!(intent.reading.confidence >= 0.45); - 1448
} - 1449
} - 1450
} - 1451
- 1452
/// What a human delegated applies whether or not the request was read: - 1453
/// `manual` asks at every level, even for a part nobody understood. - 1454
#[test] - 1455
fn authority_governs_a_part_the_free_tiers_could_not_read() { - 1456
let manual = Authority { - 1457
autonomy: Autonomy::Manual, - 1458
..Authority::default() - 1459
}; - 1460
let intent = resolve( - 1461
&request("zorble the frobnicator"), - 1462
&Declared::default(), - 1463
&manual, - 1464
&ResolverConfig::default(), - 1465
) - 1466
.intent(); - 1467
assert_eq!(intent.provenance.tier, Tier::General); - 1468
assert_eq!( - 1469
intent.engagement.limits.approval_ceiling, - 1470
crate::ApprovalCeiling::Ask - 1471
); - 1472
} - 1473
- 1474
/// The single most important safety property of the paid tier: a model - 1475
/// cannot talk the runtime out of caution it already arrived at — not by - 1476
/// lowering stakes, not by changing the act, not by lowering evidence. - 1477
#[test] - 1478
fn a_classifier_can_never_lower_deterministic_caution() { - 1479
let partial = resolve_text("deploy the service").intent(); - 1480
assert_eq!(partial.reading.stakes, Stakes::Irreversible); - 1481
let downplayed = Classification { - 1482
act: Some("answer".into()), - 1483
stakes: Some("inert".into()), - 1484
confidence: Some(0.99), - 1485
..Classification::default() - 1486
}; - 1487
let intent = apply_classification( - 1488
partial, - 1489
std::slice::from_ref(&downplayed), - 1490
"cheap-model", - 1491
"digest", - 1492
&Authority::default(), - 1493
&ResolverConfig::default(), - 1494
true, - 1495
); - 1496
assert_eq!(intent.reading.stakes, Stakes::Irreversible); - 1497
assert_eq!( - 1498
intent.engagement.limits.approval_ceiling, - 1499
crate::ApprovalCeiling::Ask - 1500
); - 1501
// Nor by changing the act: the stop rule stays the effect it was. - 1502
assert_eq!(intent.engagement.posture.stop, crate::StopProfile::Effect); - 1503
- 1504
let partial = resolve_text("make sure the tests pass and prove it").intent(); - 1505
assert_eq!(partial.reading.evidence, Evidence::Verified); - 1506
let relaxed = Classification { - 1507
evidence: Some("none".into()), - 1508
confidence: Some(0.99), - 1509
..Classification::default() - 1510
}; - 1511
let intent = apply_classification( - 1512
partial, - 1513
std::slice::from_ref(&relaxed), - 1514
"cheap-model", - 1515
"digest", - 1516
&Authority::default(), - 1517
&ResolverConfig::default(), - 1518
true, - 1519
); - 1520
assert_eq!(intent.reading.evidence, Evidence::Verified); - 1521
assert_eq!( - 1522
intent.engagement.limits.min_satisfaction, - 1523
crate::Satisfaction::Observed - 1524
); - 1525
} - 1526
- 1527
#[test] - 1528
fn a_model_tier_is_recorded_as_non_reproducible_with_its_digest() { - 1529
let partial = resolve_text("something unclear").intent(); - 1530
let intent = apply_classification( - 1531
partial, - 1532
&[Classification { - 1533
act: Some("analyze".into()), - 1534
confidence: Some(0.9), - 1535
..Classification::default() - 1536
}], - 1537
"local-model", - 1538
"abc123", - 1539
&Authority::default(), - 1540
&ResolverConfig::default(), - 1541
false, - 1542
); - 1543
assert_eq!(intent.provenance.tier, Tier::LocalModel); - 1544
assert!(!intent.provenance.reproducible); - 1545
assert_eq!(intent.provenance.model.as_deref(), Some("local-model")); - 1546
assert_eq!(intent.provenance.prompt_digest.as_deref(), Some("abc123")); - 1547
} - 1548
- 1549
#[test] - 1550
fn an_empty_classification_leaves_the_free_tier_result_intact() { - 1551
let partial = resolve_text("refactor the parser").intent(); - 1552
let before = partial.clone(); - 1553
let after = apply_classification( - 1554
partial, - 1555
&[Classification::default()], - 1556
"flaky-model", - 1557
"digest", - 1558
&Authority::default(), - 1559
&ResolverConfig::default(), - 1560
true, - 1561
); - 1562
assert_eq!(after.reading, before.reading); - 1563
assert_eq!(after.engagement, before.engagement); - 1564
} - 1565
- 1566
/// A classifier that states no confidence is provisional: it may raise a - 1567
/// floor, it may not remove a tool. - 1568
#[test] - 1569
fn a_confidence_less_classification_does_not_slice() { - 1570
let partial = resolve_text("zorble the frobnicator").intent(); - 1571
let after = apply_classification( - 1572
partial, - 1573
&[Classification { - 1574
act: Some("modify".into()), - 1575
..Classification::default() - 1576
}], - 1577
"m", - 1578
"d", - 1579
&Authority::default(), - 1580
&ResolverConfig::default(), - 1581
false, - 1582
); - 1583
assert_eq!( - 1584
after.engagement.limits.required_domains, - 1585
Engagement::orienting().limits.required_domains - 1586
); - 1587
} - 1588
- 1589
/// A domain the classifier names counts as an answer on its own, and - 1590
/// only names from the shared vocabulary are taken. - 1591
#[test] - 1592
fn classifier_domains_count_and_are_checked_against_the_vocabulary() { - 1593
let partial = resolve_text("zorble the frobnicator").intent(); - 1594
let after = apply_classification( - 1595
partial, - 1596
&[Classification { - 1597
domains: vec!["web".into(), "weather".into()], - 1598
confidence: Some(0.9), - 1599
..Classification::default() - 1600
}], - 1601
"m", - 1602
"d", - 1603
&Authority::default(), - 1604
&ResolverConfig::default(), - 1605
false, - 1606
); - 1607
assert_eq!(after.provenance.tier, Tier::LocalModel); - 1608
assert!(after.reading.domains.contains("web")); - 1609
assert!(!after.reading.domains.contains("weather")); - 1610
} - 1611
- 1612
#[test] - 1613
fn operate_is_never_assumed_cheap_and_governing_is_reversible() { - 1614
assert_eq!(implied_stakes(Act::Operate), Stakes::Irreversible); - 1615
assert_eq!(implied_stakes(Act::Govern), Stakes::Reversible); - 1616
assert_eq!(implied_stakes(Act::Answer), Stakes::Inert); - 1617
} - 1618
- 1619
/// Below the acceptance bar the turn gets the orientation floor, never a - 1620
/// guessed slice and never everything. - 1621
#[test] - 1622
fn provisional_readings_get_the_orientation_floor() { - 1623
let config = ResolverConfig { - 1624
accept_confidence: 0.99, - 1625
provisional_confidence: 0.0, - 1626
..ResolverConfig::default() - 1627
}; - 1628
let resolution = resolve( - 1629
&request("explain the deploy script"), - 1630
&Declared::default(), - 1631
&Authority::default(), - 1632
&config, - 1633
); - 1634
assert_eq!( - 1635
resolution.peek().engagement.limits.required_domains, - 1636
Engagement::orienting().limits.required_domains - 1637
); - 1638
} - 1639
- 1640
#[test] - 1641
fn a_confident_reading_does_slice() { - 1642
let resolution = resolve_text("hello"); - 1643
assert!( - 1644
!resolution - 1645
.peek() - 1646
.engagement - 1647
.limits - 1648
.required_domains - 1649
.is_empty() - 1650
); - 1651
} - 1652
- 1653
// ----------------------------------------------------------- strands --- - 1654
- 1655
#[test] - 1656
fn a_compound_request_becomes_ordered_strands_with_the_union_of_domains() { - 1657
let intent = resolve_text("first explain the parser, then refactor it").intent(); - 1658
assert_eq!(intent.strands.len(), 2); - 1659
assert_eq!(intent.strands[0].reading.act, Act::Answer); - 1660
assert_eq!(intent.strands[1].reading.act, Act::Modify); - 1661
assert_eq!( - 1662
intent.strands[1].relation, - 1663
StrandRelation::Sequential { - 1664
after: intent.strands[0].strand_id.clone() - 1665
} - 1666
); - 1667
// Composite: the consequential strand, widened by the other. - 1668
assert_eq!(intent.reading.act, Act::Modify); - 1669
assert!(intent.reading.alternate_acts.contains(&Act::Answer)); - 1670
// The slice covers both parts. - 1671
let domains = &intent.engagement.limits.required_domains; - 1672
assert!(domains.contains("live-data"), "answer's domains lost"); - 1673
assert!(domains.contains("code-exec"), "modify's domains lost"); - 1674
// And the model is told there are two parts, without the request - 1675
// being quoted back at it. - 1676
let note = intent.engagement.posture.note.as_deref().unwrap(); - 1677
assert!(note.contains("2 parts"), "{note}"); - 1678
assert!(note.contains("after part 1"), "{note}"); - 1679
for strand in &intent.strands { - 1680
assert!(!note.contains(strand.text.trim()), "quoted: {note}"); - 1681
} - 1682
} - 1683
- 1684
/// Strand ids come from the host's turn id, so a thread — and the - 1685
/// commitment keyed by it — never collides with another turn's. - 1686
#[test] - 1687
fn strand_ids_come_from_the_turn_id() { - 1688
let mut req = request("fix the login bug. Also check whether the nightly job ran"); - 1689
req.turn_id = "0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b"; - 1690
let intent = resolve( - 1691
&req, - 1692
&Declared::default(), - 1693
&Authority::default(), - 1694
&ResolverConfig::default(), - 1695
) - 1696
.intent(); - 1697
assert_eq!( - 1698
intent.strands[0].strand_id, - 1699
"0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b.0" - 1700
); - 1701
assert_eq!( - 1702
intent.strands[1].thread_id, - 1703
"0199a1b2-7c3d-7e4f-8a9b-0c1d2e3f4a5b.1" - 1704
); - 1705
} - 1706
- 1707
#[test] - 1708
fn conversational_card_delivery_is_an_answer_not_workspace_authoring() { - 1709
for text in [ - 1710
"Give me the latest India news and present it as a card", - 1711
"What is the current weather in Delhi? Show it as a card", - 1712
] { - 1713
let intent = resolve_text(text).intent(); - 1714
assert_eq!(intent.reading.act, Act::Answer, "{text}"); - 1715
assert!( - 1716
intent - 1717
.strands - 1718
.iter() - 1719
.all(|strand| strand.reading.act != Act::Author), - 1720
"{text}: {:?}", - 1721
intent.strands - 1722
); - 1723
assert!(intent.reading.domains.contains("live-data"), "{text}"); - 1724
} - 1725
} - 1726
- 1727
#[test] - 1728
fn unrelated_strands_are_independent_threads() { - 1729
let intent = - 1730
resolve_text("fix the login bug. Also check whether the nightly job ran").intent(); - 1731
assert_eq!(intent.strands.len(), 2); - 1732
assert_eq!(intent.strands[1].relation, StrandRelation::Independent); - 1733
assert_ne!(intent.strands[0].thread_id, intent.strands[1].thread_id); - 1734
assert!(matches!(intent.strands[0].lineage, Lineage::New)); - 1735
} - 1736
- 1737
#[test] - 1738
fn a_strand_continues_an_open_thread_it_points_at() { - 1739
let mut req = request("now refactor it"); - 1740
req.history.turn_index = 2; - 1741
req.history.open_threads = vec![ThreadFact { - 1742
thread_id: "s0.0".into(), - 1743
act: Act::Modify, - 1744
domains: BTreeSet::new(), - 1745
keywords: crate::strand::keywords("fix the parser"), - 1746
}]; - 1747
let intent = resolve( - 1748
&req, - 1749
&Declared::default(), - 1750
&Authority::default(), - 1751
&ResolverConfig::default(), - 1752
) - 1753
.intent(); - 1754
assert_eq!( - 1755
intent.strands[0].lineage, - 1756
Lineage::Continues { - 1757
thread_id: "s0.0".into() - 1758
} - 1759
); - 1760
assert_eq!(intent.strands[0].thread_id, "s0.0"); - 1761
} - 1762
- 1763
/// Corrections and replacements are never inferred from text, and a - 1764
/// replacement starts a thread of its own. - 1765
#[test] - 1766
fn corrections_only_come_from_an_explicit_hint() { - 1767
let thread = ThreadFact { - 1768
thread_id: "s0.0".into(), - 1769
act: Act::Modify, - 1770
domains: BTreeSet::new(), - 1771
keywords: crate::strand::keywords("refactor the parser"), - 1772
}; - 1773
let mut req = request("actually refactor the parser to use a state machine instead"); - 1774
req.history.turn_index = 1; - 1775
req.history.open_threads = vec![thread.clone()]; - 1776
let inferred = resolve( - 1777
&req, - 1778
&Declared::default(), - 1779
&Authority::default(), - 1780
&ResolverConfig::default(), - 1781
) - 1782
.intent(); - 1783
assert!(matches!( - 1784
inferred.strands[0].lineage, - 1785
Lineage::Continues { .. } - 1786
)); - 1787
- 1788
req.lineage_hint = Some(LineageHint::Replaces); - 1789
let explicit = resolve( - 1790
&req, - 1791
&Declared::default(), - 1792
&Authority::default(), - 1793
&ResolverConfig::default(), - 1794
) - 1795
.intent(); - 1796
assert_eq!( - 1797
explicit.strands[0].lineage, - 1798
Lineage::Replaces { - 1799
thread_id: "s0.0".into() - 1800
} - 1801
); - 1802
assert_eq!(explicit.strands[0].thread_id, explicit.strands[0].strand_id); - 1803
} - 1804
- 1805
/// A greeting beside real work is context for it, not a part of its own. - 1806
#[test] - 1807
fn a_greeting_beside_real_work_is_not_a_part() { - 1808
let intent = resolve_text("hi! then refactor the parser").intent(); - 1809
assert_eq!(intent.strands.len(), 1, "{:?}", intent.strands); - 1810
assert_eq!(intent.reading.act, Act::Modify); - 1811
assert!(intent.engagement.posture.note.is_none()); - 1812
} - 1813
- 1814
#[test] - 1815
fn the_strictest_strand_governs_authority() { - 1816
let intent = - 1817
resolve_text("summarise the changelog, then deploy the service to production").intent(); - 1818
assert_eq!( - 1819
intent.engagement.limits.approval_ceiling, - 1820
crate::ApprovalCeiling::Ask - 1821
); - 1822
assert_eq!(intent.reading.stakes, Stakes::Irreversible); - 1823
assert_eq!(intent.engagement.posture.stop, crate::StopProfile::Effect); - 1824
} - 1825
- 1826
#[test] - 1827
fn per_strand_classifications_apply_in_order() { - 1828
let partial = resolve_text("first explain the parser, then refactor it").intent(); - 1829
let answer = apply_classification( - 1830
partial, - 1831
&[ - 1832
Classification { - 1833
evidence: Some("cited".into()), - 1834
confidence: Some(0.9), - 1835
..Classification::default() - 1836
}, - 1837
Classification { - 1838
horizon: Some("session".into()), - 1839
confidence: Some(0.9), - 1840
..Classification::default() - 1841
}, - 1842
], - 1843
"m", - 1844
"d", - 1845
&Authority::default(), - 1846
&ResolverConfig::default(), - 1847
false, - 1848
); - 1849
assert_eq!(answer.strands[0].reading.evidence, Evidence::Cited); - 1850
assert_eq!(answer.strands[1].reading.horizon, Horizon::Session); - 1851
assert_ne!(answer.strands[0].reading.horizon, Horizon::Session); - 1852
} - 1853
- 1854
/// A classifier that splits the request differently than the segmenter - 1855
/// is still evidence: its objects fold on the cautious side and apply - 1856
/// to every strand, never discarded. - 1857
#[test] - 1858
fn a_mismatched_classification_count_folds_cautiously() { - 1859
let partial = resolve_text("zorble the frobnicator, then flimflam the widget").intent(); - 1860
assert_eq!(partial.strands.len(), 1); - 1861
let answer = apply_classification( - 1862
partial, - 1863
&[ - 1864
Classification { - 1865
act: Some("answer".into()), - 1866
stakes: Some("inert".into()), - 1867
confidence: Some(0.9), - 1868
..Classification::default() - 1869
}, - 1870
Classification { - 1871
act: Some("modify".into()), - 1872
stakes: Some("costly".into()), - 1873
evidence: Some("verified".into()), - 1874
confidence: Some(0.6), - 1875
..Classification::default() - 1876
}, - 1877
], - 1878
"m", - 1879
"d", - 1880
&Authority::default(), - 1881
&ResolverConfig::default(), - 1882
false, - 1883
); - 1884
assert_eq!(answer.provenance.tier, Tier::LocalModel); - 1885
assert_eq!(answer.reading.act, Act::Modify); - 1886
assert_eq!(answer.reading.stakes, Stakes::Costly); - 1887
assert_eq!(answer.reading.evidence, Evidence::Verified); - 1888
assert!((answer.reading.axis_confidence.act - 0.6).abs() < 1e-9); - 1889
} - 1890
- 1891
#[test] - 1892
fn classifier_answers_parse_leniently() { - 1893
let parsed = parse_classifications("```json\n[{\"act\":\"modify\"}]\n```").unwrap(); - 1894
assert_eq!(parsed.len(), 1); - 1895
assert_eq!(parsed[0].act.as_deref(), Some("modify")); - 1896
let parsed = - 1897
parse_classifications("Sure: {\"act\":\"Answer\",\"confidence\":0.5} hope that helps") - 1898
.unwrap(); - 1899
assert_eq!(parsed[0].act.as_deref(), Some("answer")); - 1900
assert_eq!(parsed[0].confidence, Some(0.5)); - 1901
let parsed = - 1902
parse_classifications("[{\"domains\":\"web, live-data\",\"confidence\":\"0.8\"}]") - 1903
.unwrap(); - 1904
assert_eq!(parsed[0].domains, vec!["web", "live-data"]); - 1905
assert_eq!(parsed[0].confidence, Some(0.8)); - 1906
assert!(parse_classifications("no json here").is_err()); - 1907
} - 1908
- 1909
#[test] - 1910
fn the_classifier_prompt_bounds_each_part() { - 1911
let long = format!("explain {}", "the parser in great detail ".repeat(40)); - 1912
let intent = resolve_text(&long).intent(); - 1913
let prompt = classification_prompt(&intent); - 1914
let part = prompt.lines().find(|line| line.starts_with("1. ")).unwrap(); - 1915
assert!(part.chars().count() <= PROMPT_PART_CHARS + 4, "{part}"); - 1916
assert!(classification_budget(1) >= 200); - 1917
assert!(classification_budget(50) <= 1_600); - 1918
} - 1919
- 1920
/// Regression corpus for the lexical false positives two reviews found. - 1921
#[test] - 1922
fn everyday_requests_do_not_grow_horizons_stakes_or_evidence() { - 1923
let cases: &[(&str, Horizon, Stakes, Evidence)] = &[ - 1924
( - 1925
"fix the authentication bug in the login handler", - 1926
Horizon::Turn, - 1927
Stakes::Reversible, - 1928
Evidence::None, - 1929
), - 1930
( - 1931
"wait until the build finishes and tell me", - 1932
Horizon::Turn, - 1933
Stakes::Inert, - 1934
Evidence::None, - 1935
), - 1936
( - 1937
"where does this config live", - 1938
Horizon::Turn, - 1939
Stakes::Inert, - 1940
Evidence::None, - 1941
), - 1942
( - 1943
"explain the customer model in this codebase", - 1944
Horizon::Turn, - 1945
Stakes::Inert, - 1946
Evidence::None, - 1947
), - 1948
( - 1949
"how does our production deploy work?", - 1950
Horizon::Turn, - 1951
Stakes::Inert, - 1952
Evidence::None, - 1953
), - 1954
( - 1955
"look at the source code for the parser", - 1956
Horizon::Turn, - 1957
Stakes::Inert, - 1958
Evidence::None, - 1959
), - 1960
( - 1961
"rename the Customer struct to Client", - 1962
Horizon::Turn, - 1963
Stakes::Reversible, - 1964
Evidence::None, - 1965
), - 1966
( - 1967
"fix the payment form validation", - 1968
Horizon::Turn, - 1969
Stakes::Reversible, - 1970
Evidence::None, - 1971
), - 1972
( - 1973
"remember that I prefer tabs over spaces", - 1974
Horizon::Turn, - 1975
Stakes::Reversible, - 1976
Evidence::None, - 1977
), - 1978
( - 1979
"what happens whenever I press ctrl-c in the REPL?", - 1980
Horizon::Turn, - 1981
Stakes::Inert,
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.