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");
}
}