Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

src/tools.rs

//! **The 🧰 Tools menu** — modgunn's slice of the constellation-wide COMMON
//! FRAMEWORK: a menu whose entries *generate demo data* by starting a container
//! **THROUGH jera** ([`jera::container::ContainerSpec`] run as a tracked job), so
//! modgunn can then **scan** the result. This is the same "not a demo *mode*, but a
//! menu of one-click data-fill actions" model korp's `Tools` menu uses — and it is
//! CONSUMED from the SAME shared runtime (jera → draupnir's ONE OCI engine), never a
//! bespoke `docker`/`podman` call and never a `thread::spawn` (L5, Core-saturation).
//!
//! The model here is **pure + UI-free** (unit-testable headless): the menu is a list
//! of [`ToolEntry`]s, each naming the jera container spec it starts and the modgunn
//! verb that then VIEWs the generated data. The LIVE spawn ([`run_demo_feed`]) is
//! behind the `framework-oci` feature (→ `jera/container-oci`); without it the spec
//! still compiles and `to_draupnir().validate()`s — the tested wiring.
//!
//! **Shared-component note (L5, move-to-edda candidate).** korp carries an almost
//! identical `tools.rs` (`ToolEffect`/`ToolEntry`/`entries()`/`state_json`) + a thin
//! `jobs.rs` ledger `sink()` adapter. That menu-model + the `default_workspace_sink`
//! shape are **copied, not shared** across korp and now modgunn — a clean candidate
//! to lift into an `edda` leaf (a `jera::jobs::workspace_sink(workspace)` helper + a
//! generic `ToolMenu` model). For THIS task modgunn CONSUMES jera directly (the
//! ContainerSpec + `run_container` job ledger); the thin adapter below is the minimum
//! the current jera API needs (`JobStore::open` wants a root dir).

use std::path::PathBuf;

use jera::container::{BindMount, ContainerSpec};

/// modgunn's workspace tag on every job row (the `workspace` axis in `jera`), so a
/// reader groups/filters modgunn's Tools-menu jobs.
pub const WORKSPACE: &str = "modgunn";

/// The **demo advisory-feed** container: a pinned image that writes a tiny OSV
/// advisory document into a bind-mounted `/out` directory, which `modgunn warehouse
/// import --osv <dir>` then ingests → and `modgunn warehouse scan` VIEWs. Pinned —
/// **never `latest`** — so a re-run is reproducible (env-overridable via
/// `MODGUNN_DEMO_FEED_IMAGE`). Alpine `sh` is only used to write a file *inside the
/// container* (the container's own job) — modgunn's build/release tooling stays
/// zero-shell (L9).
pub const DEMO_FEED_IMAGE_DEFAULT: &str = "docker.io/library/alpine:3.20.3";
/// The stable managed-container name for the demo feed (draupnir prefixes `draupnir-`).
pub const DEMO_FEED_NAME: &str = "modgunn-demo-feed";

/// A minimal, valid OSV advisory the demo feed emits (a benign `left-pad < 1.3.0`
/// fixture) so a subsequent `warehouse import --osv` has a real document to fold.
const DEMO_OSV_JSON: &str = r#"{"id":"MODGUNN-DEMO-0001","summary":"demo advisory generated by the modgunn Tools demo feed","affected":[{"package":{"ecosystem":"npm","name":"left-pad"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.3.0"}]}]}]}"#;

/// The demo image to run — `MODGUNN_DEMO_FEED_IMAGE` else [`DEMO_FEED_IMAGE_DEFAULT`].
pub fn demo_feed_image() -> String {
    std::env::var("MODGUNN_DEMO_FEED_IMAGE").unwrap_or_else(|_| DEMO_FEED_IMAGE_DEFAULT.to_string())
}

/// The in-container script the demo feed runs: create `/out` and write the demo OSV
/// document there. Runs INSIDE the container (its own job); it is not host shell-out.
fn demo_feed_script() -> String {
    format!("mkdir -p /out && printf '%s' '{DEMO_OSV_JSON}' > /out/MODGUNN-DEMO-0001.json")
}

/// What a Tools-menu entry does on pick. Each variant is a *generate → import →
/// scan* chain over jera-started infra (the modgunn analogue of korp's *fill → view*).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolEffect {
    /// Start the **demo advisory feed** (jera) → it writes a demo OSV doc into a
    /// bind-mounted dir → `warehouse import --osv` folds it → VIEW via `warehouse scan`.
    DemoAdvisoryFeed,
}

impl ToolEffect {
    /// A stable key (for `state_json` + tests).
    pub fn key(self) -> &'static str {
        match self {
            ToolEffect::DemoAdvisoryFeed => "demo_advisory_feed",
        }
    }

    /// The jera container this effect starts (its stable name).
    pub fn needs_container(self) -> &'static str {
        match self {
            ToolEffect::DemoAdvisoryFeed => DEMO_FEED_NAME,
        }
    }
}

/// One entry in the 🧰 Tools menu: a human title + hover, the [`ToolEffect`] it runs,
/// and the modgunn verb the generated data is then VIEWed through.
#[derive(Debug, Clone)]
pub struct ToolEntry {
    /// Menu label (with a leading glyph).
    pub title: &'static str,
    /// Hover help — what data this generates and where to view it.
    pub hover: &'static str,
    /// The effect the host runs on click.
    pub effect: ToolEffect,
    /// The modgunn CLI verb that VIEWs the resulting data.
    pub view_verb: &'static str,
}

/// The 🧰 Tools menu contents — the one source of truth the CLI renders and a test
/// drives. Order is stable (used by the robot test).
pub fn entries() -> Vec<ToolEntry> {
    vec![ToolEntry {
        title: "🩺 Demo advisory feed — generate + scan",
        hover: "Start a pinned demo container (jera) that writes a demo OSV advisory \
                into an out dir, then `warehouse import --osv` folds it and `warehouse \
                scan` shows the verdict",
        effect: ToolEffect::DemoAdvisoryFeed,
        view_verb: "warehouse scan",
    }]
}

/// The **demo advisory-feed** [`jera::container::ContainerSpec`]: the pinned image,
/// the stable name, the in-container write script, and a read-write bind of the host
/// `out_dir` at `/out` (where the OSV doc lands). Starting it goes THROUGH jera →
/// draupnir ([`run_demo_feed`]); no bespoke docker call.
pub fn demo_feed_spec(out_dir: &str) -> ContainerSpec {
    ContainerSpec {
        image: demo_feed_image(),
        name: DEMO_FEED_NAME.to_string(),
        cmd: vec!["sh".to_string(), "-c".to_string(), demo_feed_script()],
        mounts: vec![BindMount::rw(out_dir, "/out")],
        // Engine defaults for the remaining knobs; `..Default::default()` stays
        // forward-compatible as jera grows the spec.
        ..Default::default()
    }
}

/// Where modgunn keeps its Tools-menu job ledger. `MODGUNN_JOBS` overrides; otherwise
/// `<temp>/modgunn_jobs.redb`. One redb file (the same thin `sink()` shape korp uses —
/// the move-to-edda candidate noted on this module).
pub fn ledger_path() -> PathBuf {
    if let Ok(p) = std::env::var("MODGUNN_JOBS") {
        return PathBuf::from(p);
    }
    std::env::temp_dir().join("modgunn_jobs.redb")
}

/// Open modgunn's durable job sink, best-effort. `jera`'s [`jera::jobs::JobStore::open`]
/// takes a *root dir* and manages `jobs.redb` under it, so we pass the ledger file's
/// parent. Returns [`jera::jobs::JobSink::noop`] if the ledger can't be opened so a
/// demo run never breaks on a missing writable path.
pub fn sink() -> jera::jobs::JobSink {
    let path = ledger_path();
    let root = path
        .parent()
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));
    match jera::jobs::JobStore::open(&root) {
        Ok(store) => store.sink(),
        Err(_) => jera::jobs::JobSink::noop(),
    }
}

/// **Run the demo advisory feed THROUGH jera as a tracked job** (routed THROUGH
/// draupnir's ONE OCI engine): bring the pinned container up, let it write the demo
/// OSV doc into `out_dir`, wait for it to exit, and record the terminal ledger row —
/// via [`jera::container::run_container`], NOT a bespoke docker call and NOT a
/// `thread::spawn`. After it returns, `out_dir` holds `MODGUNN-DEMO-0001.json` ready
/// for `modgunn warehouse import --osv <out_dir>`.
///
/// Only compiled with `framework-oci` (→ `jera/container-oci`, the live engine).
/// Without it there is nothing to spawn; the spec + its mapping
/// ([`ContainerSpec::to_draupnir`]) still compile + validate, which the tests assert.
#[cfg(feature = "framework-oci")]
pub fn run_demo_feed(out_dir: &str) -> anyhow::Result<jera::container::ContainerOutcome> {
    let spec = demo_feed_spec(out_dir);
    jera::container::run_container(sink(), &spec, DEMO_FEED_NAME, WORKSPACE)
}

/// The Tools-menu roster as JSON — the headless oracle a robot asserts (each entry's
/// effect key · the jera container it starts · the verb that views the result · the
/// demo image), plus the last effect picked. Proves the menu is complete + wired to
/// jera without a live daemon.
pub fn state_json(last_picked: Option<ToolEffect>) -> serde_json::Value {
    serde_json::json!({
        "workspace": WORKSPACE,
        "demo_feed_image": demo_feed_image(),
        "entries": entries().iter().map(|e| serde_json::json!({
            "effect": e.effect.key(),
            "needs_container": e.effect.needs_container(),
            "view_verb": e.view_verb,
        })).collect::<Vec<_>>(),
        "last_picked": last_picked.map(|e| e.key()),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    /// **RED-when-broken: the Tools menu is the jera-backed demo action.** The menu
    /// carries the demo advisory-feed entry, it names the jera container it starts,
    /// and it views the result through a real modgunn verb. If the entry lost its
    /// container or view verb (breaking the generate→import→scan chain), this is red.
    #[test]
    fn tools_menu_has_the_jera_demo_action() {
        let es = entries();
        assert_eq!(es.len(), 1);
        let demo = es
            .iter()
            .find(|e| e.effect == ToolEffect::DemoAdvisoryFeed)
            .unwrap();
        assert_eq!(demo.effect.needs_container(), DEMO_FEED_NAME);
        assert_eq!(demo.view_verb, "warehouse scan");
        assert_eq!(demo.effect.key(), "demo_advisory_feed");
    }

    /// **RED-when-broken: the demo feed is a jera `ContainerSpec` that maps + validates,
    /// and the image is PINNED (never `latest`).** The spec maps onto a draupnir spec
    /// that passes draupnir's own `validate()` — the "the wiring is real" gate — carries
    /// the demo name + the `/out` bind mount, and pins a reproducible image tag. A
    /// `:latest` default silently drifts across runs (reproducibility law). If the spec
    /// stopped validating, lost the bind mount, or drifted to `:latest`, this is red.
    #[test]
    fn demo_feed_spec_maps_validates_and_is_pinned() {
        let spec = demo_feed_spec("/tmp/modgunn-demo-out");
        assert_eq!(spec.name, DEMO_FEED_NAME);
        assert!(spec.image.contains("alpine"), "demo image: {}", spec.image);
        assert!(
            !DEMO_FEED_IMAGE_DEFAULT.ends_with(":latest"),
            "the demo image must be pinned, not :latest: {DEMO_FEED_IMAGE_DEFAULT}",
        );
        // The `/out` bind mount is present + read-write (where the OSV doc lands).
        assert_eq!(
            spec.bind_strings(),
            vec!["/tmp/modgunn-demo-out:/out".to_string()]
        );
        // Maps onto a draupnir container spec that VALIDATES (real jera→draupnir wiring).
        let d = spec.to_draupnir();
        d.validate()
            .expect("mapped demo-feed container spec is valid");

        crate::functional_status(
            "modgunn/tools",
            "demo_feed_spec_maps_validates_pinned",
            spec.image == DEMO_FEED_IMAGE_DEFAULT
                && !DEMO_FEED_IMAGE_DEFAULT.ends_with(":latest")
                && spec.bind_strings() == vec!["/tmp/modgunn-demo-out:/out".to_string()],
            "the Tools demo feed is a jera ContainerSpec (pinned image, /out bind) that maps + validates \
             onto draupnir's engine — started THROUGH jera, not a bespoke docker call",
        );
    }

    /// The `state_json` oracle carries the entry, the container it starts, the view
    /// verb, and the last pick — the surface a robot asserts after a menu click.
    #[test]
    fn state_json_carries_entry_and_last_pick() {
        let none = state_json(None);
        assert_eq!(none["entries"].as_array().unwrap().len(), 1);
        assert!(none["last_picked"].is_null());
        assert_eq!(none["workspace"], WORKSPACE);
        assert_eq!(none["entries"][0]["needs_container"], DEMO_FEED_NAME);
        assert_eq!(none["entries"][0]["view_verb"], "warehouse scan");

        let picked = state_json(Some(ToolEffect::DemoAdvisoryFeed));
        assert_eq!(picked["last_picked"], "demo_advisory_feed");
    }
}