pushkin-core 0.2.1

Core envelope, manifest, pipeline, and waiver types for the pushkin write-gate
Documentation
//! `[floor]` — the declared mechanical floor (spec §8.2 stage 5).
//!
//! Authorized by `docs/claude_stage5-floor-verb-charter-2026-08-18.md`
//! (Workstream A step 1 of `docs/charters/2026-08-18-coding-standards-stream.md`,
//! RATIFIED 2026-08-18). The schema is written out in full in that brief, which
//! is what N13 requires of a manifest schema change: the human ratified a
//! spelled-out table, not a direction.
//!
//! A NEW file per N10. No committed suite is touched by this pass.
//!
//! **Why the manifest owns the floor at all.** Stage 5 must "mirror CI commands
//! exactly," and at the head of this pass there was no single sequence to
//! mirror: `Makefile`'s `floor` ran clippy with `--all-features` and `ci.yml`
//! did not, `cargo deny` ran by two different mechanisms, and `ci.yml` never ran
//! `bun test` or `tsc` at all. So the first deliverable is the floor
//! *definition* — one committed table every surface reads — not an orchestrator
//! over an undefined one.
//!
//! **What `scope` and `inputs` are for.** Neither changes execution in this
//! pass: everything runs whole-repo. They are DECLARED rather than inferred
//! because the alternative is a tool guessing at decomposability, which is the
//! Nx/Turborepo/Bazel lesson. `scope` is the contract a future warm charter
//! reads; `inputs` is what makes `cargo deny`'s RustSec-DB dependency a
//! disclosed fact instead of a surprise (F69's rider — an unchanged commit can
//! newly fail when an advisory publishes).
//!
//! **Deliberately NOT in scope here.** The parser owns the SHAPE of the table
//! only. The absent-`[floor]` case is deliberately NOT a parse error — a repo
//! without a declared floor is a valid manifest, and it is the verb, not the
//! parser, that has to refuse to run. That split is asserted below so a later
//! change cannot quietly move it.

use pushkin_core::manifest::Manifest;

const BASE: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"

[gates]
"#;

fn parse(floor: &str) -> Result<Manifest, pushkin_core::manifest::ManifestError> {
    Manifest::parse(&format!("{BASE}{floor}"))
}

/// The full table, every field exercised at least once.
const FULL: &str = r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt", "--check"]
scope = "per_file"
inputs = "toolchain"

[[floor.commands]]
name = "test"
run = ["cargo", "test", "--workspace"]
scope = "whole_repo"
inputs = "repo"
reconcile_ignored = true

[[floor.commands]]
name = "bench"
run = ["cargo", "test", "--release", "-p", "pushkin"]
scope = "whole_repo"
inputs = "toolchain"
covers_ignored_of = "test"

[[floor.commands]]
name = "deny"
run = ["cargo", "deny", "check"]
scope = "whole_repo"
inputs = "network"
install = "cargo install --locked cargo-deny"
on_stop = true
"#;

#[test]
fn the_full_table_parses_and_preserves_declared_order() {
    let manifest = parse(FULL).expect("full table must parse");
    let floor = manifest.floor.as_ref().expect("[floor] must be present");
    let names: Vec<&str> = floor.commands.iter().map(|c| c.name.as_str()).collect();
    // Order is load-bearing: the verb runs commands in declared order, and a
    // reordering parser would silently change which findings a reader sees first.
    assert_eq!(names, vec!["fmt", "test", "bench", "deny"]);
}

#[test]
fn every_declared_field_round_trips() {
    let manifest = parse(FULL).expect("full table must parse");
    let floor = manifest.floor.as_ref().expect("[floor] must be present");

    let fmt = &floor.commands[0];
    assert_eq!(fmt.run, vec!["cargo", "fmt", "--check"]);
    assert_eq!(fmt.scope, pushkin_core::manifest::FloorScope::PerFile);
    assert_eq!(fmt.inputs, pushkin_core::manifest::FloorInputs::Toolchain);
    assert!(fmt.install.is_none());

    let deny = &floor.commands[3];
    assert_eq!(deny.inputs, pushkin_core::manifest::FloorInputs::Network);
    assert_eq!(
        deny.install.as_deref(),
        Some("cargo install --locked cargo-deny")
    );
    assert!(deny.on_stop);
}

#[test]
fn on_stop_and_reconcile_ignored_default_to_false() {
    let manifest = parse(FULL).expect("full table must parse");
    let floor = manifest.floor.as_ref().expect("[floor] must be present");
    let fmt = &floor.commands[0];
    // Both defaults ship the conservative posture: a command is not run at Stop
    // and is not ignored-accounted unless the manifest positively says so.
    assert!(!fmt.on_stop, "on_stop must default to false");
    assert!(
        !fmt.reconcile_ignored,
        "reconcile_ignored must default to false"
    );
    assert!(floor.commands[1].reconcile_ignored);
}

#[test]
fn an_absent_floor_table_parses_because_the_verb_owns_that_error() {
    // A repo with no declared floor is a VALID manifest. `pushkin floor` refuses
    // to run against it with a named error; the parser must not pre-empt that,
    // or every other verb breaks on a manifest that was never wrong.
    let manifest = Manifest::parse(BASE).expect("a manifest without [floor] must parse");
    assert!(manifest.floor.is_none());
}

#[test]
fn an_empty_commands_list_parses_and_is_the_verbs_problem_too() {
    let manifest = parse("[floor]\ncommands = []\n").expect("empty commands must parse");
    let floor = manifest.floor.as_ref().expect("[floor] must be present");
    assert!(floor.commands.is_empty());
}

// ---------- required fields: each omission fails, naming the field ----------

#[test]
fn a_missing_name_is_rejected_naming_the_field() {
    let error = parse(
        r#"
[[floor.commands]]
run = ["cargo", "fmt"]
scope = "whole_repo"
inputs = "repo"
"#,
    )
    .expect_err("a command without a name must be rejected");
    assert!(
        error.to_string().contains("name"),
        "the error must name the missing field, got: {error}"
    );
}

#[test]
fn a_missing_run_is_rejected_naming_the_field() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
scope = "whole_repo"
inputs = "repo"
"#,
    )
    .expect_err("a command without run must be rejected");
    assert!(
        error.to_string().contains("run"),
        "the error must name the missing field, got: {error}"
    );
}

#[test]
fn a_missing_scope_is_rejected_naming_the_field() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt"]
inputs = "repo"
"#,
    )
    .expect_err("a command without scope must be rejected");
    assert!(
        error.to_string().contains("scope"),
        "the error must name the missing field, got: {error}"
    );
}

#[test]
fn a_missing_inputs_is_rejected_naming_the_field() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt"]
scope = "whole_repo"
"#,
    )
    .expect_err("a command without inputs must be rejected");
    assert!(
        error.to_string().contains("inputs"),
        "the error must name the missing field, got: {error}"
    );
}

// ---------- loud rejections ----------

#[test]
fn an_unknown_key_inside_a_command_is_rejected() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt"]
scope = "whole_repo"
inputs = "repo"
timeout_ms = 5000
"#,
    )
    .expect_err("an unknown key must be rejected (deny_unknown_fields)");
    assert!(
        error.to_string().contains("timeout_ms"),
        "the error must name the offending key, got: {error}"
    );
}

#[test]
fn an_unknown_key_on_the_floor_table_is_rejected() {
    let error = parse("[floor]\ncommands = []\nparallel = true\n")
        .expect_err("an unknown key on [floor] must be rejected");
    assert!(
        error.to_string().contains("parallel"),
        "the error must name the offending key, got: {error}"
    );
}

#[test]
fn a_duplicate_name_is_rejected_naming_the_duplicate() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt"]
scope = "whole_repo"
inputs = "repo"

[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt", "--check"]
scope = "whole_repo"
inputs = "repo"
"#,
    )
    .expect_err("a duplicate command name must be rejected");
    let message = error.to_string();
    assert!(
        message.contains("fmt") && message.contains("duplicate"),
        "the error must name the duplicated entry, got: {message}"
    );
}

#[test]
fn an_empty_run_array_is_rejected_naming_the_command() {
    let error = parse(
        r#"
[[floor.commands]]
name = "nothing"
run = []
scope = "whole_repo"
inputs = "repo"
"#,
    )
    .expect_err("an empty run array must be rejected");
    let message = error.to_string();
    assert!(
        message.contains("nothing"),
        "the error must name the offending entry, got: {message}"
    );
}

#[test]
fn an_unknown_scope_value_is_rejected() {
    let error = parse(
        r#"
[[floor.commands]]
name = "fmt"
run = ["cargo", "fmt"]
scope = "per_line"
inputs = "repo"
"#,
    )
    .expect_err("an unknown scope must be rejected");
    assert!(
        error.to_string().contains("per_line"),
        "the error must name the offending value, got: {error}"
    );
}

#[test]
fn covers_ignored_of_naming_an_undeclared_command_is_rejected() {
    // The declared link is what W3's ignored-test accounting checks. A dangling
    // reference would make the accounting silently vacuous — the exact shape of
    // the defect `scripts/floor.sh` was written to prevent (D7(a)).
    let error = parse(
        r#"
[[floor.commands]]
name = "bench"
run = ["cargo", "test", "--release"]
scope = "whole_repo"
inputs = "toolchain"
covers_ignored_of = "no_such_command"
"#,
    )
    .expect_err("a dangling covers_ignored_of must be rejected");
    let message = error.to_string();
    assert!(
        message.contains("no_such_command"),
        "the error must name the dangling reference, got: {message}"
    );
}

#[test]
fn covers_ignored_of_resolves_regardless_of_declared_order() {
    // The coverer is declared BEFORE the command it covers. Resolution is by
    // name across the whole table, not a forward-only scan — otherwise the
    // reference would be order-dependent and the error message a lie.
    let manifest = parse(
        r#"
[[floor.commands]]
name = "bench"
run = ["cargo", "test", "--release"]
scope = "whole_repo"
inputs = "toolchain"
covers_ignored_of = "test"

[[floor.commands]]
name = "test"
run = ["cargo", "test", "--workspace"]
scope = "whole_repo"
inputs = "repo"
reconcile_ignored = true
"#,
    )
    .expect("a backward reference must resolve");
    let floor = manifest.floor.as_ref().expect("[floor] must be present");
    assert_eq!(floor.commands[0].covers_ignored_of.as_deref(), Some("test"));
}

#[test]
fn a_command_may_not_cover_its_own_ignored_tests() {
    // Self-coverage would satisfy the accounting arithmetic while executing
    // nothing new — a vacuous green.
    let error = parse(
        r#"
[[floor.commands]]
name = "test"
run = ["cargo", "test", "--workspace"]
scope = "whole_repo"
inputs = "repo"
reconcile_ignored = true
covers_ignored_of = "test"
"#,
    )
    .expect_err("self-coverage must be rejected");
    assert!(
        error.to_string().contains("test"),
        "the error must name the offending entry, got: {error}"
    );
}