pushkin-core 0.2.0

Core envelope, manifest, pipeline, and waiver types for the pushkin write-gate
Documentation
//! F48 Phase B — the ANCHORED half of edit application, pinned at the pure
//! layer where `edits.rs` says its rules belong.
//!
//! A NEW file per N10; `edit_application.rs` continues to pin the unanchored
//! rules and is untouched by this suite.
//!
//! **The property every test here defends.** An anchor NARROWS where a target
//! is looked for. It never relaxes a rule. A target absent from its anchored
//! lines is not found — even when it sits plainly elsewhere in the file —
//! because the family told us where it was and it is not there. Letting the
//! search escape its anchor would silently reintroduce exactly the guess the
//! anchors exist to remove.

use pushkin_core::edits::{apply_edits, EditError, LineSpan, Replacement};

/// Line 1 and line 3 are IDENTICAL, which is what makes anchors load-bearing:
/// every unanchored edit naming that text is ambiguous.
const CONTENT: &str = "let a = 1;\nlet b = 2;\nlet a = 1;\n";

fn anchored(old: &str, new: &str, start: usize, end: usize) -> Replacement {
    Replacement {
        old: old.to_owned(),
        new: new.to_owned(),
        replace_all: false,
        anchor: Some(LineSpan { start, end }),
    }
}

fn plain(old: &str, new: &str) -> Replacement {
    Replacement {
        old: old.to_owned(),
        new: new.to_owned(),
        replace_all: false,
        anchor: None,
    }
}

// ---------------------------------------------------------------------------
// What the anchor buys
// ---------------------------------------------------------------------------

#[test]
fn an_anchored_edit_replaces_inside_its_range() {
    let out = apply_edits(CONTENT, &[anchored("let b = 2;", "let b = 9;", 2, 2)]);

    assert_eq!(out.unwrap(), "let a = 1;\nlet b = 9;\nlet a = 1;\n");
}

/// The point of the auggie arm. Unanchored, this target occurs twice and must
/// be refused; anchored, it occurs once and resolves — and resolves to the
/// occurrence the anchor names, not the first one in the file.
#[test]
fn a_repeated_target_is_unambiguous_inside_its_anchor() {
    let out = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 3, 3)]);

    assert_eq!(
        out.unwrap(),
        "let a = 1;\nlet b = 2;\nlet a = 7;\n",
        "the THIRD line was anchored; replacing the first would be a fabricated file"
    );
}

#[test]
fn the_same_target_unanchored_is_still_refused_as_ambiguous() {
    let error = apply_edits(CONTENT, &[plain("let a = 1;", "let a = 7;")]).unwrap_err();

    assert!(
        matches!(error, EditError::NotUnique { count: 2, .. }),
        "{error:?}"
    );
}

#[test]
fn an_anchor_may_span_several_lines() {
    let out = apply_edits(CONTENT, &[anchored("= 1;\nlet b", "= 4;\nlet b", 1, 2)]);

    assert_eq!(out.unwrap(), "let a = 4;\nlet b = 2;\nlet a = 1;\n");
}

// ---------------------------------------------------------------------------
// What the anchor must NOT do — narrow, never relax
// ---------------------------------------------------------------------------

/// The load-bearing negative. The target exists on lines 1 and 3; the anchor
/// says line 2. A search that escaped its anchor would happily edit line 1 and
/// report success on a file the caller never described.
#[test]
fn a_target_outside_its_anchor_is_not_found_even_though_it_exists() {
    let error = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 2, 2)]).unwrap_err();

    assert!(matches!(error, EditError::NotFound { .. }), "{error:?}");
}

#[test]
fn a_target_twice_inside_one_anchor_is_still_ambiguous() {
    let error = apply_edits(CONTENT, &[anchored("let a = 1;", "let a = 7;", 1, 3)]).unwrap_err();

    assert!(
        matches!(error, EditError::NotUnique { count: 2, .. }),
        "an anchor wide enough to contain both occurrences resolves nothing: {error:?}"
    );
}

// ---------------------------------------------------------------------------
// Anchors that describe a different file
// ---------------------------------------------------------------------------

#[test]
fn an_anchor_past_the_last_line_is_refused() {
    let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 3, 9)]).unwrap_err();

    assert_eq!(
        error,
        EditError::AnchorOutOfRange {
            start: 3,
            end: 9,
            lines: 3
        }
    );
}

#[test]
fn a_zero_start_is_refused_because_line_numbering_is_one_based() {
    let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 0, 1)]).unwrap_err();

    assert!(
        matches!(error, EditError::AnchorOutOfRange { .. }),
        "{error:?}"
    );
}

#[test]
fn an_inverted_range_is_refused() {
    let error = apply_edits(CONTENT, &[anchored("let a = 1;", "x", 3, 1)]).unwrap_err();

    assert!(
        matches!(error, EditError::AnchorOutOfRange { .. }),
        "{error:?}"
    );
}

// ---------------------------------------------------------------------------
// The coordinate question the capture could not settle
// ---------------------------------------------------------------------------

/// Once a preceding edit changes the line count, a later anchor is ambiguous
/// between two readings — original-file coordinates, or coordinates in the file
/// as previous edits left it — and the capture distinguishes neither. Refuse.
#[test]
fn an_anchored_edit_after_a_line_count_change_is_refused() {
    let error = apply_edits(
        CONTENT,
        &[
            plain("let b = 2;", "let b = 2;\nlet c = 3;"),
            anchored("let a = 1;", "let a = 7;", 3, 3),
        ],
    )
    .unwrap_err();

    assert_eq!(error, EditError::AnchorShifted);
}

/// The refusal is scoped to what is actually ambiguous. An edit that preserves
/// the line count leaves every later anchor meaning the same thing under both
/// readings, so it must still apply.
#[test]
fn an_anchored_edit_after_a_line_count_preserving_change_still_applies() {
    let out = apply_edits(
        CONTENT,
        &[
            plain("let b = 2;", "let b = 9;"),
            anchored("let a = 1;", "let a = 7;", 3, 3),
        ],
    );

    assert_eq!(out.unwrap(), "let a = 1;\nlet b = 9;\nlet a = 7;\n");
}

/// And an UNANCHORED edit is unaffected by a line-count change: it never
/// depended on coordinates, so there is nothing ambiguous about it.
#[test]
fn an_unanchored_edit_after_a_line_count_change_still_applies() {
    let out = apply_edits(
        CONTENT,
        &[
            anchored("let a = 1;", "let a = 1;\nlet c = 3;", 1, 1),
            plain("let b = 2;", "let b = 9;"),
        ],
    );

    assert_eq!(
        out.unwrap(),
        "let a = 1;\nlet c = 3;\nlet b = 9;\nlet a = 1;\n"
    );
}

/// Atomicity holds across the anchored path too: a later failure discards the
/// earlier edit rather than returning a half-applied file no editor would
/// produce.
#[test]
fn a_later_anchor_failure_discards_the_whole_application() {
    let error = apply_edits(
        CONTENT,
        &[
            anchored("let b = 2;", "let b = 9;", 2, 2),
            anchored("nowhere", "x", 1, 1),
        ],
    );

    assert!(error.is_err(), "the application must not partially succeed");
}