memstead-cli 0.18.1

Command-line interface for Memstead — query and mutate typed entity graphs from the shell. Default build produces the full `memstead` binary (multi-mem, git-backed); `--no-default-features` builds the lean folder-only surface.
Documentation
//! `software@0.5.0` (backlog-decisions plan B6): a `contract` with
//! `protocol: engine_state` validates with and without `version_axes`
//! (the field is optional); a `version_axes` member that is not a
//! `name=constant` pair refuses `INVALID_FIELD_VALUE` naming the member
//! and the pattern; `spec` carries an optional `notes` section; and a
//! schema package whose `value_pattern` does not compile refuses at
//! `schema validate` naming the field.

use std::fs;

use assert_cmd::Command;
use tempfile::TempDir;

fn memstead() -> Command {
    Command::cargo_bin("memstead").expect("memstead binary must be built by cargo")
}

/// A folder workspace whose single mem pins the built-in `software@0.5.0`.
fn workspace() -> TempDir {
    let ws = TempDir::new().unwrap();
    memstead()
        .current_dir(ws.path())
        .args([
            "init",
            "--name",
            "code",
            "--schema",
            "software@0.5.0",
            "--quiet",
        ])
        .assert()
        .success();
    ws
}

fn contract(ws: &TempDir, title: &str, axes: Option<&str>) -> std::process::Output {
    let mut args: Vec<String> = vec![
        "create".into(),
        "--quiet".into(),
        "--type".into(),
        "contract".into(),
        "--title".into(),
        title.into(),
        "--section".into(),
        "summary=An engine-owned sidecar the next engine reads back.".into(),
        "--metadata".into(),
        "protocol=engine_state".into(),
        "--metadata".into(),
        "version=2".into(),
        "--metadata".into(),
        "stable_since=2026-09-02".into(),
        "--identity".into(),
        "author-one".into(),
        "--role".into(),
        "author".into(),
    ];
    if let Some(a) = axes {
        args.push("--metadata".into());
        args.push(format!("version_axes={a}"));
    }
    memstead()
        .current_dir(ws.path())
        .args(&args)
        .output()
        .unwrap()
}

/// B6 AC2 refusal complement, first half: `engine_state` with no
/// `version_axes` validates; a well-formed pair list validates too.
#[test]
fn an_engine_state_contract_validates_with_and_without_version_axes() {
    let ws = workspace();
    let out = contract(&ws, "Anchors sidecar", None);
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let out = contract(
        &ws,
        "Binding record",
        Some("BINDING_RECORD_VERSION=2,ANCHOR_SIDECAR_VERSION=2"),
    );
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let text = String::from_utf8_lossy(
        &memstead()
            .current_dir(ws.path())
            .args(["entity", "code--binding-record", "--quiet"])
            .output()
            .unwrap()
            .stdout,
    )
    .to_string();
    assert!(text.contains("engine_state"), "{text}");
    assert!(text.contains("ANCHOR_SIDECAR_VERSION=2"), "{text}");
}

/// B6 AC2 refusal complement, second half: a member that is not a
/// `name=constant` pair refuses `INVALID_FIELD_VALUE` naming it.
#[test]
fn a_malformed_version_axis_refuses_invalid_field_value() {
    let ws = workspace();
    let out = contract(
        &ws,
        "Broken axes",
        Some("ANCHOR_SIDECAR_VERSION=2,just a note"),
    );
    assert!(!out.status.success());
    let text =
        String::from_utf8_lossy(&out.stderr).to_string() + &String::from_utf8_lossy(&out.stdout);
    assert!(text.contains("INVALID_FIELD_VALUE"), "{text}");
    assert!(text.contains("just a note"), "{text}");
    assert!(text.contains("version_axes"), "{text}");
    assert!(text.contains("pattern"), "{text}");
}

/// `spec` gains an optional Notes section: a spec with the historical
/// marker in Notes and a one-sentence Identity validates.
#[test]
fn a_spec_carries_its_standing_remark_in_notes() {
    let ws = workspace();
    memstead()
        .current_dir(ws.path())
        .args([
            "create",
            "--quiet",
            "--type",
            "spec",
            "--title",
            "Pipeline store",
            "--section",
            "identity=The gen-1 pipeline store that persisted four primitives to the workspace.",
            "--section",
            "purpose=Kept for its citations from the knowledge mem.",
            "--section",
            "notes=Historical record: migrated into v2 single-record bindings on 2026-08-23; stability frozen.",
            "--metadata",
            "stability=frozen",
            "--identity",
            "author-one",
            "--role",
            "author",
        ])
        .assert()
        .success();
}

/// A package whose `value_pattern` does not compile refuses at
/// `schema validate` naming the type, the field and the pattern.
#[test]
fn a_value_pattern_that_does_not_compile_refuses_at_validate() {
    let ws = TempDir::new().unwrap();
    let pkg = ws.path().join("bad");
    fs::create_dir_all(pkg.join("types")).unwrap();
    fs::write(
        pkg.join("schema.yaml"),
        "name: bad\nversion: 0.1.0\ndescription: pattern fixture\nwhen_to_use: tests\ntypes:\n  - item\nrelationships:\n  mode: strict\n  definitions:\n    - name: _default\n      description: fallback\n      default_weight: 1.0\ncommunity:\n  resolution: 1.0\n  seed: 42\n",
    )
    .unwrap();
    fs::write(
        pkg.join("types").join("item.yaml"),
        "name: item\ndescription: an item\nwhen_to_use: Here\nsections:\n  - key: body\n    heading: Body\n    required: true\n    search_weight: 10.0\n    catch_all: true\n    write_rules: []\nmetadata_fields:\n  - key: axes\n    description: pairs\n    field_type: string\n    serialization: csv_array\n    value_pattern: \"[A-Z\"\ntitle_weight: 100.0\ntext_fields:\n  - body\nhierarchy_relationship: _default\nno_self_loop_relationships: []\nupdatable_fields:\n  - title\n  - body\n  - axes\nhealth_required_fields:\n  - body\nstaleness_threshold_days: 90\nwrite_rules: []\n",
    )
    .unwrap();
    let out = memstead()
        .current_dir(ws.path())
        .args(["schema", "validate", pkg.to_str().unwrap(), "--quiet"])
        .output()
        .unwrap();
    assert!(!out.status.success());
    let text =
        String::from_utf8_lossy(&out.stderr).to_string() + &String::from_utf8_lossy(&out.stdout);
    assert!(text.contains("value_pattern"), "{text}");
    assert!(text.contains("axes"), "{text}");
    assert!(text.contains("does not compile"), "{text}");
}