sprawling 0.0.12

A local multi-agent harness for persistent agent teams: long-running project work, agent communication, and experiments in coordination and social simulation.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! What one building's capability bits call for, and which of it this
//! machine lacks (`crates/sprawling/spec/Doctor.lean` §8-48).
//!
//! No I/O: the bits arrive read, the findings arrive examined, and what
//! comes out is the list of things a person would otherwise learn from
//! a refusal in the middle of a run.

use kernel::Address;

use super::family::{GECKO, WEBKIT};
use super::table::{CHROMEDRIVER, MSEDGEDRIVER, PWSH, SHELL};
use super::{Finding, Presence};

/// The bits a building declares. One lives in `RULES.toml`, one in the
/// building's frozen `CONFIG.toml`; they are one value here because a
/// building is judged whole. `desktop` is not among them: everything it
/// needs is in this binary (`crates/sprawling/spec/Main.lean` §8-4d).
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Bits {
    pub(crate) browser: bool,
    pub(crate) shell: bool,
    /// Which interpreter that shell runs under; only read when `shell`.
    pub(crate) interpreter: kernel::Interpreter,
}

/// One thing a building can ask this machine for.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Capability {
    Browser,
    /// The platform's own shell.
    Shell,
    /// PowerShell 7, for a building whose shell runs under it.
    Pwsh,
}

impl Capability {
    pub(crate) const ALL: [Capability; 3] =
        [Capability::Browser, Capability::Shell, Capability::Pwsh];

    /// The items of which any one satisfies this capability, first
    /// choice first. The Gecko family leads because it needs no driver;
    /// a Chromium browser is reached only through the driver beside it,
    /// so the driver rather than the browser is what is tried here.
    pub(crate) fn any_of(self) -> &'static [&'static str] {
        match self {
            Capability::Browser => &[GECKO, CHROMEDRIVER, MSEDGEDRIVER, WEBKIT],
            Capability::Shell => &[SHELL],
            Capability::Pwsh => &[PWSH],
        }
    }

    /// The key in the building's own files that asked for it, spelled
    /// the way that file is written.
    ///
    /// The two rules files are not the same syntax, and the spelling
    /// here is the one a person has to type: a building's capability is
    /// a key in its `RULES.toml`, a shell is a key in its
    /// `CONFIG.toml`. This is the only place either is written out for
    /// a person to read; prose that named one itself would be the
    /// second home, and the first to go stale when the file's syntax
    /// moved.
    pub(crate) fn declared_as(self) -> &'static str {
        match self {
            Capability::Browser => "browser = true",
            Capability::Shell => "sandbox.shell = true",
            Capability::Pwsh => "sandbox.interpreter = \"pwsh\"",
        }
    }

    fn asked_by(self, bits: &Bits) -> bool {
        match self {
            Capability::Browser => bits.browser,
            Capability::Shell => bits.shell && bits.interpreter == kernel::Interpreter::System,
            Capability::Pwsh => bits.shell && bits.interpreter == kernel::Interpreter::Pwsh,
        }
    }
}

/// One capability a building asked for and this machine cannot give,
/// with every item that was tried and what each one answered.
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Lack {
    pub(crate) building: Address,
    pub(crate) capability: Capability,
    pub(crate) tried: Vec<(&'static str, Presence)>,
}

/// Everything this building asked for that this machine lacks, in the
/// order of `Capability::ALL`.
pub(crate) fn lacks(building: &Address, bits: &Bits, findings: &[Finding]) -> Vec<Lack> {
    Capability::ALL
        .into_iter()
        .filter(|capability| capability.asked_by(bits))
        .filter_map(|capability| {
            let tried: Vec<(&'static str, Presence)> = capability
                .any_of()
                .iter()
                .map(|item| (*item, answer_for(findings, item)))
                .collect();
            if tried.iter().any(|(_, presence)| presence.usable()) {
                return None;
            }
            Some(Lack {
                building: building.clone(),
                capability,
                tried,
            })
        })
        .collect()
}

/// The one line a person reads about one lack: the building, the line
/// that asked, and each road with why it is closed.
pub(crate) fn lack_line(lack: &Lack) -> String {
    let roads: Vec<String> = lack
        .tried
        .iter()
        .map(|(item, presence)| match presence {
            Presence::Absent(absence) => format!("no {item} ({})", absence.describe()),
            other @ (Presence::Present { .. } | Presence::Broken { .. }) => {
                format!("{item} {}", other.describe())
            }
        })
        .collect();
    format!(
        "  {}: {}, and this machine has {}",
        lack.building.as_str(),
        lack.capability.declared_as(),
        roads.join(" and ")
    )
}

/// What the findings say about one item; an item the table does not
/// carry is reported as not on the search path rather than skipped, so
/// a capability can never be satisfied by an item nobody looked for.
fn answer_for(findings: &[Finding], item: &str) -> Presence {
    findings
        .iter()
        .find(|finding| finding.requirement.name == item)
        .map_or(
            Presence::Absent(super::Absence::NotOnSearchPath),
            |finding| finding.presence.clone(),
        )
}