Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/src/compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -2599,7 +2599,9 @@ $ prism index --diff before.json after.json
Changed Lib.base
```

The viewer accepts the older artifact through `?diff=<url>` and renders changed definitions side by side. Authored changes lead the review; cosmetic and dependency-cone changes remain available without overwhelming the primary diff.
The artifact carries both revisions of every definition that moved, and the edges one revision has and the other does not, so a consumer holding the new index recovers the old dependency graph exactly without a second index.

The viewer accepts the older artifact through `?diff=<url>` and shows every field of a changed definition as a diff against the other revision: the body line by line with the words that moved marked inside an edited line, the signature, effect row and docstring the same way, the claims, visibility and deprecation tags as what left and what arrived, and each relation row (callers, calls, tests, uses, members) as the set it was against the set it is. The status line names which fields moved. A page-wide control lays the pairs out split (old beside new) or unified (old above new), remembered across visits, and any card can choose its own layout. Authored changes lead the review; cosmetic and dependency-cone changes remain available without overwhelming the primary diff.

### 30.3 Review State and Questions {#review-state}

Expand Down
45 changes: 44 additions & 1 deletion src/index/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::collections::{BTreeMap, BTreeSet};

use serde::{Deserialize, Serialize};

use super::{Def, Index};
use super::{Def, Edge, Index};

/// Schema tag for the diff artifact.
pub const INDEX_DIFF_FORMAT: &str = "prism-index-diff-v1";
Expand Down Expand Up @@ -119,6 +119,28 @@ pub struct DiffEnvelope {
pub counts: Counts,
}

/// The edges one revision has and the other does not.
///
/// The entries carry each changed definition's two records, which is enough to
/// show its two bodies but not its two *neighbourhoods*: who called it before,
/// what it called, which tests reached it. Those are edges, and an edge's other
/// end is very often an untouched definition the entry list omits. Carrying the
/// whole old edge set would repeat the index; carrying the difference is a few
/// rows per edit, and a consumer that has the new index recovers the old edge
/// set exactly as `new − added + removed`.
///
/// Always present, even when empty, so a consumer can tell "nothing moved" from
/// an artifact written before the delta existed.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct EdgeDelta {
/// In the new revision only. Sorted like an index's edge list.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub added: Vec<Edge>,
/// In the old revision only.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub removed: Vec<Edge>,
}

/// The diff artifact.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct IndexDiff {
Expand All @@ -127,6 +149,11 @@ pub struct IndexDiff {
/// name. Untouched definitions are omitted rather than listed: a review view
/// wants what moved, and the count is in the envelope.
pub entries: Vec<Entry>,
/// What moved in the dependency graph. Absent only in an artifact older
/// than the field, which a consumer should treat as "unknown" rather than
/// "nothing".
#[serde(default)]
pub edges: Option<EdgeDelta>,
}

impl IndexDiff {
Expand Down Expand Up @@ -277,6 +304,21 @@ pub fn diff(old: &Index, new: &Index) -> Result<IndexDiff, String> {
// within each. A reviewer reads this top to bottom.
entries.sort_by(|a, b| a.status.cmp(&b.status).then_with(|| a.id.cmp(&b.id)));

// Both edge lists are sorted and deduplicated, so the two differences are
// set differences and come out in the same order.
let old_edges: BTreeSet<&Edge> = old.edges.iter().collect();
let new_edges: BTreeSet<&Edge> = new.edges.iter().collect();
let edges = EdgeDelta {
added: new_edges
.difference(&old_edges)
.map(|e| (*e).clone())
.collect(),
removed: old_edges
.difference(&new_edges)
.map(|e| (*e).clone())
.collect(),
};

Ok(IndexDiff {
envelope: DiffEnvelope {
format: INDEX_DIFF_FORMAT.to_string(),
Expand All @@ -296,6 +338,7 @@ pub fn diff(old: &Index, new: &Index) -> Result<IndexDiff, String> {
counts,
},
entries,
edges: Some(edges),
})
}

Expand Down
53 changes: 53 additions & 0 deletions src/index/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,59 @@ fn a_revision_against_itself_has_no_entries() {
let d = super::diff(&index, &index).expect("comparable schemes");
assert!(d.entries.is_empty());
assert_eq!(d.envelope.counts.unchanged, index.defs.len());
// The edge delta is carried even when empty: "nothing moved" is a fact, and
// must read differently from an artifact that never recorded edges at all.
let edges = d.edges.as_ref().expect("delta present");
assert!(edges.added.is_empty() && edges.removed.is_empty());
let json = d.to_json().expect("serialize");
assert!(json.contains("\"edges\": {}"), "{json}");
}

// The entries say what a definition's text became; the edge delta says what
// its neighbourhood became, which the entries cannot: the other end of an edge
// is usually an untouched definition the entry list omits. A consumer holding
// the new index recovers the old edge set as `new − added + removed`.
#[test]
fn the_diff_carries_the_edges_that_moved() {
use super::Edge;
let old = index_of(REV_OLD);
// `top` stops calling `mid` and calls `spare` instead.
let new = index_of(&REV_OLD.replace("mid(n) + mid(n)", "spare(n) + spare(n)"));
let d = super::diff(&old, &new).expect("comparable schemes");
let edges = d.edges.as_ref().expect("delta present");
let calls = |from: &str, to: &str| Edge {
kind: EdgeKind::Calls,
from: from.into(),
to: to.into(),
};
assert!(edges.removed.contains(&calls("top", "mid")), "{edges:?}");
assert!(edges.added.contains(&calls("top", "spare")), "{edges:?}");
// Exactly the set difference, in both directions.
let old_set: BTreeSet<&Edge> = old.edges.iter().collect();
let new_set: BTreeSet<&Edge> = new.edges.iter().collect();
let recovered: BTreeSet<&Edge> = new_set
.iter()
.copied()
.filter(|e| !edges.added.contains(e))
.chain(edges.removed.iter())
.collect();
assert_eq!(recovered, old_set);
// And the delta survives the artifact boundary; an artifact without one
// still reads, as unknown rather than as empty.
let back = super::IndexDiff::from_json(&d.to_json().expect("serialize")).expect("reads back");
assert_eq!(back.edges, d.edges);
let without = d.to_json().expect("serialize");
let without = without.replace(
&format!(
",\n \"edges\": {}",
serde_json::to_string_pretty(edges)
.expect("serialize")
.replace('\n', "\n ")
),
"",
);
let older = super::IndexDiff::from_json(&without).expect("reads back");
assert!(older.edges.is_none(), "{without}");
}

// The artifact is the input to a `--check` gate, so identical source must yield
Expand Down
Loading
Loading