1//! A shell command, before it runs.
2//!
3//! Jev is asked three questions in one request: which of eight acts the
4//! command is ([`Act`]), how hard it would be to undo, and how much of the
5//! machine it takes. Five facts are read out of those three answers, each by
6//! a threshold below. Two more are not Jev's: how much memory there is room
7//! for, which the daemon measures when a rule tells it to, and whether the
8//! command has waited long enough for room, which the daemon knows.
9//! [`RULES`] turn the seven into a verdict.
10
11use jev_facts::{Judged, Prepared, Source, Unlearned};
12use jev_protocol::{Choice, Json, ProtocolError, Question, Response, Score};
13use jevhooks_events::Verdict;
14use rete::{Domain, Known, Network, Next, Rule, Test, Then};
15use serde_json::json;
16
17use crate::{Never, SHOWN_CHARS, head, percent};
18
19/// A command is put to the user when Jev's probability of one particular
20/// consequential act reaches this. One act, not their sum: a thin spread over
21/// several is Jev being unsure, which is a `Pass`, not a flag.
22pub const ASK_AT_CONSEQUENTIAL: f64 = 0.60;
23/// ... or when its expected undo level reaches this (1 is "one ordinary
24/// command puts it back", 2 is "only with care or luck").
25pub const ASK_AT_UNDO: f64 = 1.60;
26/// A command runs without asking when Jev's probability that it is one of
27/// the ordinary acts reaches this, and its expected undo level is at most
28/// [`ALLOW_UNDO_AT_MOST`].
29pub const ALLOW_AT_ORDINARY: f64 = 0.90;
30pub const ALLOW_UNDO_AT_MOST: f64 = 1.20;
31/// The memory a command of each load level should find available before it
32/// starts, in megabytes, lowest level first.
33pub const LOAD_NEEDS_MB: [f64; 4] = [0.0, 500.0, 3000.0, 6000.0];
34
35/// What "the command does" means:
36/// without it Jev judges dangerous words wherever they appear, and a command
37/// that only writes, prints or sends `rm -rf` as text is stopped as if it
38/// ran it. Sent once per request, as the state's `how_to_judge_the_command`.
39const WHAT_RUNS: &str = "Judge only what the shell would execute when this command line runs. Text that is \
40    merely carried as data is not executed: the body of a here-document, or a quoted string, that is written to \
41    a file, printed, searched for, compared, or sent as the content of a request. Such text is executed only \
42    when it is handed to something that runs it: sh, bash, eval, source, xargs, ssh, or an interpreter such as \
43    python or node. Writing a script to a file does not run the script.";
44
45/// The most consequential thing a shell command does. Each variant's
46/// definition is the text Jev chooses by.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Act {
49    Read,
50    Build,
51    Edit,
52    Delete,
53    History,
54    System,
55    Remote,
56    Unread,
57}
58
59impl Act {
60    pub const ALL: [Act; 8] =
61        [Act::Read, Act::Build, Act::Edit, Act::Delete, Act::History, Act::System, Act::Remote, Act::Unread];
62
63    /// The option's label, as Jev is given it and answers with it.
64    pub fn label(self) -> &'static str {
65        match self {
66            Act::Read => "read",
67            Act::Build => "build",
68            Act::Edit => "edit",
69            Act::Delete => "delete",
70            Act::History => "history",
71            Act::System => "system",
72            Act::Remote => "remote",
73            Act::Unread => "unread",
74        }
75    }
76
77    /// What the option means, in acts.
78    pub fn definition(self) -> &'static str {
79        match self {
80            Act::Read => {
81                "It only reads or prints: listing, searching, showing, measuring, asking a program or a \
82                 service for its status, counters or settings. Nothing on disk or anywhere else is different \
83                 afterwards. It is still this when the reading is done through another shell or on another \
84                 machine (ssh, PowerShell, a remote exec wrapper, an API request that only fetches)."
85            }
86            Act::Build => {
87                "It builds, tests, formats, lints or installs a project's dependencies: it creates, replaces \
88                 or removes generated files (build output, caches, lockfiles, formatted source) that running \
89                 the build again would produce. Also this: creating, overwriting or removing scratch files \
90                 and directories under a temporary location (/tmp, a scratchpad, a temp folder) that the \
91                 command itself makes or that exist only to be thrown away."
92            }
93            Act::Edit => {
94                "It creates or changes files a person wrote or will keep (source, notes, configuration inside \
95                 a repository), in any directory, or records them in version control (add, commit, a new \
96                 branch, a stash). The earlier content can be recovered with an ordinary version-control or \
97                 editor command."
98            }
99            Act::Delete => {
100                "It removes or overwrites files or data that someone keeps, that no build regenerates and \
101                 version control does not hold: rm of untracked or personal files, truncating a kept file \
102                 with a redirect, emptying a directory of kept files, dropping a database or its rows."
103            }
104            Act::History => {
105                "It discards or rewrites version-control state: reset --hard, checkout or restore over \
106                 uncommitted work, clean, rebase, amending pushed commits, a force push, deleting a branch \
107                 or a stash."
108            }
109            Act::System => {
110                "It changes how the machine or the user's account is set up: installing, removing or \
111                 upgrading system packages, switching the system to a new configuration, starting, stopping \
112                 or enabling services, changing permissions and ownership, editing shell profiles and \
113                 dotfiles in the home directory, writing the registry or system settings, killing processes, \
114                 anything that changes something with sudo."
115            }
116            Act::Remote => {
117                "It publishes or sends something to another machine or service: push, deploy, publish, \
118                 release, an API call that writes, sending a message."
119            }
120            Act::Unread => {
121                "It runs code nobody has read: a script piped from the network into a shell, eval of fetched \
122                 content, an installer run straight from a URL."
123            }
124        }
125    }
126
127    /// The phrase the user reads.
128    pub fn phrase(self) -> &'static str {
129        match self {
130            Act::Read => "only reads",
131            Act::Build => "builds or tests",
132            Act::Edit => "edits files in a way version control can undo",
133            Act::Delete => "deletes or overwrites data nothing regenerates",
134            Act::History => "discards or rewrites version-control state",
135            Act::System => "changes the machine or the account",
136            Act::Remote => "publishes or sends something elsewhere",
137            Act::Unread => "runs code nobody has read",
138        }
139    }
140
141    /// Acts worth stopping for. The rest are a developer's ordinary work.
142    pub fn is_consequential(self) -> bool {
143        matches!(self, Act::Delete | Act::History | Act::System | Act::Remote | Act::Unread)
144    }
145
146    fn from_label(label: &str) -> Option<Act> {
147        Act::ALL.into_iter().find(|act| act.label() == label)
148    }
149}
150
151/// How hard a command is to put back, lowest first: the Score's levels.
152const UNDO_LEVELS: [&str; 4] = [
153    "Nothing to put back: the command changes nothing.",
154    "One ordinary command puts it back: deleting a new file, git checkout or revert, running a build again.",
155    "It can be put back only with care or luck: digging through the reflog, restoring a backup, re-creating \
156     work by hand, reinstalling.",
157    "It cannot be put back from this machine: the data is gone, or it has been published or sent somewhere else.",
158];
159
160/// The same levels as the user reads them.
161const UNDO_PHRASES: [&str; 4] = ["nothing to undo", "undone by one command", "hard to undo", "cannot be undone"];
162
163/// How much of the machine a command takes while it runs, lowest first: the
164/// load Score's levels.
165const LOAD_LEVELS: [&str; 4] = [
166    "Negligible: it finishes at once and uses almost no memory. Listing, reading, git bookkeeping, moving a file.",
167    "Light: one small program for a moment. A formatter, a linter on a few files, a short script, one small test.",
168    "Heavy: it compiles a project, runs a whole test suite, builds a container or a package, or starts a \
169     browser or another AI coding session. Several processor cores and gigabytes of memory for a while.",
170    "Very heavy: several heavy jobs at once, an optimised release build of a large project, a build of many \
171     packages, or anything that holds many gigabytes of memory.",
172];
173
174/// The same levels as the user reads them.
175const LOAD_PHRASES: [&str; 4] = ["a negligible", "a light", "a heavy", "a very heavy"];
176
177/// The domain: a marker for the engine, and the source of the questions.
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct Command;
180
181/// Something known about a command.
182#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
183pub enum Fact {
184    /// One consequential act is at least [`ASK_AT_CONSEQUENTIAL`] likely.
185    Consequential,
186    /// The expected undo level is at least [`ASK_AT_UNDO`].
187    HardToUndo,
188    /// The ordinary acts together are at least [`ALLOW_AT_ORDINARY`] likely.
189    Ordinary,
190    /// The expected undo level is at most [`ALLOW_UNDO_AT_MOST`].
191    EasyToUndo,
192    /// How much of the machine it takes: the level Jev's score rounds to.
193    Load,
194    /// Whether there is memory for it now. Not Jev's: [`Effect::Measure`]
195    /// teaches it.
196    Room,
197    /// Whether it can still wait for room. Not Jev's: the daemon knows how
198    /// long the command has waited, and says so before the rules run.
199    Patience,
200}
201
202/// What a fact can turn out to be.
203#[derive(Clone, Copy, Debug, PartialEq, Eq)]
204pub enum Value {
205    Yes,
206    No,
207    /// [`Fact::Load`]'s four levels.
208    Negligible,
209    Light,
210    Heavy,
211    VeryHeavy,
212    /// [`Fact::Room`]: there is memory for what the command needs.
213    Enough,
214    /// There is less than it needs.
215    Short,
216    /// Memory cannot be measured on this machine, so the load is not judged.
217    Unmeasured,
218    /// [`Fact::Patience`]: it may wait longer.
219    Left,
220    /// It has waited as long as a command waits, or cannot be kept waiting.
221    Spent,
222}
223
224const YES_NO: [Value; 2] = [Value::Yes, Value::No];
225const LOADS: [Value; 4] = [Value::Negligible, Value::Light, Value::Heavy, Value::VeryHeavy];
226
227fn yes(holds: bool) -> Value {
228    if holds { Value::Yes } else { Value::No }
229}
230
231/// The one thing a rule sends the daemon to do.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum Effect {
234    /// See how much memory there is room for: what the machine reports
235    /// available, less what is booked for commands still growing into theirs.
236    Measure,
237}
238
239impl Domain for Command {
240    type Fact = Fact;
241    type Value = Value;
242    type Effect = Effect;
243    type End = Verdict;
244    type Note = Never;
245
246    fn facts() -> &'static [Fact] {
247        &[Fact::Consequential, Fact::HardToUndo, Fact::Ordinary, Fact::EasyToUndo, Fact::Load, Fact::Room, Fact::Patience]
248    }
249
250    // Short, because a drawing has a narrow box for a name and its value.
251    fn fact_name(fact: Fact) -> &'static str {
252        match fact {
253            Fact::Consequential => "consequential",
254            Fact::HardToUndo => "hard to undo",
255            Fact::Ordinary => "ordinary work",
256            Fact::EasyToUndo => "easy to undo",
257            Fact::Load => "load",
258            Fact::Room => "room",
259            Fact::Patience => "patience",
260        }
261    }
262
263    fn values(fact: Fact) -> &'static [Value] {
264        match fact {
265            Fact::Consequential | Fact::HardToUndo | Fact::Ordinary | Fact::EasyToUndo => &YES_NO,
266            Fact::Load => &LOADS,
267            Fact::Room => &[Value::Enough, Value::Short, Value::Unmeasured],
268            Fact::Patience => &[Value::Left, Value::Spent],
269        }
270    }
271
272    fn value_name(value: Value) -> &'static str {
273        match value {
274            Value::Yes => "yes",
275            Value::No => "no",
276            Value::Negligible => "negligible",
277            Value::Light => "light",
278            Value::Heavy => "heavy",
279            Value::VeryHeavy => "very heavy",
280            Value::Enough => "enough",
281            Value::Short => "short",
282            Value::Unmeasured => "unmeasured",
283            Value::Left => "left",
284            Value::Spent => "spent",
285        }
286    }
287
288    fn asked_for(fact: Fact) -> bool {
289        !matches!(fact, Fact::Room | Fact::Patience)
290    }
291
292    fn teaches(effect: Effect) -> Fact {
293        match effect {
294            Effect::Measure => Fact::Room,
295        }
296    }
297
298    fn effect_name(effect: Effect) -> String {
299        match effect {
300            Effect::Measure => "measure".to_owned(),
301        }
302    }
303
304    fn end_name(end: Verdict) -> String {
305        verdict_name(end).to_owned()
306    }
307
308    fn note_name(note: Never) -> String {
309        match note {}
310    }
311}
312
313/// A verdict's name, as the mod and the log spell it.
314pub fn verdict_name(verdict: Verdict) -> &'static str {
315    match verdict {
316        Verdict::Allow => "allow",
317        Verdict::Ask => "ask",
318        Verdict::Pass => "pass",
319        Verdict::Hold => "hold",
320    }
321}
322
323/// The rules, in priority order: of the rules that hold, the first decides.
324///
325/// Measuring comes first so that a command put to the user for what it does
326/// is put with both reasons when memory is short as well. The two rules on
327/// what it does come before the two on room, so such a command is never kept
328/// waiting. A command with no room is put to the user unless it is known to
329/// have patience left: not knowing is not a reason to wait. The last two
330/// are every answer the rule before them does not allow, each named for
331/// what is true of it, so an answer no other rule acts on is a `Pass`, and
332/// the user's own permission rules decide.
333pub const RULES: [Rule<'static, Command>; 8] = [
334    Rule { name: "look for room", when: &[Test::Known(Fact::Load)], then: Then::Do(Effect::Measure) },
335    Rule { name: "a consequential act", when: &[Test::Is(Fact::Consequential, Value::Yes)], then: Then::End(Verdict::Ask) },
336    Rule { name: "hard to put back", when: &[Test::Is(Fact::HardToUndo, Value::Yes)], then: Then::End(Verdict::Ask) },
337    Rule {
338        name: "no room yet",
339        when: &[Test::Is(Fact::Room, Value::Short), Test::Is(Fact::Patience, Value::Left)],
340        then: Then::End(Verdict::Hold),
341    },
342    Rule { name: "no room", when: &[Test::Is(Fact::Room, Value::Short)], then: Then::End(Verdict::Ask) },
343    Rule {
344        name: "ordinary and easily undone",
345        when: &[Test::Is(Fact::Ordinary, Value::Yes), Test::Is(Fact::EasyToUndo, Value::Yes)],
346        then: Then::End(Verdict::Allow),
347    },
348    Rule { name: "not surely ordinary", when: &[Test::Is(Fact::Ordinary, Value::No)], then: Then::End(Verdict::Pass) },
349    Rule { name: "not easily undone", when: &[Test::Is(Fact::EasyToUndo, Value::No)], then: Then::End(Verdict::Pass) },
350];
351
352/// [`RULES`] as the network that runs.
353pub fn network() -> Network<Command> {
354    Network::compile(&RULES)
355}
356
357/// What Jev judges: the command, where it runs, and how to read it.
358///
359/// What "the command does" means is said once, here, where all three
360/// questions read it. It used to end each question; asked both ways, ten
361/// commands got the same verdicts and the request was 212 tokens smaller
362/// (1,555 to 1,343; the daemon's live test, 2026-10-06).
363pub fn state(command: &str, cwd: Option<&str>, root: Option<&str>) -> Result<Json, ProtocolError> {
364    let state = json!({
365        "command": head(command, SHOWN_CHARS),
366        "working_directory": cwd,
367        "project_root": root,
368        "how_to_judge_the_command": WHAT_RUNS,
369    });
370    Json::verbatim(&state.to_string())
371}
372
373/// The probability of each act, from the `act` Choice's answer.
374fn acts(answer: &jev_protocol::ChoiceAnswer) -> Vec<(Act, f64)> {
375    answer.probabilities.iter().filter_map(|(label, p)| Act::from_label(label).map(|act| (act, *p))).collect()
376}
377
378/// The likeliest single consequential act's probability. One act, never
379/// their sum.
380fn consequential(acts: &[(Act, f64)]) -> f64 {
381    acts.iter().filter(|(act, _)| act.is_consequential()).map(|(_, p)| *p).fold(0.0, f64::max)
382}
383
384/// The ordinary acts' probabilities together.
385fn ordinary(acts: &[(Act, f64)]) -> f64 {
386    acts.iter().filter(|(act, _)| !act.is_consequential()).map(|(_, p)| p).sum()
387}
388
389impl Source for Command {
390    type Fact = Fact;
391    type Value = Value;
392
393    fn question_id(&self, fact: Fact) -> String {
394        match fact {
395            Fact::Consequential | Fact::Ordinary => "act",
396            Fact::HardToUndo | Fact::EasyToUndo => "undo",
397            Fact::Load => "load",
398            Fact::Room => "room",
399            Fact::Patience => "patience",
400        }
401        .to_owned()
402    }
403
404    fn question(&self, fact: Fact) -> Result<Question, ProtocolError> {
405        Ok(match fact {
406            Fact::Consequential | Fact::Ordinary => Question::Choice(Choice::new(
407                Json::text(
408                    "A coding assistant is about to run this shell command for a developer. Which one of these \
409                     describes what the command does? If it does several of them, pick the one that is hardest to \
410                     undo.",
411                ),
412                Act::ALL.map(|act| (act.label().to_owned(), Some(Json::text(act.definition())))),
413            )?),
414            Fact::HardToUndo | Fact::EasyToUndo => Question::Score(Score::new(
415                Json::text("How hard would it be to put everything back exactly as it was before this command ran?"),
416                UNDO_LEVELS.map(Json::text),
417            )?),
418            Fact::Load => Question::Score(Score::new(
419                Json::text("How much of the machine does this command take while it runs?"),
420                LOAD_LEVELS.map(Json::text),
421            )?),
422            Fact::Room | Fact::Patience => {
423                return Err(ProtocolError::Invalid(format!("{} is not a fact Jev is asked for", Command::fact_name(fact))));
424            }
425        })
426    }
427
428    fn learned(&self, fact: Fact, judged: &Judged) -> Option<Value> {
429        Some(match (fact, judged) {
430            (Fact::Consequential, Judged::Choice(answer)) => yes(consequential(&acts(answer)) >= ASK_AT_CONSEQUENTIAL),
431            (Fact::Ordinary, Judged::Choice(answer)) => yes(ordinary(&acts(answer)) >= ALLOW_AT_ORDINARY),
432            (Fact::HardToUndo, Judged::Score(answer)) => yes(answer.score >= ASK_AT_UNDO),
433            (Fact::EasyToUndo, Judged::Score(answer)) => yes(answer.score <= ALLOW_UNDO_AT_MOST),
434            (Fact::Load, Judged::Score(answer)) => LOADS[(answer.score.round() as usize).min(LOADS.len() - 1)],
435            _ => return None,
436        })
437    }
438}
439
440/// Jev's numbers about a command, beside the facts they teach: what the line
441/// the user reads is written from, what the memory it needs is worked out
442/// from, and what the decision log keeps.
443#[derive(Clone, Debug)]
444pub struct Answers {
445    /// The probability of each act.
446    pub acts: Vec<(Act, f64)>,
447    /// The expected undo level, 0 to 3.
448    pub undo: f64,
449    /// The expected load level, 0 to 3.
450    pub load: f64,
451    json: serde_json::Value,
452}
453
454impl Answers {
455    /// From a request's answers; `None` unless all three questions were in it.
456    fn read(prepared: &Prepared, judged: &[Judged]) -> Option<Self> {
457        let answer = |id: &str| prepared.parts.iter().position(|part| part.id == id).and_then(|index| judged.get(index));
458        let (Judged::Choice(act), Judged::Score(undo), Judged::Score(load)) = (answer("act")?, answer("undo")?, answer("load")?) else {
459            return None;
460        };
461        Some(Answers {
462            acts: acts(act),
463            undo: undo.score,
464            load: load.score,
465            json: json!({
466                "act": {
467                    "choice": act.choice,
468                    "confidence": act.confidence,
469                    "probabilities": act.probabilities.iter().map(|(label, p)| (label.clone(), json!(p))).collect::<serde_json::Map<String, serde_json::Value>>(),
470                },
471                "undo": { "score": undo.score, "probabilities": undo.probabilities },
472                "load": { "score": load.score, "probabilities": load.probabilities },
473            }),
474        })
475    }
476
477    /// The megabytes this command should find free before it starts.
478    pub fn needs_mb(&self) -> f64 {
479        needed_mb(self.load)
480    }
481
482    /// The likeliest single consequential act's probability: the number
483    /// behind [`Fact::Consequential`].
484    pub fn consequential(&self) -> f64 {
485        consequential(&self.acts)
486    }
487
488    /// The ordinary acts' probabilities together: the number behind
489    /// [`Fact::Ordinary`].
490    pub fn ordinary(&self) -> f64 {
491        ordinary(&self.acts)
492    }
493
494    /// Every option's probability, as the decision log keeps it.
495    pub fn json(&self) -> serde_json::Value {
496        self.json.clone()
497    }
498}
499
500/// What Jev said about a command: the facts, and the numbers behind them.
501/// Kept apart from the verdict because the verdict also depends on how much
502/// memory there is room for, and that changes while a command waits its turn.
503#[derive(Clone, Debug)]
504pub struct Asked {
505    pub known: Known<Command>,
506    pub answers: Answers,
507}
508
509/// The facts the first step asks Jev for: every fact of the domain that is
510/// Jev's to give, since every rule is waiting on one.
511pub fn wanted(network: &Network<Command>) -> Vec<Fact> {
512    match network.next(&Known::default()) {
513        Next::Ask(facts) => facts,
514        // The rules always begin by asking; `the_first_step_asks_everything` holds that.
515        _ => Vec::new(),
516    }
517}
518
519/// What a response to `prepared` (a request for `facts`) teaches.
520pub fn taught(prepared: &Prepared, response: &Response, facts: &[Fact]) -> Result<Asked, Unlearned> {
521    let judged = prepared.judged(response);
522    let mut known = Known::default();
523    for (fact, value) in jev_facts::learn(&Command, prepared, &judged, facts)? {
524        known.learn(fact, value);
525    }
526    let answers = Answers::read(prepared, &judged).ok_or(Unlearned::NotAsked { id: "act, undo and load".to_owned() })?;
527    Ok(Asked { known, answers })
528}
529
530/// The memory a command of expected load level `load` should find
531/// available, interpolated between the levels' needs.
532pub fn needed_mb(load: f64) -> f64 {
533    let load = load.clamp(0.0, (LOAD_NEEDS_MB.len() - 1) as f64);
534    let below = load.floor() as usize;
535    let above = (below + 1).min(LOAD_NEEDS_MB.len() - 1);
536    LOAD_NEEDS_MB[below] + (LOAD_NEEDS_MB[above] - LOAD_NEEDS_MB[below]) * (load - below as f64)
537}
538
539/// What [`Effect::Measure`] teaches: whether `room_mb` megabytes are enough
540/// for a command that needs `needs_mb`. No figure is no opinion.
541pub fn room(needs_mb: f64, room_mb: Option<f64>) -> Value {
542    match room_mb {
543        None => Value::Unmeasured,
544        Some(room) if room < needs_mb => Value::Short,
545        Some(_) => Value::Enough,
546    }
547}
548
549/// [`Fact::Patience`], from whether the command may be kept waiting longer.
550pub fn patience(may_wait: bool) -> Value {
551    if may_wait { Value::Left } else { Value::Spent }
552}
553
554impl Asked {
555    /// Whether the command is put to the user whatever memory there is.
556    pub fn is_risky(&self) -> bool {
557        [Fact::Consequential, Fact::HardToUndo].into_iter().any(|fact| self.known.get(fact) == Some(Value::Yes))
558    }
559
560    /// The verdict, with `room_mb` megabytes to spare (absent where memory
561    /// cannot be measured) and whether the command may be kept waiting, and
562    /// the line the user reads. The network decides; when it says to
563    /// measure, the figure given is the measurement.
564    pub fn settle(&self, network: &Network<Command>, room_mb: Option<f64>, may_wait: bool) -> (Verdict, String) {
565        let mut known = self.known.clone();
566        known.forget(Fact::Room);
567        known.learn(Fact::Patience, patience(may_wait));
568        let verdict = loop {
569            match network.next(&known) {
570                Next::Do(Effect::Measure) => known.learn(Fact::Room, room(self.answers.needs_mb(), room_mb)),
571                Next::End(verdict) => break verdict,
572                // Nothing Jev did not answer can be asked from here, and
573                // not knowing never blocks.
574                Next::Ask(_) | Next::Done => break Verdict::Pass,
575            }
576        };
577        (verdict, self.line(&known, room_mb, verdict))
578    }
579
580    /// What Jev thought and how sure it was, as the permission prompt and
581    /// the band show it.
582    fn line(&self, known: &Known<Command>, room_mb: Option<f64>, verdict: Verdict) -> String {
583        let Answers { acts, undo, load, .. } = &self.answers;
584        let (likeliest, likelihood) = acts.iter().copied().max_by(|a, b| a.1.total_cmp(&b.1)).unwrap_or((Act::Read, 0.0));
585        let level = (undo.round() as usize).min(UNDO_PHRASES.len() - 1);
586        let said = format!("Jev: {} ({}%); {} ({undo:.1} of 3)", likeliest.phrase(), percent(likelihood), UNDO_PHRASES[level]);
587        // Too little memory for what the command is about to start.
588        let short = room_mb.filter(|_| known.get(Fact::Room) == Some(Value::Short)).map(|room| {
589            let load_level = (load.round() as usize).min(LOAD_PHRASES.len() - 1);
590            format!("{} command ({load:.1} of 3) with {:.1} GB of memory to spare", LOAD_PHRASES[load_level], room / 1024.0)
591        });
592        match (verdict, self.is_risky(), short) {
593            (Verdict::Ask, true, Some(short)) => format!("{said}; {short}"),
594            (Verdict::Ask, true, None) => said,
595            (Verdict::Ask | Verdict::Hold, false, Some(short)) => format!("Jev: {short}"),
596            (Verdict::Allow, ..) => format!("{said}; allowed"),
597            _ => format!("{said}; left to the usual permission check"),
598        }
599    }
600
601    /// Answers made up for a test or a worked example, read the way a real
602    /// response is: a response body with these numbers, parsed and learned
603    /// from.
604    #[cfg(feature = "made-up")]
605    pub fn made_up(acts: &[(Act, f64)], undo: f64, load: f64) -> Self {
606        let network = network();
607        let facts = wanted(&network);
608        let model = crate::jev_model().expect("the pinned model");
609        let prepared = jev_facts::wanted(&Command, &model, state("a command", None, None).expect("a state"), &facts).expect("a request");
610        let probabilities: Vec<(&str, f64)> =
611            Act::ALL.iter().map(|each| (each.label(), acts.iter().find(|(act, _)| act == each).map_or(0.0, |(_, p)| *p))).collect();
612        let body = crate::made_up_body(&[
613            ("act", crate::made_up_choice(&probabilities)),
614            ("undo", crate::made_up_score(undo, UNDO_LEVELS.len())),
615            ("load", crate::made_up_score(load, LOAD_LEVELS.len())),
616        ]);
617        let response = Response::parse(&model, prepared.asking().1, body.as_bytes()).expect("a response Jev could send");
618        taught(&prepared, &response, &facts).expect("every fact asked for")
619    }
620
621    /// Made up: Jev sure of one act.
622    #[cfg(feature = "made-up")]
623    pub fn sure_of(act: Act, undo: f64, load: f64) -> Self {
624        Self::made_up(&[(act, 1.0)], undo, load)
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    /// The verdict on made-up answers with this much memory to spare and nobody to wait for.
633    fn verdict(acts: &[(Act, f64)], undo: f64, load: f64, room_mb: Option<f64>) -> (Verdict, String) {
634        Asked::made_up(acts, undo, load).settle(&network(), room_mb, false)
635    }
636
637    fn surely(act: Act) -> Vec<(Act, f64)> {
638        vec![(act, 1.0)]
639    }
640
641    /// Every fact Jev can give goes in the one first request, and the three questions behind
642    /// the five facts are each asked once.
643    #[test]
644    fn the_first_step_asks_everything() {
645        let facts = wanted(&network());
646        assert_eq!(facts, [Fact::Consequential, Fact::HardToUndo, Fact::Ordinary, Fact::EasyToUndo, Fact::Load]);
647        let prepared = jev_facts::wanted(&Command, &crate::jev_model().unwrap(), state("ls", None, None).unwrap(), &facts).unwrap();
648        let ids: Vec<&str> = prepared.parts.iter().map(|part| part.id.as_str()).collect();
649        assert_eq!(ids, ["act", "undo", "load"]);
650        // What "the command does" means is in the state, once.
651        assert_eq!(prepared.request.matches("Judge only what the shell would execute").count(), 1);
652    }
653
654    /// Reading, building and editing, undone by one command at most, run without a prompt: this is
655    /// the plugin's whole benefit on an ordinary day.
656    #[test]
657    fn ordinary_work_is_allowed() {
658        assert_eq!(verdict(&surely(Act::Read), 0.0, 1.0, Some(8000.0)).0, Verdict::Allow);
659        assert_eq!(verdict(&surely(Act::Build), 1.0, 1.0, Some(8000.0)).0, Verdict::Allow);
660        // A commit, or a note written in another repository.
661        assert_eq!(verdict(&surely(Act::Edit), 1.0, 1.0, Some(8000.0)).0, Verdict::Allow);
662    }
663
664    /// Each of the five consequential acts stops for the user, even when it would be easy to undo.
665    #[test]
666    fn consequential_acts_are_put_to_the_user() {
667        for act in [Act::Delete, Act::History, Act::System, Act::Remote, Act::Unread] {
668            assert_eq!(verdict(&surely(act), 1.0, 1.0, Some(8000.0)).0, Verdict::Ask, "{act:?}");
669        }
670    }
671
672    /// The undo score is a second, independent reason to ask: a harmless-sounding act that cannot
673    /// be put back still stops.
674    #[test]
675    fn anything_hard_to_undo_is_put_to_the_user_whatever_it_is_called() {
676        assert_eq!(verdict(&surely(Act::Edit), 2.0, 1.0, Some(8000.0)).0, Verdict::Ask);
677        assert_eq!(verdict(&surely(Act::Build), 1.6, 1.0, Some(8000.0)).0, Verdict::Ask);
678    }
679
680    /// Jev not knowing is a `Pass`: the user's own permission rules decide, as if the plugin were
681    /// not there.
682    #[test]
683    fn an_unsure_answer_is_neither_allowed_nor_asked() {
684        let split = [(Act::Edit, 0.6), (Act::Delete, 0.4)];
685        assert_eq!(verdict(&split, 1.3, 1.0, Some(8000.0)).0, Verdict::Pass);
686    }
687
688    /// The bug this fixed: summing a thin spread over several consequential acts stopped a command
689    /// Jev was merely unsure about.
690    #[test]
691    fn doubt_spread_over_several_consequential_acts_is_not_a_flag() {
692        let spread = [(Act::Unread, 0.36), (Act::System, 0.2), (Act::Delete, 0.14), (Act::Edit, 0.3)];
693        assert_eq!(verdict(&spread, 1.3, 1.0, Some(8000.0)).0, Verdict::Pass);
694    }
695
696    /// With no room and nobody to wait for, a heavy command is put to the user, and the line says
697    /// what it needs and what there is. No figure means no opinion.
698    #[test]
699    fn a_heavy_command_is_put_to_the_user_when_memory_is_short() {
700        // A build with 1.5 GB available: a heavy command wants 3 GB.
701        let (verdict_, line) = verdict(&surely(Act::Build), 1.0, 2.0, Some(1500.0));
702        assert_eq!(verdict_, Verdict::Ask);
703        assert_eq!(line, "Jev: a heavy command (2.0 of 3) with 1.5 GB of memory to spare");
704        // The same build with room to spare runs.
705        assert_eq!(verdict(&surely(Act::Build), 1.0, 2.0, Some(8000.0)).0, Verdict::Allow);
706        // A light command is not held up by the same shortage.
707        assert_eq!(verdict(&surely(Act::Edit), 1.0, 0.3, Some(1500.0)).0, Verdict::Allow);
708        // Where memory cannot be measured, load is not judged.
709        assert_eq!(verdict(&surely(Act::Build), 1.0, 3.0, None).0, Verdict::Allow);
710    }
711
712    /// The same shortage with patience left is a wait, not a question: and a command that would be
713    /// put to the user anyway is never kept waiting, and is put with both reasons.
714    #[test]
715    fn a_command_with_no_room_waits_unless_it_is_risky_or_has_waited() {
716        let build = Asked::sure_of(Act::Build, 1.0, 2.0);
717        assert_eq!(build.settle(&network(), Some(1500.0), true).0, Verdict::Hold);
718        assert_eq!(build.settle(&network(), Some(1500.0), false).0, Verdict::Ask);
719        let push = Asked::sure_of(Act::History, 2.3, 2.0);
720        let (verdict, line) = push.settle(&network(), Some(1500.0), true);
721        assert_eq!(verdict, Verdict::Ask);
722        assert_eq!(
723            line,
724            "Jev: discards or rewrites version-control state (100%); hard to undo (2.3 of 3); a heavy command (2.0 of 3) with 1.5 GB of memory to spare"
725        );
726    }
727
728    /// A command with no room and nothing known about its patience is put to the user: only a
729    /// command known to have patience left waits.
730    #[test]
731    fn not_knowing_whether_it_may_wait_is_not_a_reason_to_wait() {
732        let mut known = Asked::sure_of(Act::Build, 1.0, 2.0).known;
733        known.learn(Fact::Room, Value::Short);
734        assert_eq!(network().next(&known), Next::End(Verdict::Ask));
735    }
736
737    /// Room is measured before anything ends, whatever Jev said: the rule that measures is first.
738    #[test]
739    fn the_room_is_measured_before_a_verdict() {
740        for asked in [Asked::sure_of(Act::Read, 0.0, 0.0), Asked::sure_of(Act::History, 3.0, 3.0)] {
741            assert_eq!(network().next(&asked.known), Next::Do(Effect::Measure));
742        }
743    }
744
745    /// A load between two levels needs memory between theirs, so a score of 2.5 is not treated as a
746    /// 2 or a 3.
747    #[test]
748    fn need_rises_between_levels() {
749        assert_eq!(needed_mb(0.0), 0.0);
750        assert_eq!(needed_mb(2.0), 3000.0);
751        assert_eq!(needed_mb(2.5), 4500.0);
752        assert_eq!(needed_mb(9.0), 6000.0);
753    }
754
755    /// The line is what the user reads in the permission prompt and the band: it must say what Jev
756    /// thought and how sure it was.
757    #[test]
758    fn the_line_names_the_likeliest_act_and_the_undo_level() {
759        let (_, line) = verdict(&surely(Act::History), 2.3, 1.0, Some(8000.0));
760        assert_eq!(line, "Jev: discards or rewrites version-control state (100%); hard to undo (2.3 of 3)");
761    }
762
763    /// A label is what Jev answers with and what is matched back; two acts sharing one would
764    /// silently merge their probabilities.
765    #[test]
766    fn every_act_has_its_own_label() {
767        for act in Act::ALL {
768            assert_eq!(Act::from_label(act.label()), Some(act));
769        }
770    }
771
772    /// A rule lights up in the drawing when it holds, so no rule may hold for an answer it is not
773    /// about: for ordinary, easily undone work with room, the rule that allows is the only one
774    /// that ends it. (The catch-all this replaced held for every answer.)
775    #[test]
776    fn only_the_rule_that_applies_holds() {
777        let mut known = Asked::sure_of(Act::Build, 1.0, 1.0).known;
778        known.learn(Fact::Room, Value::Enough);
779        known.learn(Fact::Patience, Value::Left);
780        assert_eq!(network().holding(&known), ["look for room", "ordinary and easily undone"]);
781    }
782
783    /// The rules name only values their facts can have, and every rule has a name of its own: what
784    /// a loader would refuse in a file is refused here for the rules in code.
785    #[test]
786    fn every_rule_tests_a_value_its_fact_can_have() {
787        for rule in RULES {
788            for test in rule.when {
789                if let Test::Is(fact, value) = test {
790                    assert!(Command::values(*fact).contains(value), "{}: {fact:?} cannot be {value:?}", rule.name);
791                }
792            }
793            assert_eq!(RULES.iter().filter(|other| other.name == rule.name).count(), 1, "{}", rule.name);
794        }
795    }
796}