- 1
//! Deterministic signal extraction — resolver tier 1. - 2
//! - 3
//! Everything here is a pure function of [`Request`]. That is not an - 4
//! aesthetic preference: a decision that narrowed a turn's capabilities has to - 5
//! be reconstructable from the ledger months later, and the only way to - 6
//! promise that is to make the decision depend on nothing but recorded inputs. - 7
//! Tiers 2 and 3 may call a model and are explicitly marked non-reproducible; - 8
//! this tier is not. - 9
//! - 10
//! Signals *vote*, they do not decide. Each observation contributes weight to - 11
//! one or more axis values, and [`Votes::winner`] turns the tally into a - 12
//! reading. A request that produces no signals lands at low confidence and - 13
//! falls back to the orienting engagement, which is exactly the desired - 14
//! behaviour for input this lexicon has never seen. - 15
//! - 16
//! # What may vote - 17
//! - 18
//! Words vote only from the part of the message that is a request. Two - 19
//! things are set aside first: - 20
//! - 21
//! * **Pasted material** — fenced code, and runs of lines that are not prose - 22
//! (log lines, CSV rows, stack frames, indented code). A log line that says - 23
//! "worker deploy job 12 for customer account" is data, not an instruction - 24
//! to deploy anything; before this rule a 300-line log produced 301 - 25
//! "parts", an irreversible reading and a 26 KB note. - 26
//! * **Statements** — a clause that starts with its subject ("the deploy - 27
//! failed", "it crashes on empty input") describes the situation; its - 28
//! nouns are not verbs. Such a clause keeps its stakes, evidence and - 29
//! recency words as context, but casts no act vote. - 30
//! - 31
//! What remains is read by clause role: an imperative ("fix the parser"), a - 32
//! question ("did the email send?"), a request phrased as a wish ("I want - 33
//! the parser refactored"), or social talk ("thanks!"). - 34
- 35
use std::sync::LazyLock; - 36
- 37
use serde::{Deserialize, Serialize}; - 38
- 39
use crate::axes::{Act, Attendance, Clarity, Evidence, Horizon, Modality, Stakes}; - 40
- 41
/// Where an observation came from. Recorded so an explain view can group by - 42
/// cause, and so a bad lexicon entry is traceable to its category. - 43
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] - 44
#[serde(rename_all = "kebab-case")] - 45
pub enum SignalKind { - 46
/// A word or phrase in the request. - 47
Lexical, - 48
/// Shape of the text: code fences, paths, URLs, pasted material. - 49
Structural, - 50
/// Reference to something not in the request ("this file", "that error"). - 51
Deictic, - 52
/// Facts about the workspace the request landed in. - 53
Workspace, - 54
/// Facts about the conversation so far. - 55
Session, - 56
/// Which surface the request arrived on. - 57
Surface, - 58
/// Attached media or documents. - 59
Attachment, - 60
/// Stated outright by a caller; tier 0. - 61
Declared, - 62
} - 63
- 64
impl SignalKind { - 65
pub fn as_str(self) -> &'static str { - 66
match self { - 67
SignalKind::Lexical => "lexical", - 68
SignalKind::Structural => "structural", - 69
SignalKind::Deictic => "deictic", - 70
SignalKind::Workspace => "workspace", - 71
SignalKind::Session => "session", - 72
SignalKind::Surface => "surface", - 73
SignalKind::Attachment => "attachment", - 74
SignalKind::Declared => "declared", - 75
} - 76
} - 77
} - 78
- 79
/// One typed observation with the weight it carried. - 80
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 81
pub struct Signal { - 82
pub kind: SignalKind, - 83
/// Stable identifier, e.g. `verb:refactor` or `structural:code-fence`. - 84
/// Stable because misread analysis groups by it across months of ledgers. - 85
pub name: String, - 86
pub weight: f64, - 87
/// What was actually seen, for a human reading an explain view. - 88
pub detail: String, - 89
} - 90
- 91
impl Signal { - 92
fn new( - 93
kind: SignalKind, - 94
name: impl Into<String>, - 95
weight: f64, - 96
detail: impl Into<String>, - 97
) -> Self { - 98
Signal { - 99
kind, - 100
name: name.into(), - 101
weight, - 102
detail: detail.into(), - 103
} - 104
} - 105
} - 106
- 107
/// Which surface a request arrived on. - 108
/// - 109
/// This is the strongest available evidence about [`Attendance`], because it - 110
/// is a fact about the deployment rather than a guess about the person: a cron - 111
/// tick genuinely has nobody watching it. - 112
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] - 113
#[serde(rename_all = "kebab-case")] - 114
pub enum Surface { - 115
#[default] - 116
Cli, - 117
Desktop, - 118
Server, - 119
/// A chat gateway: Telegram, Discord, Slack. - 120
Chat, - 121
/// A scheduled task or watchdog firing. - 122
Cron, - 123
/// A proactive check-in the runtime initiated. - 124
Heartbeat, - 125
/// A parent agent dispatching a child. - 126
Worker, - 127
} - 128
- 129
impl Surface { - 130
pub fn as_str(self) -> &'static str { - 131
match self { - 132
Surface::Cli => "cli", - 133
Surface::Desktop => "desktop", - 134
Surface::Server => "server", - 135
Surface::Chat => "chat", - 136
Surface::Cron => "cron", - 137
Surface::Heartbeat => "heartbeat", - 138
Surface::Worker => "worker", - 139
} - 140
} - 141
- 142
pub fn parse(value: &str) -> Option<Surface> { - 143
match value.trim().to_ascii_lowercase().as_str() { - 144
"cli" | "terminal" => Some(Surface::Cli), - 145
"desktop" | "tauri" => Some(Surface::Desktop), - 146
"server" | "http" | "api" => Some(Surface::Server), - 147
"chat" | "telegram" | "discord" | "slack" | "gateway" => Some(Surface::Chat), - 148
"cron" | "task" | "schedule" | "watchdog" => Some(Surface::Cron), - 149
"heartbeat" => Some(Surface::Heartbeat), - 150
"worker" | "child" | "subagent" => Some(Surface::Worker), - 151
_ => None, - 152
} - 153
} - 154
- 155
/// Attendance implied by the surface alone, before anything the request - 156
/// says. A caller that knows better overrides it on [`Request`]. - 157
pub fn implied_attendance(self) -> Attendance { - 158
match self { - 159
Surface::Cli | Surface::Desktop => Attendance::Interactive, - 160
// A chat message was typed by somebody, but they may well have put - 161
// the phone down; treat them as reachable, not present. - 162
Surface::Chat | Surface::Server => Attendance::Supervised, - 163
Surface::Cron | Surface::Heartbeat => Attendance::Unattended, - 164
// A child inherits its parent's attendance; this is only the - 165
// fallback when the parent said nothing. - 166
Surface::Worker => Attendance::Unattended, - 167
} - 168
} - 169
} - 170
- 171
/// An attached input. - 172
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - 173
pub struct Attachment { - 174
pub modality: Modality, - 175
pub name: String, - 176
} - 177
- 178
/// Facts about the workspace the request landed in. - 179
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - 180
pub struct WorkspaceFacts { - 181
pub is_repo: bool, - 182
/// Uncommitted changes present. Raises stakes: work here is recoverable - 183
/// only if a checkpoint is taken, because git alone will not save it. - 184
pub has_uncommitted_changes: bool, - 185
} - 186
- 187
/// Facts about the conversation so far. - 188
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - 189
pub struct HistoryFacts { - 190
/// What the previous turn was read as, if any. - 191
#[serde(default, skip_serializing_if = "Option::is_none")] - 192
pub previous_act: Option<Act>, - 193
/// How many messages the conversation held before this request; `0` - 194
/// means this is its first. - 195
pub turn_index: usize, - 196
/// Threads from earlier turns that are still open, for strand lineage. - 197
#[serde(default)] - 198
pub open_threads: Vec<crate::strand::ThreadFact>, - 199
} - 200
- 201
/// Everything tier 1 is allowed to look at. - 202
#[derive(Debug, Clone, Default)] - 203
pub struct Request<'a> { - 204
pub text: &'a str, - 205
/// A unique id the host mints for this turn (a UUIDv7). Strand ids are - 206
/// `{turn_id}.{index}`, so a thread — and the commitment keyed by it — is - 207
/// unique across turns, sessions and workspaces. Empty only in tests and - 208
/// previews, where a positional id stands in. - 209
pub turn_id: &'a str, - 210
pub surface: Surface, - 211
pub attachments: &'a [Attachment], - 212
pub workspace: WorkspaceFacts, - 213
pub history: HistoryFacts, - 214
/// Attendance the host knows for certain, overriding the surface's guess. - 215
pub attendance_override: Option<Attendance>, - 216
/// Set when the request arrived under an explicit `/goal fix` or - 217
/// `/goal replace` command. The resolver never infers these. - 218
pub lineage_hint: Option<crate::strand::LineageHint>, - 219
} - 220
- 221
// ------------------------------------------------------------- lexicon --- - 222
- 223
/// Verbs that indicate an act. One verb may vote for several acts; the tally - 224
/// across all signals decides. - 225
/// - 226
/// Kept deliberately small and general. This is a *prior*, not a classifier: - 227
/// its job is to be right often enough on plain requests that the paid tiers - 228
/// stay rare, and to abstain rather than guess on anything unusual. - 229
const ACT_VERBS: &[(&str, Act, f64)] = &[ - 230
// Answer. The wh-words are here because a question's head is one. - 231
("what", Act::Answer, 0.8), - 232
("why", Act::Answer, 0.8), - 233
("how", Act::Answer, 0.8), - 234
("explain", Act::Answer, 0.9), - 235
("describe", Act::Answer, 0.7), - 236
("summarize", Act::Answer, 0.7), - 237
("summarise", Act::Answer, 0.7), - 238
("tell", Act::Answer, 0.5), - 239
// Conversational delivery verbs describe the answer the user wants in - 240
// this chat; they do not imply a workspace artifact. Treating - 241
// "present it as a card" as Author made the stop gate demand a file or - 242
// edit receipt for an ordinary answer. - 243
("give", Act::Answer, 0.8), - 244
("present", Act::Answer, 0.7), - 245
("translate", Act::Answer, 0.7), - 246
("define", Act::Answer, 0.7), - 247
("clarify", Act::Answer, 0.6), - 248
// Locate - 249
("find", Act::Locate, 0.9), - 250
("search", Act::Locate, 0.9), - 251
("where", Act::Locate, 0.7), - 252
("locate", Act::Locate, 1.0), - 253
("list", Act::Locate, 0.7), - 254
("grep", Act::Locate, 1.0), - 255
("look", Act::Locate, 0.5), - 256
("show", Act::Locate, 0.7), - 257
// Browsing reads the web; it does not act on it. - 258
("browse", Act::Locate, 0.6), - 259
// Analyze - 260
("analyze", Act::Analyze, 1.0), - 261
("analyse", Act::Analyze, 1.0), - 262
("research", Act::Analyze, 0.9), - 263
("investigate", Act::Analyze, 0.9), - 264
("compare", Act::Analyze, 0.8), - 265
("evaluate", Act::Analyze, 0.8), - 266
("assess", Act::Analyze, 0.8), - 267
("diagnose", Act::Analyze, 0.8), - 268
("review", Act::Analyze, 0.7), - 269
("calculate", Act::Analyze, 0.8), - 270
("compute", Act::Analyze, 0.7), - 271
("measure", Act::Analyze, 0.7), - 272
("estimate", Act::Analyze, 0.7), - 273
("inspect", Act::Analyze, 0.7), - 274
("examine", Act::Analyze, 0.8), - 275
// Author - 276
("write", Act::Author, 0.8), - 277
("draft", Act::Author, 0.9), - 278
("create", Act::Author, 0.7), - 279
("generate", Act::Author, 0.7), - 280
("design", Act::Author, 0.7), - 281
("compose", Act::Author, 0.8), - 282
("plan", Act::Author, 0.5), - 283
("add", Act::Author, 0.5), - 284
// Modify - 285
("fix", Act::Modify, 0.9), - 286
("refactor", Act::Modify, 1.0), - 287
("update", Act::Modify, 0.8), - 288
("change", Act::Modify, 0.8), - 289
("edit", Act::Modify, 0.9), - 290
("rename", Act::Modify, 0.9), - 291
("remove", Act::Modify, 0.8), - 292
("delete", Act::Modify, 0.7), - 293
("implement", Act::Modify, 0.7), - 294
("migrate", Act::Modify, 0.8), - 295
("patch", Act::Modify, 0.8), - 296
("debug", Act::Modify, 0.7), - 297
("modify", Act::Modify, 0.9), - 298
("append", Act::Modify, 0.9), - 299
("prepend", Act::Modify, 0.9), - 300
("insert", Act::Modify, 0.8), - 301
("replace", Act::Modify, 0.8), - 302
("rewrite", Act::Modify, 0.8), - 303
("reformat", Act::Modify, 0.9), - 304
("convert", Act::Modify, 0.7), - 305
("tidy", Act::Modify, 0.7), - 306
("optimize", Act::Modify, 0.8), - 307
("optimise", Act::Modify, 0.8), - 308
("upgrade", Act::Modify, 0.8), - 309
("downgrade", Act::Modify, 0.8), - 310
("revert", Act::Modify, 0.9), - 311
("rebase", Act::Modify, 0.9), - 312
("merge", Act::Modify, 0.7), - 313
// Commits and moves are local and undoable; both are also nouns - 314
// ("the last commit"), so they carry less weight than a pure verb. - 315
("commit", Act::Modify, 0.6), - 316
("move", Act::Modify, 0.6), - 317
("reorder", Act::Modify, 0.8), - 318
("rearrange", Act::Modify, 0.8), - 319
("amend", Act::Modify, 0.8), - 320
("revise", Act::Modify, 0.8), - 321
("tweak", Act::Modify, 0.8), - 322
("uncomment", Act::Modify, 0.9), - 323
("enable", Act::Modify, 0.6), - 324
("disable", Act::Modify, 0.6), - 325
// Operate: reaching outside the workspace. Several of these are common - 326
// nouns too ("a blog post", "the release plan"), so they carry less - 327
// weight than an unambiguous verb and rely on the head-position bonus - 328
// when they really are the action. - 329
("deploy", Act::Operate, 0.8), - 330
("release", Act::Operate, 0.6), - 331
("publish", Act::Operate, 0.9), - 332
("send", Act::Operate, 0.9), - 333
("email", Act::Operate, 0.8), - 334
("post", Act::Operate, 0.5), - 335
("install", Act::Operate, 0.8), - 336
("restart", Act::Operate, 0.8), - 337
("schedule", Act::Operate, 0.7), - 338
("click", Act::Operate, 0.8), - 339
("pay", Act::Operate, 0.9), - 340
("notify", Act::Operate, 0.9), - 341
("remind", Act::Operate, 0.9), - 342
("alert", Act::Operate, 0.8), - 343
// Pushing, uploading and buying reach someone else's system and cannot - 344
// be quietly taken back. "Run" and "execute" are deliberately absent: - 345
// "run the tests" is a check, and reading it as an operation would make - 346
// it irreversible. - 347
("push", Act::Operate, 0.6), - 348
("upload", Act::Operate, 0.8), - 349
("submit", Act::Operate, 0.7), - 350
("purchase", Act::Operate, 0.9), - 351
("buy", Act::Operate, 0.8), - 352
// Verify. Watching and monitoring observe; they change nothing, so they - 353
// are checks rather than operations on the world. - 354
("test", Act::Verify, 0.8), - 355
("verify", Act::Verify, 1.0), - 356
("check", Act::Verify, 0.8), - 357
("validate", Act::Verify, 0.9), - 358
("confirm", Act::Verify, 0.7), - 359
("audit", Act::Verify, 0.8), - 360
("reproduce", Act::Verify, 0.8), - 361
("watch", Act::Verify, 0.7), - 362
("monitor", Act::Verify, 0.8), - 363
("compile", Act::Verify, 0.7), - 364
("lint", Act::Verify, 0.8), - 365
// Orchestrate - 366
("orchestrate", Act::Orchestrate, 1.0), - 367
("coordinate", Act::Orchestrate, 0.8), - 368
("delegate", Act::Orchestrate, 0.9), - 369
("parallel", Act::Orchestrate, 0.6), - 370
// Govern - 371
("configure", Act::Govern, 0.8), - 372
("remember", Act::Govern, 0.8), - 373
("forget", Act::Govern, 0.8), - 374
("permission", Act::Govern, 0.7), - 375
("setting", Act::Govern, 0.6), - 376
("settings", Act::Govern, 0.6), - 377
]; - 378
- 379
/// Greetings and thanks. Not verbs, so they never take the imperative bonus: - 380
/// "hey, what did we decide yesterday?" is a question with a greeting in - 381
/// front of it, and reading it as `converse` gave it minimal context. - 382
const SOCIAL_WORDS: &[(&str, f64)] = &[ - 383
("hi", 1.0), - 384
("hello", 1.0), - 385
("hey", 1.0), - 386
("hiya", 1.0), - 387
("yo", 0.8), - 388
("thanks", 1.0), - 389
("thank", 0.8), - 390
("thx", 0.8), - 391
("cheers", 0.8), - 392
("bye", 1.0), - 393
("goodbye", 1.0), - 394
]; - 395
- 396
/// Whole social phrases. Their words are consumed, so the `how` in "how are - 397
/// you" does not also vote for a factual answer. - 398
const SOCIAL_PHRASES: &[(&str, f64)] = &[ - 399
("how are you", 1.3), - 400
("how is it going", 1.3), - 401
("how s it going", 1.3), - 402
("what s up", 1.3), - 403
("good morning", 1.3), - 404
("good afternoon", 1.3), - 405
("good evening", 1.3), - 406
("good night", 1.3), - 407
("nice to meet you", 1.3), - 408
("nice to see you", 1.3), - 409
("long time no see", 1.3), - 410
("see you later", 1.3), - 411
("see you soon", 1.3), - 412
("thank you", 1.0), - 413
]; - 414
- 415
/// Acknowledgements stripped from the front of a clause. They carry no - 416
/// request of their own; alone they are social. - 417
const ACKNOWLEDGEMENTS: &[&str] = &["ok", "okay", "cool", "great", "sure", "alright", "right"]; - 418
- 419
/// Polite and conversational openers stripped so the operational verb lands - 420
/// in head position. - 421
const PREAMBLES: &[&str] = &[ - 422
"could you please", - 423
"can you please", - 424
"would you please", - 425
"would you mind", - 426
"could you", - 427
"can you", - 428
"would you", - 429
"will you", - 430
"can u", - 431
"can we", - 432
"could we", - 433
"please", - 434
"pls", - 435
"plz", - 436
"kindly", - 437
"just", - 438
"go ahead and", - 439
"help me to", - 440
"help me", - 441
"i would like you to", - 442
"i d like you to", - 443
"i want you to", - 444
"i need you to", - 445
"i want to", - 446
"i need to", - 447
"we need to", - 448
"let s", - 449
"lets", - 450
]; - 451
- 452
/// Sequencing words at the front of a clause. Structure, not verbs. - 453
const LEAD_FILLERS: &[&str] = &[ - 454
"first", "firstly", "second", "secondly", "third", "thirdly", "next", "now", "then", "finally", - 455
"lastly", "also", "and", "so", "actually", "well", "hmm", "oh", "btw", - 456
]; - 457
- 458
/// Openers that make a statement a request: "I want the parser refactored", - 459
/// "what I need is for you to refactor the module". Their words are - 460
/// consumed: the `what` of a pseudo-cleft does not ask a question. - 461
const DESIRE_PREFIXES: &[&str] = &[ - 462
"what i need", - 463
"what we need", - 464
"what i want", - 465
"what we want", - 466
"what i d like", - 467
"all i need", - 468
"i want", - 469
"i d like", - 470
"i would like", - 471
"we want", - 472
"we d like", - 473
"we would like", - 474
"i need", - 475
"we need", - 476
"i wish", - 477
]; - 478
- 479
/// Who the requester is. "Alert me", "send me the report": delivering a - 480
/// result to the person asking is an answer, not an irreversible operation - 481
/// on someone else. - 482
const REQUESTER: &[&str] = &["me", "us", "myself", "ourselves"]; - 483
- 484
/// A question's head: wh-words vote through [`ACT_VERBS`]; these auxiliaries - 485
/// open a question when a subject follows ("did the email send", "can I - 486
/// deploy on Friday", "is it done"). - 487
const AUX_WORDS: &[&str] = &[ - 488
"is", "are", "was", "were", "am", "does", "did", "has", "have", "had", "can", "could", "will", - 489
"would", "should", "shall", "may", "might", "isn", "aren", "wasn", "weren", "doesn", "didn", - 490
"hasn", "haven", "hadn", "couldn", "wouldn", "shouldn", "won", "do", - 491
]; - 492
const WH_WORDS: &[&str] = &[ - 493
"what", "why", "how", "where", "when", "which", "who", "whom", "whose", - 494
]; - 495
- 496
/// Words that follow an auxiliary in a question. `do` asks only before a - 497
/// person ("do you know"), because "do this every day" is an instruction. - 498
const QUESTION_SUBJECTS: &[&str] = &[ - 499
"i", - 500
"you", - 501
"we", - 502
"they", - 503
"he", - 504
"she", - 505
"it", - 506
"this", - 507
"that", - 508
"these", - 509
"those", - 510
"the", - 511
"there", - 512
"my", - 513
"our", - 514
"your", - 515
"their", - 516
"his", - 517
"her", - 518
"its", - 519
"any", - 520
"anyone", - 521
"anything", - 522
"anybody", - 523
"everyone", - 524
"everything", - 525
"someone", - 526
"something", - 527
"all", - 528
"each", - 529
"every", - 530
]; - 531
const PERSONS: &[&str] = &["i", "you", "we", "they", "he", "she"]; - 532
- 533
/// Verbs that, in second place, make the first word a subject: "build fails - 534
/// on CI", "deploy failed", "value moved here". A statement describes the - 535
/// situation; its first word is not an instruction even when it could be a - 536
/// verb somewhere else. - 537
const STATEMENT_VERBS: &[&str] = &[ - 538
"is", "are", "was", "were", "has", "have", "had", "does", "did", "doesn", "didn", "isn", - 539
"wasn", "won", "can", "cannot", "should", "will", "would", "fails", "failed", "failing", - 540
"occurs", "occurred", "happens", "happened", "crashes", "crashed", "crashing", "breaks", - 541
"broke", "broken", "works", "worked", "returns", "returned", "throws", "threw", "shows", - 542
"showed", "says", "said", "seems", "seemed", "looks", "looked", "keeps", "kept", "stopped", - 543
"started", "hangs", "hung", "errors", "errored", "times", "timed", "panics", "panicked", - 544
"moved", "passes", "passed", "runs", "ran", "went", "gets", "got", - 545
]; - 546
- 547
/// A clause that starts with one of these is a statement unless a verb heads - 548
/// one of its sub-clauses: "it crashes on empty input", "the deploy failed". - 549
const SUBJECT_STARTERS: &[&str] = &[ - 550
"i", - 551
"we", - 552
"you", - 553
"they", - 554
"he", - 555
"she", - 556
"it", - 557
"this", - 558
"that", - 559
"these", - 560
"those", - 561
"the", - 562
"a", - 563
"an", - 564
"my", - 565
"our", - 566
"your", - 567
"their", - 568
"his", - 569
"her", - 570
"its", - 571
"there", - 572
"here", - 573
"some", - 574
"all", - 575
"each", - 576
"every", - 577
"no", - 578
"none", - 579
"one", - 580
"someone", - 581
"something", - 582
"everyone", - 583
"everything", - 584
"nothing", - 585
"nobody", - 586
"anyone", - 587
"anything", - 588
]; - 589
- 590
/// Words that raise stakes when the act touches something. They name the - 591
/// target environment or the nature of the operation, never a topic: - 592
/// "customer", "payment", "live" and "everyone" read "rename the Customer - 593
/// struct" and "fix the payment form validation" as irreversible, capped - 594
/// approval at `ask`, and told the model to confirm an edit. - 595
const STAKES_WORDS: &[(&str, Stakes, f64)] = &[ - 596
("production", Stakes::Irreversible, 1.0), - 597
("prod", Stakes::Irreversible, 0.8), - 598
("irreversible", Stakes::Irreversible, 1.0), - 599
("irreversibly", Stakes::Irreversible, 1.0), - 600
("permanently", Stakes::Irreversible, 0.9), - 601
("force push", Stakes::Irreversible, 0.9), - 602
// The flag spellings of the same commands: `git push --force`, - 603
// `git push -f`, `git reset --hard` discard history or uncommitted work. - 604
("push force", Stakes::Irreversible, 0.9), - 605
("push f", Stakes::Irreversible, 0.9), - 606
("reset hard", Stakes::Irreversible, 0.9), - 607
("rm rf", Stakes::Irreversible, 1.0), - 608
("drop table", Stakes::Irreversible, 0.9), - 609
("drop database", Stakes::Irreversible, 1.0), - 610
// Launching: "we go live in an hour". Bare `live` is a topic word or a - 611
// recency word ([`LIVE_WORD`]), never stakes. - 612
("go live", Stakes::Irreversible, 0.8), - 613
("going live", Stakes::Irreversible, 0.8), - 614
]; - 615
- 616
/// Words that raise the evidence standard. - 617
const EVIDENCE_WORDS: &[(&str, Evidence, f64)] = &[ - 618
("cite", Evidence::Cited, 1.0), - 619
("cites", Evidence::Cited, 0.9), - 620
("citation", Evidence::Cited, 1.0), - 621
("citations", Evidence::Cited, 1.0), - 622
// Matched exactly (see `EXACT_EVIDENCE_WORDS`): inflection would fold - 623
// "source code" into it. - 624
("sources", Evidence::Cited, 0.8), - 625
("evidence", Evidence::Cited, 0.6), - 626
("prove", Evidence::Verified, 0.8), - 627
("proof", Evidence::Verified, 0.7), - 628
("passing", Evidence::Verified, 0.7), - 629
("sign off", Evidence::Audited, 0.9), - 630
("signed off", Evidence::Audited, 0.9), - 631
("acceptance test", Evidence::Audited, 0.8), - 632
("acceptance testing", Evidence::Audited, 0.8), - 633
]; - 634
- 635
/// Evidence words whose inflections mean something else. `sources` must not - 636
/// match `source` — "the source code" is not a citation request. - 637
const EXACT_EVIDENCE_WORDS: &[&str] = &["sources"]; - 638
- 639
/// "Make sure" and "ensure" demand machine-checkable proof only when the - 640
/// thing to be sure of is checkable: "make sure the tests pass" is a - 641
/// predicate, "make sure it rhymes" is a style instruction, and reading the - 642
/// latter as `verified` made the stop gate demand a shell receipt for a poem. - 643
const ASSURANCE_PHRASES: &[(&str, f64)] = &[("make sure", 0.8), ("ensure", 0.7)]; - 644
const CHECKABLE_WORDS: &[&str] = &[ - 645
"test", - 646
"tests", - 647
"testing", - 648
"pass", - 649
"passes", - 650
"passing", - 651
"build", - 652
"builds", - 653
"compile", - 654
"compiles", - 655
"compiling", - 656
"lint", - 657
"ci", - 658
"green", - 659
"works", - 660
"working", - 661
"run", - 662
"runs", - 663
]; - 664
- 665
/// `live` is two words. As an adjective or adverb ("a live score", "is it - 666
/// live") it means *current*, and is a recency word like "right now": a - 667
/// request for a fact with it asks for a value observed this turn. As a verb - 668
/// ("we live in the city", "my kids live with me") it means *reside*, and - 669
/// says nothing about time: measured live, "we live in the city" in a - 670
/// weekend-planning request set `live-data`, the freshness check refused the - 671
/// plan card, and the person got no plan at all. "Go live" — launching — - 672
/// is a stakes phrase, not a recency word ([`STAKES_WORDS`]). - 673
/// - 674
/// The verb reading is recognised from its neighbours, never from a topic: - 675
/// a subject or auxiliary right before it, or — unless a copula or "go" - 676
/// right before it makes it the adjective — a residence preposition right - 677
/// after it. Only the bare form `live` is ever read in the current sense; - 678
/// `lives`, `lived` and `living` are always the verb. - 679
const LIVE_WORD: &str = "live"; - 680
/// A word before `live` that makes it the verb "reside". - 681
const RESIDE_SUBJECTS: &[&str] = &[ - 682
"i", "we", "you", "they", "he", "she", "who", "people", "both", "all", "to", "can", "could", - 683
"would", "will", "might", "should", "must", "not", "never", "t", "d", "ll", - 684
]; - 685
/// A word after `live` that makes it "reside", unless [`LIVE_COPULAS`] - 686
/// precedes it. - 687
const RESIDE_PREPOSITIONS: &[&str] = &[ - 688
"in", "near", "with", "nearby", "abroad", "alone", "together", "close", "downtown", "outside", - 689
]; - 690
/// A word before `live` that keeps it the adjective ("is live in prod", - 691
/// "go live in an hour"). - 692
const LIVE_COPULAS: &[&str] = &[ - 693
"is", "are", "was", "were", "be", "been", "being", "s", "re", "go", "goes", "going", "went", - 694
"gone", "now", - 695
]; - 696
- 697
/// Whether some occurrence of `live` in `tokens` means *current*, not - 698
/// *reside*. - 699
fn live_means_current(tokens: &[String]) -> bool { - 700
tokens.iter().enumerate().any(|(i, token)| { - 701
if token != LIVE_WORD { - 702
return false; - 703
} - 704
let prev = i.checked_sub(1).map(|p| tokens[p].as_str()); - 705
let next = tokens.get(i + 1).map(String::as_str); - 706
let copula = prev.is_some_and(|w| LIVE_COPULAS.contains(&w)); - 707
let subject = prev.is_some_and(|w| RESIDE_SUBJECTS.contains(&w)); - 708
let preposition = next.is_some_and(|w| RESIDE_PREPOSITIONS.contains(&w)); - 709
copula || !(subject || preposition) - 710
}) - 711
} - 712
- 713
/// Temporal deixis: the request asks for a value as it stands *now*, which - 714
/// no model knows from training and which must therefore be observed on this - 715
/// turn (`live-data` domain, docs/design/68-context-engine.md §7). References - 716
/// to time, never to a topic. Whether it applies also depends on the act — - 717
/// only a request for a fact asks for a current value — which the resolver - 718
/// decides once the act is known. - 719
const RECENCY_PHRASES: &[(&str, f64)] = &[ - 720
("right now", 1.0), - 721
("currently", 0.9), - 722
("current", 0.8), - 723
("as of today", 1.0), - 724
("as of now", 1.0), - 725
("today", 0.6), - 726
("tonight", 0.7), - 727
("this morning", 0.8), - 728
("this week", 0.5), - 729
("latest", 0.7), - 730
("real time", 0.8), - 731
("at the moment", 0.9), - 732
("up to date", 0.7), - 733
// Only in the sense of *current* ([`live_means_current`]). - 734
(LIVE_WORD, 0.5), - 735
]; - 736
- 737
/// Nouns that make a recency word local rather than live: "the current - 738
/// directory", "the latest changes", "live reload". The workspace, the - 739
/// conversation and the running session are observed with local tools and - 740
/// hold no value a model could carry over stale from training. - 741
const LOCAL_NOUNS: &[&str] = &[ - 742
"directory", - 743
"dir", - 744
"folder", - 745
"file", - 746
"files", - 747
"path", - 748
"branch", - 749
"commit", - 750
"commits", - 751
"diff", - 752
"changes", - 753
"change", - 754
"working", - 755
"workspace", - 756
"project", - 757
"repo", - 758
"repository", - 759
"codebase", - 760
"code", - 761
"implementation", - 762
"function", - 763
"method", - 764
"class", - 765
"module", - 766
"line", - 767
"lines", - 768
"cursor", - 769
"selection", - 770
"tab", - 771
"window", - 772
"page", - 773
"screen", - 774
"session", - 775
"conversation", - 776
"chat", - 777
"thread", - 778
"context", - 779
"task", - 780
"plan", - 781
"step", - 782
"turn", - 783
"user", - 784
"config", - 785
"configuration", - 786
"settings", - 787
"setup", - 788
"build", - 789
"test", - 790
"tests", - 791
"reload", - 792
"preview", - 793
"server", - 794
"share", - 795
"coding", - 796
"edit", - 797
"editing", - 798
"demo", - 799
"mode", - 800
"state", - 801
// The agent's own state is read with its own tools, not retrieved from - 802
// the world: "what are you holding right now", "your current plan". - 803
"you", - 804
"your", - 805
"yours", - 806
"yourself", - 807
"commitment", - 808
"commitments", - 809
"tasks", - 810
"plans", - 811
"inbox", - 812
"memory", - 813
"notes", - 814
"reminders", - 815
]; - 816
- 817
/// The runtime tells the model the time and date on every turn, so asking - 818
/// for them needs no retrieval. - 819
const TIME_WORDS: &[&str] = &[ - 820
"time", "date", "day", "weekday", "clock", "timezone", "hour", "year", "month", - 821
]; - 822
- 823
/// Phrases implying the work outlives this turn. Sequencing words ("then", - 824
/// "after that") are not here: they separate the parts of one request, which - 825
/// is what strands are for, not a claim that the work spans sessions. - 826
const HORIZON_PHRASES: &[(&str, Horizon, f64)] = &[ - 827
("every day", Horizon::Durable, 1.0), - 828
("every night", Horizon::Durable, 1.0), - 829
("every morning", Horizon::Durable, 1.0), - 830
("every evening", Horizon::Durable, 1.0), - 831
("every month", Horizon::Durable, 1.0), - 832
("every week", Horizon::Durable, 1.0), - 833
("every hour", Horizon::Durable, 1.0), - 834
("each day", Horizon::Durable, 1.0), - 835
("each week", Horizon::Durable, 1.0), - 836
("each month", Horizon::Durable, 1.0), - 837
("every monday", Horizon::Durable, 1.0), - 838
("every tuesday", Horizon::Durable, 1.0), - 839
("every wednesday", Horizon::Durable, 1.0), - 840
("every thursday", Horizon::Durable, 1.0), - 841
("every friday", Horizon::Durable, 1.0), - 842
("every saturday", Horizon::Durable, 1.0), - 843
("every sunday", Horizon::Durable, 1.0), - 844
("every weekday", Horizon::Durable, 1.0), - 845
("every weekend", Horizon::Durable, 1.0), - 846
("nightly", Horizon::Durable, 0.9), - 847
("monthly", Horizon::Durable, 0.9), - 848
("daily", Horizon::Durable, 0.9), - 849
("weekly", Horizon::Durable, 0.9), - 850
("hourly", Horizon::Durable, 0.9), - 851
("continuously", Horizon::Durable, 0.9), - 852
("whenever", Horizon::Durable, 0.7), - 853
("keep watching", Horizon::Durable, 1.0), - 854
("keep an eye", Horizon::Durable, 0.9), - 855
("from now on", Horizon::Durable, 0.9), - 856
("ongoing", Horizon::Durable, 0.8), - 857
("until", Horizon::Durable, 0.4), - 858
("over the next", Horizon::Durable, 0.7), - 859
]; - 860
- 861
/// Recurrence words that are adjectives after a determiner ("the nightly - 862
/// job") and adverbs otherwise ("check it nightly"). Only the adverb votes. - 863
const RECURRENCE_ADJECTIVES: &[&str] = &["nightly", "daily", "weekly", "monthly", "hourly"]; - 864
const DETERMINERS: &[&str] = &[ - 865
"the", "a", "an", "this", "that", "our", "my", "your", "its", "their", "each", "of", - 866
]; - 867
- 868
/// Deictic markers: the request points at something it does not contain. - 869
pub(crate) const DEICTIC_WORDS: &[&str] = &[ - 870
"this", "that", "it", "these", "those", "here", "there", "again", "same", - 871
]; - 872
- 873
/// Pronouns that leave a request with no object of its own when they end - 874
/// it: "fix it", "deploy that". "Write a function that parses dates" uses - 875
/// `that` as a relative pronoun and points at nothing. - 876
const BARE_PRONOUNS: &[&str] = &["it", "this", "that", "these", "those"]; - 877
- 878
/// Words before a verb that still leave it heading its clause. - 879
const HEAD_PREDECESSORS: &[&str] = &[ - 880
"and", "then", "also", "plus", "next", "after", "or", "now", "please", "first", "finally", "so", - 881
]; - 882
- 883
const IMPERATIVE_BONUS: f64 = 1.6; - 884
- 885
// ------------------------------------------------------------ scoring --- - 886
- 887
/// An axis value that can name itself, so vote tallies can break ties - 888
/// deterministically without depending on the enum's declaration order. - 889
/// - 890
/// `Ord` would have been shorter and is deliberately not used: the axes spell - 891
/// their `rank` out precisely so that reordering variants cannot change a - 892
/// safety decision, and a derived `Ord` would quietly reintroduce exactly that - 893
/// coupling here. - 894
pub trait AxisValue: Copy + PartialEq { - 895
fn axis_name(self) -> &'static str; - 896
- 897
/// Position on an ordered axis, or `None` for a categorical one. - 898
/// - 899
/// This distinction is load-bearing. `Act` is categorical: `modify` and - 900
/// `answer` are rival explanations, and evidence for one really is evidence - 901
/// against the other. `Stakes` is *ordered*: a signal saying "reversible" - 902
/// does not argue against "irreversible", it agrees with it more weakly. - 903
fn axis_rank(self) -> Option<u8> { - 904
None - 905
} - 906
} - 907
- 908
impl AxisValue for Act { - 909
fn axis_name(self) -> &'static str { - 910
self.as_str() - 911
} - 912
} - 913
impl AxisValue for Horizon { - 914
fn axis_name(self) -> &'static str { - 915
self.as_str() - 916
} - 917
- 918
fn axis_rank(self) -> Option<u8> { - 919
Some(self.rank()) - 920
} - 921
} - 922
impl AxisValue for Stakes { - 923
fn axis_name(self) -> &'static str { - 924
self.as_str() - 925
} - 926
- 927
fn axis_rank(self) -> Option<u8> { - 928
Some(self.rank()) - 929
} - 930
} - 931
impl AxisValue for Evidence { - 932
fn axis_name(self) -> &'static str { - 933
self.as_str() - 934
} - 935
- 936
fn axis_rank(self) -> Option<u8> { - 937
Some(self.rank()) - 938
} - 939
} - 940
impl AxisValue for Clarity { - 941
fn axis_name(self) -> &'static str { - 942
self.as_str() - 943
} - 944
} - 945
- 946
/// Weighted votes for one axis. - 947
#[derive(Debug, Clone)] - 948
pub struct Votes<T: AxisValue> { - 949
tally: Vec<(T, f64)>, - 950
} - 951
- 952
impl<T: AxisValue> Default for Votes<T> { - 953
fn default() -> Self { - 954
Votes { tally: Vec::new() } - 955
} - 956
} - 957
- 958
impl<T: AxisValue> Votes<T> { - 959
pub fn add(&mut self, value: T, weight: f64) { - 960
match self.tally.iter_mut().find(|(v, _)| *v == value) { - 961
Some((_, score)) => *score += weight, - 962
None => self.tally.push((value, weight)), - 963
} - 964
} - 965
- 966
pub fn is_empty(&self) -> bool { - 967
self.tally.is_empty() - 968
} - 969
- 970
/// Every value that received weight, strongest first. Ties break by name - 971
/// so the ordering is total and stable across runs. - 972
pub fn ranked(&self) -> Vec<(T, f64)> { - 973
let mut ranked = self.tally.clone(); - 974
ranked.sort_by(|a, b| { - 975
b.1.partial_cmp(&a.1) - 976
.unwrap_or(std::cmp::Ordering::Equal) - 977
.then_with(|| a.0.axis_name().cmp(b.0.axis_name())) - 978
}); - 979
ranked - 980
} - 981
- 982
/// Minimum weight before a signal may escalate an ordered axis. Filters - 983
/// out the incidental 0.3-weight hints so a stray word cannot promote a - 984
/// one-liner to durable multi-day work. - 985
pub(crate) const ESCALATION_FLOOR: f64 = 0.5; - 986
- 987
/// Every value scoring within `band` of the winner, strongest first. - 988
/// - 989
/// Used for the act axis, where two readings being close is often not - 990
/// ambiguity to resolve but a request that genuinely spans both: "fix the - 991
/// failing test" is a `modify` and a `verify`, and picking one loses the - 992
/// other's tools. - 993
pub fn contenders(&self, band: f64) -> Vec<T> { - 994
let ranked = self.ranked(); - 995
let Some((_, best)) = ranked.first().copied() else { - 996
return Vec::new(); - 997
}; - 998
if best <= 0.0 { - 999
return Vec::new(); - 1000
}
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.