From aba5fef470130a9c7a16907dc97ef5aa4328f21c Mon Sep 17 00:00:00 2001 From: forhappy Date: Fri, 14 Aug 2026 15:20:35 -0700 Subject: [PATCH 1/4] Fix legacy review fingerprint upgrades --- CHANGELOG.md | 5 +++ COMPATIBILITY.md | 12 ++++--- crates/compass-cli/src/history_build.rs | 47 ++++++++++++++++++++++--- crates/compass-cli/tests/review_cli.rs | 6 ++-- docs/guides/versioned-history.md | 11 +++--- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05ec1497..514ea432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Allow `compass review` on `0.3.x` to rebuild comparable realizations from + build profiles persisted by Compass `0.1.10` or later. The upgrade preserves + user-selected options, refreshes engine-owned fingerprint fields, and keeps + the original historical realization immutable. + ## 0.3.13 - 2026-08-14 - Make `compass review` recover automatically when one compared revision still diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 73be20bf..9208462b 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -330,11 +330,13 @@ integer rubric is version 1; each deterministic gate has its own rule version. Presentation formats and the reusable GitHub Action consume this report and do not redefine its semantics. -When only one side of a review has a preferred realization from an older patch -release in the same supported `0.3.x` line, Compass advances engine-owned -profile fields and materializes both revisions with the running binary. The -older realization remains immutable and queryable. Newer profiles and profiles -from another release line still fail explicitly rather than being downgraded. +When only one side of a review has a preferred realization whose build profile +was persisted by Compass `0.1.10` or later, the `0.3.x` line advances +engine-owned profile fields and materializes both revisions with the running +binary. User-selected build options are preserved, while the older realization +remains immutable and queryable. Profiles older than `0.1.10`, newer than the +running binary, or targeting a future release line still fail explicitly rather +than being guessed or downgraded. Dependency findings in `compass.semantic_diff.report/1` may now carry the optional strict `dependency_topology` object. It records source/target community diff --git a/crates/compass-cli/src/history_build.rs b/crates/compass-cli/src/history_build.rs index 78ca60c4..c0bd9557 100644 --- a/crates/compass-cli/src/history_build.rs +++ b/crates/compass-cli/src/history_build.rs @@ -15,6 +15,13 @@ use compass_history::{ HISTORY_GRAPH_SCHEMA, HistoryError, MAX_DIAGNOSTIC_BYTES, }; +// Compass 0.1.10 is the earliest released build-profile shape qualified for +// current-option reconstruction. Pin the target minor as well: before 1.0, a +// future minor must re-evaluate this migration instead of inheriting it. +const COMPATIBLE_PROFILE_UPGRADE_FLOOR: semver::Version = semver::Version::new(0, 1, 10); +const COMPATIBLE_PROFILE_TARGET_MAJOR: u64 = 0; +const COMPATIBLE_PROFILE_TARGET_MINOR: u64 = 3; + #[derive(Clone, Debug)] pub(crate) struct HistoryBuildOptions { profile: BuildProfile, @@ -128,10 +135,7 @@ impl HistoryBuildOptions { "running compass_version is not a semantic version".to_owned(), ) })?; - if persisted.major != current.major - || persisted.minor != current.minor - || persisted >= current - { + if !is_compatible_profile_upgrade(&persisted, ¤t) { return Err(HistoryError::InvalidFingerprint(format!( "persisted compass_version {persisted} cannot be upgraded by {}", env!("CARGO_PKG_VERSION") @@ -300,6 +304,13 @@ impl HistoryBuildOptions { } } +fn is_compatible_profile_upgrade(persisted: &semver::Version, current: &semver::Version) -> bool { + current.major == COMPATIBLE_PROFILE_TARGET_MAJOR + && current.minor == COMPATIBLE_PROFILE_TARGET_MINOR + && persisted >= &COMPATIBLE_PROFILE_UPGRADE_FLOOR + && persisted < current +} + fn insert_current_engine_profile( profile: &mut BuildProfile, deep: bool, @@ -1622,6 +1633,34 @@ mod tests { Ok(()) } + #[test] + fn compatible_0_1_10_profiles_advance_engine_fields_and_preserve_user_options() + -> Result<(), Box> { + let mut profile = HistoryBuildOptions::defaults()?.profile(); + profile.insert("compass_version", "0.1.10")?; + profile.insert("pipeline_version", "compass-core/older")?; + profile.insert("resolution", "2")?; + + let upgraded = HistoryBuildOptions::from_compatible_profile(profile)?.profile(); + + assert_eq!( + upgraded.value("compass_version"), + Some(env!("CARGO_PKG_VERSION")) + ); + assert_eq!(upgraded.value("pipeline_version"), Some("compass-core/v1")); + assert_eq!(upgraded.value("resolution"), Some("2")); + + let below_floor = semver::Version::new(0, 1, 9); + let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))?; + assert!(!is_compatible_profile_upgrade(&below_floor, ¤t)); + assert!(!is_compatible_profile_upgrade(¤t, ¤t)); + assert!(!is_compatible_profile_upgrade( + &semver::Version::new(1, 0, 0), + ¤t + )); + Ok(()) + } + #[test] fn diagnostics_never_exceed_the_documented_limit() -> Result<(), Box> { let bytes = vec![b'x'; MAX_DIAGNOSTIC_BYTES + 1024]; diff --git a/crates/compass-cli/tests/review_cli.rs b/crates/compass-cli/tests/review_cli.rs index 8b90cc19..70807b46 100644 --- a/crates/compass-cli/tests/review_cli.rs +++ b/crates/compass-cli/tests/review_cli.rs @@ -48,7 +48,7 @@ fn publish_historical_base(root: &Path, commit: &str) -> Result<(), Box Result<(), Box Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; @@ -182,7 +182,7 @@ fn local_review_rebuilds_a_comparable_pair_after_a_compass_patch_upgrade() Some(env!("CARGO_PKG_VERSION")) ); assert!(history.list(Some(&base))?.iter().any(|realization| { - realization.version.build_profile.value("compass_version") == Some("0.3.9") + realization.version.build_profile.value("compass_version") == Some("0.1.10") })); Ok(()) } diff --git a/docs/guides/versioned-history.md b/docs/guides/versioned-history.md index c7978616..8ea73efe 100644 --- a/docs/guides/versioned-history.md +++ b/docs/guides/versioned-history.md @@ -355,11 +355,12 @@ There is no profile-mismatch override: unlike profiles do not produce a semantic or exact report. Compass checks graph-engine compatibility explicitly before comparing the complete build profiles. -`compass review` handles an older preferred realization from a compatible -`0.3.x` patch release automatically when it must materialize the other side. -It preserves user-selected profile options, rebuilds a current-version pair, -and leaves the older immutable realization intact. A profile from a newer -binary or another release line remains an explicit compatibility error. +`compass review` handles an older preferred realization with a compatible +profile from Compass `0.1.10` or later automatically when it must materialize +the other side. It preserves user-selected profile options, rebuilds a +current-version pair, and leaves the older immutable realization intact. A +profile older than `0.1.10`, newer than the running binary, or from another +release line remains an explicit compatibility error. ## 7. Export a realization From dced5ca72e8726c833e8c8ae8e775fe135ca2980 Mon Sep 17 00:00:00 2001 From: forhappy Date: Fri, 14 Aug 2026 15:33:06 -0700 Subject: [PATCH 2/4] Reconcile compatible review profiles --- CHANGELOG.md | 7 +- COMPATIBILITY.md | 14 +- crates/compass-cli/src/history_commands.rs | 42 +++++- crates/compass-cli/tests/review_cli.rs | 158 ++++++++++++++++++++- docs/guides/versioned-history.md | 14 +- 5 files changed, 211 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 514ea432..a72eb366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,10 @@ ## Unreleased - Allow `compass review` on `0.3.x` to rebuild comparable realizations from - build profiles persisted by Compass `0.1.10` or later. The upgrade preserves - user-selected options, refreshes engine-owned fingerprint fields, and keeps - the original historical realization immutable. + repository profiles and preferred realizations persisted by Compass `0.1.10` + or later, including when both compared revisions are already materialized. + The upgrade preserves matching user-selected options, refreshes engine-owned + fingerprint fields, and keeps original historical realizations immutable. ## 0.3.13 - 2026-08-14 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 9208462b..8dca734e 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -330,13 +330,15 @@ integer rubric is version 1; each deterministic gate has its own rule version. Presentation formats and the reusable GitHub Action consume this report and do not redefine its semantics. -When only one side of a review has a preferred realization whose build profile -was persisted by Compass `0.1.10` or later, the `0.3.x` line advances +When either side of a review has a preferred realization or repository history +profile persisted by Compass `0.1.10` or later, the `0.3.x` line advances engine-owned profile fields and materializes both revisions with the running -binary. User-selected build options are preserved, while the older realization -remains immutable and queryable. Profiles older than `0.1.10`, newer than the -running binary, or targeting a future release line still fail explicitly rather -than being guessed or downgraded. +binary. This also applies when both revisions already have preferred +realizations. Reconciliation proceeds only when their user-selected options are +identical after engine advancement. Older realizations remain immutable and +queryable. Profiles older than `0.1.10`, newer than the running binary, +targeting a future release line, or retaining different user options still fail +explicitly rather than being guessed or downgraded. Dependency findings in `compass.semantic_diff.report/1` may now carry the optional strict `dependency_topology` object. It records source/target community diff --git a/crates/compass-cli/src/history_commands.rs b/crates/compass-cli/src/history_commands.rs index 7b806c07..a5453623 100644 --- a/crates/compass-cli/src/history_commands.rs +++ b/crates/compass-cli/src/history_commands.rs @@ -144,10 +144,14 @@ pub(crate) fn resolve_or_materialize( rebuild: bool, replace_corrupt: bool, ) -> Result<(HistoryStore, PublishedVersion), String> { + let requested_profile = options.profile(); let existing = HistoryStore::open_existing(repository).map_err(|error| error.to_string())?; if !rebuild && let Some(history) = existing { match history.preferred(&commit) { - Ok(Some(preferred)) => return Ok((history, preferred)), + Ok(Some(preferred)) if preferred.version.build_profile == requested_profile => { + return Ok((history, preferred)); + } + Ok(Some(_)) => {} Ok(None) => {} Err(error) => return Err(error.to_string()), } @@ -160,7 +164,7 @@ pub(crate) fn resolve_or_materialize( let queue = HistoryQueue::for_repository(repository).map_err(|error| error.to_string())?; let request = JobRequest { commit: commit.clone(), - profile: options.profile(), + profile: requested_profile, }; let job_id = if rebuild { queue.enqueue_rebuild(request, replace_corrupt) @@ -258,11 +262,35 @@ pub(crate) fn resolve_comparable_pair( return Err("the requested fingerprint is not materialized at both commits".to_owned()); } let (history, old, new) = match (old, new) { - (Some(old), Some(new)) => ( - existing.ok_or_else(|| "history store disappeared".to_owned())?, - old, - new, - ), + (Some(old), Some(new)) => { + if old.version.build_profile == new.version.build_profile { + ( + existing.ok_or_else(|| "history store disappeared".to_owned())?, + old, + new, + ) + } else { + let old_options = + HistoryBuildOptions::from_compatible_profile(old.version.build_profile.clone()) + .map_err(|error| error.to_string())?; + let new_options = + HistoryBuildOptions::from_compatible_profile(new.version.build_profile.clone()) + .map_err(|error| error.to_string())?; + if old_options.profile() == new_options.profile() { + let (_, old) = + resolve_or_materialize(repository, old_commit, &old_options, false, false)?; + let (history, new) = + resolve_or_materialize(repository, new_commit, &new_options, false, false)?; + (history, old, new) + } else { + ( + existing.ok_or_else(|| "history store disappeared".to_owned())?, + old, + new, + ) + } + } + } (Some(old), None) => { let options = HistoryBuildOptions::from_compatible_profile(old.version.build_profile.clone()) diff --git a/crates/compass-cli/tests/review_cli.rs b/crates/compass-cli/tests/review_cli.rs index 70807b46..9386e9b9 100644 --- a/crates/compass-cli/tests/review_cli.rs +++ b/crates/compass-cli/tests/review_cli.rs @@ -1,8 +1,10 @@ use std::path::Path; use std::process::{Command, Output}; -use compass_history::{ExtractionFingerprint, HistoryStore, PublishRequest, Repository}; -use compass_pr_intelligence::{GateState, PullRequestReport, RiskBand}; +use compass_history::{ + ExtractionFingerprint, HistoryConfig, HistoryStore, PublishRequest, Repository, +}; +use compass_pr_intelligence::{GateState, MergeOutcome, PullRequestReport, RiskBand}; fn git(root: &Path, arguments: &[&str]) -> Result> { let output = Command::new("git") @@ -61,6 +63,24 @@ fn publish_historical_base(root: &Path, commit: &str) -> Result<(), Box Result<(), Box> { + let enabled = run(root, &["history", "enable", "--code-only"])?; + if !enabled.status.success() { + return Err(format!( + "could not enable history: {}", + String::from_utf8_lossy(&enabled.stderr) + ) + .into()); + } + let repository = Repository::discover(root)?; + let mut profile = HistoryConfig::load(&repository)? + .profile + .ok_or("enabled profile")?; + profile.insert("compass_version", "0.1.10")?; + HistoryConfig::enable(&repository, profile)?; + Ok(()) +} + #[test] fn local_review_writes_round_trippable_exact_report() -> Result<(), Box> { let directory = tempfile::tempdir()?; @@ -187,6 +207,140 @@ fn local_review_rebuilds_a_comparable_pair_from_compass_0_1_10() Ok(()) } +#[test] +fn local_review_upgrades_a_persisted_0_1_10_profile_after_a_current_graph_build() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + initialize(directory.path())?; + let base = git(directory.path(), &["rev-parse", "HEAD"])?; + git(directory.path(), &["checkout", "--quiet", "-b", "feature"])?; + std::fs::write( + directory.path().join("feature.rs"), + "pub fn feature() -> u8 { 2 }\n", + )?; + git(directory.path(), &["add", "feature.rs"])?; + git(directory.path(), &["commit", "--quiet", "-m", "feature"])?; + let head = git(directory.path(), &["rev-parse", "HEAD"])?; + persist_historical_repository_profile(directory.path())?; + + let built = run( + directory.path(), + &["extract", ".", "--code-only", "--no-viz"], + )?; + assert!( + built.status.success(), + "stdout={} stderr={}", + String::from_utf8_lossy(&built.stdout), + String::from_utf8_lossy(&built.stderr) + ); + let repository = Repository::discover(directory.path())?; + assert_eq!( + HistoryConfig::load(&repository)? + .profile + .and_then(|profile| profile.value("compass_version").map(str::to_owned)) + .as_deref(), + Some("0.1.10") + ); + + let reviewed = run( + directory.path(), + &[ + "review", "--base", &base, "--head", &head, "--format", "json", + ], + )?; + assert!( + reviewed.status.success(), + "stdout={} stderr={}", + String::from_utf8_lossy(&reviewed.stdout), + String::from_utf8_lossy(&reviewed.stderr) + ); + let report = PullRequestReport::from_json(&reviewed.stdout)?; + assert_eq!(report.identity.revisions.target_head, base); + assert_eq!(report.identity.revisions.pull_request_head, head); + let comparison = report + .identity + .revisions + .merge_result + .object_id() + .unwrap_or(&report.identity.revisions.pull_request_head) + .to_owned(); + + let history = HistoryStore::open_existing(&repository)?.ok_or("history store")?; + for revision in [base, comparison] { + let commit = repository.resolve(&revision)?; + let preferred = history.preferred(&commit)?.ok_or("preferred realization")?; + assert_eq!( + preferred.version.build_profile.value("compass_version"), + Some(env!("CARGO_PKG_VERSION")) + ); + } + Ok(()) +} + +#[test] +fn local_review_reconciles_existing_compatible_realizations() +-> Result<(), Box> { + let directory = tempfile::tempdir()?; + initialize(directory.path())?; + git(directory.path(), &["checkout", "--quiet", "-b", "feature"])?; + std::fs::write( + directory.path().join("lib.rs"), + "pub fn shared() -> u8 { 2 }\n", + )?; + git(directory.path(), &["add", "lib.rs"])?; + git(directory.path(), &["commit", "--quiet", "-m", "feature"])?; + let head = git(directory.path(), &["rev-parse", "HEAD"])?; + git(directory.path(), &["checkout", "--quiet", "main"])?; + std::fs::write( + directory.path().join("lib.rs"), + "pub fn shared() -> u8 { 3 }\n", + )?; + git(directory.path(), &["add", "lib.rs"])?; + git(directory.path(), &["commit", "--quiet", "-m", "target"])?; + let base = git(directory.path(), &["rev-parse", "HEAD"])?; + publish_historical_base(directory.path(), &base)?; + let built = run( + directory.path(), + &["history", "build", &head, "--code-only"], + )?; + assert!( + built.status.success(), + "stdout={} stderr={}", + String::from_utf8_lossy(&built.stdout), + String::from_utf8_lossy(&built.stderr) + ); + + let reviewed = run( + directory.path(), + &[ + "review", "--base", &base, "--head", &head, "--format", "json", + ], + )?; + assert!( + reviewed.status.success(), + "stdout={} stderr={}", + String::from_utf8_lossy(&reviewed.stdout), + String::from_utf8_lossy(&reviewed.stderr) + ); + let report = PullRequestReport::from_json(&reviewed.stdout)?; + assert!(matches!( + report.identity.revisions.merge_result, + MergeOutcome::Conflicted { .. } + )); + + let repository = Repository::discover(directory.path())?; + let history = HistoryStore::open_existing(&repository)?.ok_or("history store")?; + for revision in [base, head] { + let commit = repository.resolve(&revision)?; + let preferred = history.preferred(&commit)?.ok_or("preferred realization")?; + assert_eq!( + preferred.version.build_profile.value("compass_version"), + Some(env!("CARGO_PKG_VERSION")) + ); + } + Ok(()) +} + #[test] fn conflicted_review_is_unavailable_without_false_clean_gate() -> Result<(), Box> { diff --git a/docs/guides/versioned-history.md b/docs/guides/versioned-history.md index 8ea73efe..c5ceade0 100644 --- a/docs/guides/versioned-history.md +++ b/docs/guides/versioned-history.md @@ -355,12 +355,14 @@ There is no profile-mismatch override: unlike profiles do not produce a semantic or exact report. Compass checks graph-engine compatibility explicitly before comparing the complete build profiles. -`compass review` handles an older preferred realization with a compatible -profile from Compass `0.1.10` or later automatically when it must materialize -the other side. It preserves user-selected profile options, rebuilds a -current-version pair, and leaves the older immutable realization intact. A -profile older than `0.1.10`, newer than the running binary, or from another -release line remains an explicit compatibility error. +`compass review` handles an older preferred realization or repository history +profile from Compass `0.1.10` or later automatically. This includes comparisons +where both revisions already have preferred realizations. Compass advances the +engine fields and rebuilds a current-version pair only when both sides retain +identical user-selected options. It leaves older immutable realizations intact. +A profile older than `0.1.10`, newer than the running binary, from another +release line, or carrying genuinely different user options remains an explicit +compatibility error. ## 7. Export a realization From 2d3fe26620b0a51b830ddd9c7b1547ba15b4d2dc Mon Sep 17 00:00:00 2001 From: forhappy Date: Fri, 14 Aug 2026 16:09:18 -0700 Subject: [PATCH 3/4] Remove legacy review version floor --- CHANGELOG.md | 9 ++++---- COMPATIBILITY.md | 18 ++++++++------- crates/compass-cli/src/history_build.rs | 30 +++++++++++++++++-------- crates/compass-cli/tests/review_cli.rs | 15 ++++++++----- docs/guides/versioned-history.md | 14 +++++++----- 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a72eb366..85030121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ ## Unreleased - Allow `compass review` on `0.3.x` to rebuild comparable realizations from - repository profiles and preferred realizations persisted by Compass `0.1.10` - or later, including when both compared revisions are already materialized. - The upgrade preserves matching user-selected options, refreshes engine-owned - fingerprint fields, and keeps original historical realizations immutable. + any older repository profile or preferred realization whose persisted shape + remains reconstructable, including when both compared revisions are already + materialized. The upgrade preserves matching user-selected options, refreshes + engine-owned fingerprint fields, and keeps original historical realizations + immutable. ## 0.3.13 - 2026-08-14 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 8dca734e..7eae6dd1 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -331,14 +331,16 @@ Presentation formats and the reusable GitHub Action consume this report and do not redefine its semantics. When either side of a review has a preferred realization or repository history -profile persisted by Compass `0.1.10` or later, the `0.3.x` line advances -engine-owned profile fields and materializes both revisions with the running -binary. This also applies when both revisions already have preferred -realizations. Reconciliation proceeds only when their user-selected options are -identical after engine advancement. Older realizations remain immutable and -queryable. Profiles older than `0.1.10`, newer than the running binary, -targeting a future release line, or retaining different user options still fail -explicitly rather than being guessed or downgraded. +profile from an older Compass release, the `0.3.x` line advances engine-owned +profile fields and validates whether the persisted profile shape can still be +reconstructed before materializing both revisions with the running binary. +This also applies when both revisions already have preferred realizations. +Reconciliation proceeds only when their user-selected options are identical +after engine advancement. Older realizations remain immutable and queryable. +Malformed or unsupported profile shapes, versions newer than the running +binary, and different user options still fail explicitly rather than being +guessed or downgraded. Future running minor lines must re-qualify this migration +before inheriting it. Dependency findings in `compass.semantic_diff.report/1` may now carry the optional strict `dependency_topology` object. It records source/target community diff --git a/crates/compass-cli/src/history_build.rs b/crates/compass-cli/src/history_build.rs index c0bd9557..89069184 100644 --- a/crates/compass-cli/src/history_build.rs +++ b/crates/compass-cli/src/history_build.rs @@ -15,10 +15,9 @@ use compass_history::{ HISTORY_GRAPH_SCHEMA, HistoryError, MAX_DIAGNOSTIC_BYTES, }; -// Compass 0.1.10 is the earliest released build-profile shape qualified for -// current-option reconstruction. Pin the target minor as well: before 1.0, a -// future minor must re-evaluate this migration instead of inheriting it. -const COMPATIBLE_PROFILE_UPGRADE_FLOOR: semver::Version = semver::Version::new(0, 1, 10); +// Pin the target minor: before 1.0, a future minor must re-evaluate this +// migration instead of inheriting it. Persisted profiles are admitted by their +// reconstructable shape, not by an allowlisted historical Compass release. const COMPATIBLE_PROFILE_TARGET_MAJOR: u64 = 0; const COMPATIBLE_PROFILE_TARGET_MINOR: u64 = 3; @@ -307,7 +306,6 @@ impl HistoryBuildOptions { fn is_compatible_profile_upgrade(persisted: &semver::Version, current: &semver::Version) -> bool { current.major == COMPATIBLE_PROFILE_TARGET_MAJOR && current.minor == COMPATIBLE_PROFILE_TARGET_MINOR - && persisted >= &COMPATIBLE_PROFILE_UPGRADE_FLOOR && persisted < current } @@ -1634,10 +1632,10 @@ mod tests { } #[test] - fn compatible_0_1_10_profiles_advance_engine_fields_and_preserve_user_options() + fn reconstructable_older_profiles_advance_engine_fields_without_a_release_floor() -> Result<(), Box> { let mut profile = HistoryBuildOptions::defaults()?.profile(); - profile.insert("compass_version", "0.1.10")?; + profile.insert("compass_version", "0.0.0")?; profile.insert("pipeline_version", "compass-core/older")?; profile.insert("resolution", "2")?; @@ -1650,9 +1648,23 @@ mod tests { assert_eq!(upgraded.value("pipeline_version"), Some("compass-core/v1")); assert_eq!(upgraded.value("resolution"), Some("2")); - let below_floor = semver::Version::new(0, 1, 9); + let mut unsupported = HistoryBuildOptions::defaults()?.profile(); + unsupported.insert("compass_version", "0.0.0")?; + unsupported.insert("future_option", "enabled")?; + let error = HistoryBuildOptions::from_compatible_profile(unsupported) + .err() + .ok_or("unsupported older profile unexpectedly accepted")?; + assert!( + error + .to_string() + .contains("unsupported persisted build-profile field") + ); + let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))?; - assert!(!is_compatible_profile_upgrade(&below_floor, ¤t)); + assert!(is_compatible_profile_upgrade( + &semver::Version::new(0, 0, 0), + ¤t + )); assert!(!is_compatible_profile_upgrade(¤t, ¤t)); assert!(!is_compatible_profile_upgrade( &semver::Version::new(1, 0, 0), diff --git a/crates/compass-cli/tests/review_cli.rs b/crates/compass-cli/tests/review_cli.rs index 9386e9b9..2ca173cb 100644 --- a/crates/compass-cli/tests/review_cli.rs +++ b/crates/compass-cli/tests/review_cli.rs @@ -6,6 +6,8 @@ use compass_history::{ }; use compass_pr_intelligence::{GateState, MergeOutcome, PullRequestReport, RiskBand}; +const SYNTHETIC_OLDER_COMPASS_VERSION: &str = "0.0.0"; + fn git(root: &Path, arguments: &[&str]) -> Result> { let output = Command::new("git") .args(arguments) @@ -50,7 +52,7 @@ fn publish_historical_base(root: &Path, commit: &str) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; @@ -202,13 +204,14 @@ fn local_review_rebuilds_a_comparable_pair_from_compass_0_1_10() Some(env!("CARGO_PKG_VERSION")) ); assert!(history.list(Some(&base))?.iter().any(|realization| { - realization.version.build_profile.value("compass_version") == Some("0.1.10") + realization.version.build_profile.value("compass_version") + == Some(SYNTHETIC_OLDER_COMPASS_VERSION) })); Ok(()) } #[test] -fn local_review_upgrades_a_persisted_0_1_10_profile_after_a_current_graph_build() +fn local_review_upgrades_an_older_persisted_profile_after_a_current_graph_build() -> Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; @@ -239,7 +242,7 @@ fn local_review_upgrades_a_persisted_0_1_10_profile_after_a_current_graph_build( .profile .and_then(|profile| profile.value("compass_version").map(str::to_owned)) .as_deref(), - Some("0.1.10") + Some(SYNTHETIC_OLDER_COMPASS_VERSION) ); let reviewed = run( diff --git a/docs/guides/versioned-history.md b/docs/guides/versioned-history.md index c5ceade0..d48798f4 100644 --- a/docs/guides/versioned-history.md +++ b/docs/guides/versioned-history.md @@ -356,13 +356,15 @@ semantic or exact report. Compass checks graph-engine compatibility explicitly before comparing the complete build profiles. `compass review` handles an older preferred realization or repository history -profile from Compass `0.1.10` or later automatically. This includes comparisons -where both revisions already have preferred realizations. Compass advances the -engine fields and rebuilds a current-version pair only when both sides retain +profile automatically when its persisted shape remains reconstructable. This +includes comparisons where both revisions already have preferred realizations. +Compass advances the engine fields, validates the complete reconstructed +profile, and rebuilds a current-version pair only when both sides retain identical user-selected options. It leaves older immutable realizations intact. -A profile older than `0.1.10`, newer than the running binary, from another -release line, or carrying genuinely different user options remains an explicit -compatibility error. +A malformed or unsupported profile shape, a version newer than the running +binary, or genuinely different user options remains an explicit compatibility +error. Future running minor lines must re-qualify this migration before +inheriting it. ## 7. Export a realization From d6bf62b8c41ba09aa222f7072750675a8dfdf256 Mon Sep 17 00:00:00 2001 From: forhappy Date: Fri, 14 Aug 2026 16:38:45 -0700 Subject: [PATCH 4/4] Hard cut over review profile contracts --- CHANGELOG.md | 10 +- COMPATIBILITY.md | 20 ++-- crates/compass-cli/src/history_build.rs | 116 ++++-------------- crates/compass-cli/src/history_commands.rs | 133 ++++++++++++++------- crates/compass-cli/tests/history_cli.rs | 50 ++++---- crates/compass-cli/tests/review_cli.rs | 56 +++++---- docs/guides/versioned-history.md | 20 ++-- 7 files changed, 196 insertions(+), 209 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85030121..65c318ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,11 @@ ## Unreleased - Allow `compass review` on `0.3.x` to rebuild comparable realizations from - any older repository profile or preferred realization whose persisted shape - remains reconstructable, including when both compared revisions are already - materialized. The upgrade preserves matching user-selected options, refreshes - engine-owned fingerprint fields, and keeps original historical realizations - immutable. + any repository profile or preferred realization whose persisted user-option + shape remains reconstructable, including when both compared revisions are + already materialized. Rebuilding does not order or allowlist Compass release + numbers: it preserves matching user-selected options, replaces engine-owned + fingerprint fields, and keeps original historical realizations immutable. ## 0.3.13 - 2026-08-14 diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 7eae6dd1..dfb9d5cc 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -331,16 +331,16 @@ Presentation formats and the reusable GitHub Action consume this report and do not redefine its semantics. When either side of a review has a preferred realization or repository history -profile from an older Compass release, the `0.3.x` line advances engine-owned -profile fields and validates whether the persisted profile shape can still be -reconstructed before materializing both revisions with the running binary. -This also applies when both revisions already have preferred realizations. -Reconciliation proceeds only when their user-selected options are identical -after engine advancement. Older realizations remain immutable and queryable. -Malformed or unsupported profile shapes, versions newer than the running -binary, and different user options still fail explicitly rather than being -guessed or downgraded. Future running minor lines must re-qualify this migration -before inheriting it. +profile with noncurrent engine fields, Compass replaces every engine-owned +field with the running contract and then validates the complete reconstructed +profile before materializing both revisions. The persisted `compass_version` +is provenance, not a compatibility gate: review does not parse, order, or +allowlist release numbers. This also applies when both revisions already have +preferred realizations. Reconciliation proceeds only when their user-selected +options are identical after reconstruction. Historical realizations remain +immutable and queryable. Malformed or unsupported profile shapes and different +user options fail explicitly. When the supported profile shape changes, Compass +uses a hard cutover rather than accumulating release-specific migrations. Dependency findings in `compass.semantic_diff.report/1` may now carry the optional strict `dependency_topology` object. It records source/target community diff --git a/crates/compass-cli/src/history_build.rs b/crates/compass-cli/src/history_build.rs index 89069184..6f051fc1 100644 --- a/crates/compass-cli/src/history_build.rs +++ b/crates/compass-cli/src/history_build.rs @@ -15,12 +15,6 @@ use compass_history::{ HISTORY_GRAPH_SCHEMA, HistoryError, MAX_DIAGNOSTIC_BYTES, }; -// Pin the target minor: before 1.0, a future minor must re-evaluate this -// migration instead of inheriting it. Persisted profiles are admitted by their -// reconstructable shape, not by an allowlisted historical Compass release. -const COMPATIBLE_PROFILE_TARGET_MAJOR: u64 = 0; -const COMPATIBLE_PROFILE_TARGET_MINOR: u64 = 3; - #[derive(Clone, Debug)] pub(crate) struct HistoryBuildOptions { profile: BuildProfile, @@ -115,30 +109,11 @@ impl HistoryBuildOptions { }) } - pub(crate) fn from_compatible_profile(mut profile: BuildProfile) -> Result { - let persisted = profile.value("compass_version").ok_or_else(|| { - HistoryError::InvalidFingerprint( + pub(crate) fn from_rebuild_profile(mut profile: BuildProfile) -> Result { + if profile.value("compass_version").is_none() { + return Err(HistoryError::InvalidFingerprint( "persisted compass_version is missing from build profile".to_owned(), - ) - })?; - if persisted == env!("CARGO_PKG_VERSION") { - return Self::from_profile(profile); - } - let persisted = semver::Version::parse(persisted).map_err(|_| { - HistoryError::InvalidFingerprint( - "persisted compass_version is not a semantic version".to_owned(), - ) - })?; - let current = semver::Version::parse(env!("CARGO_PKG_VERSION")).map_err(|_| { - HistoryError::InvalidFingerprint( - "running compass_version is not a semantic version".to_owned(), - ) - })?; - if !is_compatible_profile_upgrade(&persisted, ¤t) { - return Err(HistoryError::InvalidFingerprint(format!( - "persisted compass_version {persisted} cannot be upgraded by {}", - env!("CARGO_PKG_VERSION") - ))); + )); } let deep = match profile.value("semantic_mode") { Some("standard") => false, @@ -303,12 +278,6 @@ impl HistoryBuildOptions { } } -fn is_compatible_profile_upgrade(persisted: &semver::Version, current: &semver::Version) -> bool { - current.major == COMPATIBLE_PROFILE_TARGET_MAJOR - && current.minor == COMPATIBLE_PROFILE_TARGET_MINOR - && persisted < current -} - fn insert_current_engine_profile( profile: &mut BuildProfile, deep: bool, @@ -1598,78 +1567,39 @@ mod tests { } #[test] - fn compatible_patch_profiles_advance_engine_fields_and_preserve_user_options() + fn rebuild_profiles_replace_engine_identity_without_release_comparison() -> Result<(), Box> { - let mut profile = HistoryBuildOptions::defaults()?.profile(); - let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))?; - let mut previous = current.clone(); - previous.patch = previous - .patch - .checked_sub(1) - .ok_or("patch release fixture")?; - profile.insert("compass_version", &previous.to_string())?; - profile.insert("pipeline_version", "compass-core/older")?; - profile.insert("resolution", "2")?; - - let upgraded = HistoryBuildOptions::from_compatible_profile(profile)?.profile(); + for persisted_engine in ["historical-engine", "999.0.0", env!("CARGO_PKG_VERSION")] { + let mut profile = HistoryBuildOptions::defaults()?.profile(); + profile.insert("compass_version", persisted_engine)?; + profile.insert("pipeline_version", "compass-core/other")?; + profile.insert("resolution", "2")?; - assert_eq!( - upgraded.value("compass_version"), - Some(env!("CARGO_PKG_VERSION")) - ); - assert_eq!(upgraded.value("pipeline_version"), Some("compass-core/v1")); - assert_eq!(upgraded.value("resolution"), Some("2")); - - let mut future_profile = upgraded; - let mut future = current; - future.patch = future.patch.saturating_add(1); - future_profile.insert("compass_version", &future.to_string())?; - let error = HistoryBuildOptions::from_compatible_profile(future_profile) - .err() - .ok_or("future profile unexpectedly accepted")?; - assert!(error.to_string().contains("cannot be upgraded")); + let rebuilt = HistoryBuildOptions::from_rebuild_profile(profile)?.profile(); + + assert_eq!( + rebuilt.value("compass_version"), + Some(env!("CARGO_PKG_VERSION")) + ); + assert_eq!(rebuilt.value("pipeline_version"), Some("compass-core/v1")); + assert_eq!(rebuilt.value("resolution"), Some("2")); + } Ok(()) } #[test] - fn reconstructable_older_profiles_advance_engine_fields_without_a_release_floor() - -> Result<(), Box> { - let mut profile = HistoryBuildOptions::defaults()?.profile(); - profile.insert("compass_version", "0.0.0")?; - profile.insert("pipeline_version", "compass-core/older")?; - profile.insert("resolution", "2")?; - - let upgraded = HistoryBuildOptions::from_compatible_profile(profile)?.profile(); - - assert_eq!( - upgraded.value("compass_version"), - Some(env!("CARGO_PKG_VERSION")) - ); - assert_eq!(upgraded.value("pipeline_version"), Some("compass-core/v1")); - assert_eq!(upgraded.value("resolution"), Some("2")); - + fn rebuild_profiles_hard_fail_unsupported_shapes() -> Result<(), Box> { let mut unsupported = HistoryBuildOptions::defaults()?.profile(); - unsupported.insert("compass_version", "0.0.0")?; + unsupported.insert("compass_version", "historical-engine")?; unsupported.insert("future_option", "enabled")?; - let error = HistoryBuildOptions::from_compatible_profile(unsupported) + let error = HistoryBuildOptions::from_rebuild_profile(unsupported) .err() - .ok_or("unsupported older profile unexpectedly accepted")?; + .ok_or("unsupported profile unexpectedly accepted")?; assert!( error .to_string() .contains("unsupported persisted build-profile field") ); - - let current = semver::Version::parse(env!("CARGO_PKG_VERSION"))?; - assert!(is_compatible_profile_upgrade( - &semver::Version::new(0, 0, 0), - ¤t - )); - assert!(!is_compatible_profile_upgrade(¤t, ¤t)); - assert!(!is_compatible_profile_upgrade( - &semver::Version::new(1, 0, 0), - ¤t - )); Ok(()) } diff --git a/crates/compass-cli/src/history_commands.rs b/crates/compass-cli/src/history_commands.rs index a5453623..8a0c307c 100644 --- a/crates/compass-cli/src/history_commands.rs +++ b/crates/compass-cli/src/history_commands.rs @@ -143,12 +143,36 @@ pub(crate) fn resolve_or_materialize( options: &HistoryBuildOptions, rebuild: bool, replace_corrupt: bool, +) -> Result<(HistoryStore, PublishedVersion), String> { + resolve_or_materialize_inner(repository, commit, options, rebuild, replace_corrupt, false) +} + +fn resolve_or_materialize_matching_profile( + repository: &Repository, + commit: CommitId, + options: &HistoryBuildOptions, + rebuild: bool, + replace_corrupt: bool, +) -> Result<(HistoryStore, PublishedVersion), String> { + resolve_or_materialize_inner(repository, commit, options, rebuild, replace_corrupt, true) +} + +fn resolve_or_materialize_inner( + repository: &Repository, + commit: CommitId, + options: &HistoryBuildOptions, + rebuild: bool, + replace_corrupt: bool, + require_profile_match: bool, ) -> Result<(HistoryStore, PublishedVersion), String> { let requested_profile = options.profile(); let existing = HistoryStore::open_existing(repository).map_err(|error| error.to_string())?; if !rebuild && let Some(history) = existing { match history.preferred(&commit) { - Ok(Some(preferred)) if preferred.version.build_profile == requested_profile => { + Ok(Some(preferred)) + if !require_profile_match + || preferred.version.build_profile == requested_profile => + { return Ok((history, preferred)); } Ok(Some(_)) => {} @@ -215,7 +239,7 @@ pub(crate) fn resolve_or_materialize( fn configured_build_options(repository: &Repository) -> Result { let config = HistoryConfig::load(repository).map_err(|error| error.to_string())?; if let Some(profile) = config.profile { - return HistoryBuildOptions::from_compatible_profile(profile) + return HistoryBuildOptions::from_rebuild_profile(profile) .map_err(|error| error.to_string()); } HistoryBuildOptions::defaults().map_err(|error| error.to_string()) @@ -262,67 +286,94 @@ pub(crate) fn resolve_comparable_pair( return Err("the requested fingerprint is not materialized at both commits".to_owned()); } let (history, old, new) = match (old, new) { + (Some(old), Some(new)) if required_fingerprint.is_some() => ( + existing.ok_or_else(|| "history store disappeared".to_owned())?, + old, + new, + ), (Some(old), Some(new)) => { - if old.version.build_profile == new.version.build_profile { - ( - existing.ok_or_else(|| "history store disappeared".to_owned())?, - old, - new, - ) + let old_options = + HistoryBuildOptions::from_rebuild_profile(old.version.build_profile.clone()) + .map_err(|error| error.to_string())?; + let new_options = + HistoryBuildOptions::from_rebuild_profile(new.version.build_profile.clone()) + .map_err(|error| error.to_string())?; + if old_options.profile() == new_options.profile() { + let (_, old) = resolve_or_materialize_matching_profile( + repository, + old_commit, + &old_options, + false, + false, + )?; + let (history, new) = resolve_or_materialize_matching_profile( + repository, + new_commit, + &new_options, + false, + false, + )?; + (history, old, new) } else { - let old_options = - HistoryBuildOptions::from_compatible_profile(old.version.build_profile.clone()) - .map_err(|error| error.to_string())?; - let new_options = - HistoryBuildOptions::from_compatible_profile(new.version.build_profile.clone()) - .map_err(|error| error.to_string())?; - if old_options.profile() == new_options.profile() { - let (_, old) = - resolve_or_materialize(repository, old_commit, &old_options, false, false)?; - let (history, new) = - resolve_or_materialize(repository, new_commit, &new_options, false, false)?; - (history, old, new) - } else { - ( - existing.ok_or_else(|| "history store disappeared".to_owned())?, - old, - new, - ) - } + return Err(format!( + "realizations retain different user-selected build options after current-engine reconstruction\n\nOLD {} ({}) profile: {}\nNEW {} ({}) profile: {}\n\nBuild a comparable realization:\n compass history build {} --profile-from {}", + old.version.git_commit, + old.id, + old.version.profile_digest, + new.version.git_commit, + new.id, + new.version.profile_digest, + new.version.git_commit, + old.version.git_commit, + )); } } (Some(old), None) => { let options = - HistoryBuildOptions::from_compatible_profile(old.version.build_profile.clone()) + HistoryBuildOptions::from_rebuild_profile(old.version.build_profile.clone()) .map_err(|error| error.to_string())?; let old = if old.version.build_profile == options.profile() { old } else { - resolve_or_materialize(repository, old_commit, &options, true, false)?.1 + resolve_or_materialize_matching_profile( + repository, old_commit, &options, true, false, + )? + .1 }; - let (history, new) = - resolve_or_materialize(repository, new_commit, &options, false, false)?; + let (history, new) = resolve_or_materialize_matching_profile( + repository, new_commit, &options, false, false, + )?; (history, old, new) } (None, Some(new)) => { let options = - HistoryBuildOptions::from_compatible_profile(new.version.build_profile.clone()) + HistoryBuildOptions::from_rebuild_profile(new.version.build_profile.clone()) .map_err(|error| error.to_string())?; let new = if new.version.build_profile == options.profile() { new } else { - resolve_or_materialize(repository, new_commit, &options, true, false)?.1 + resolve_or_materialize_matching_profile( + repository, new_commit, &options, true, false, + )? + .1 }; - let (history, old) = - resolve_or_materialize(repository, old_commit, &options, false, false)?; + let (history, old) = resolve_or_materialize_matching_profile( + repository, old_commit, &options, false, false, + )?; (history, old, new) } (None, None) => { let options = configured_build_options(repository)?; - let (_, old) = - resolve_or_materialize(repository, old_commit.clone(), &options, false, false)?; - let (history, new) = - resolve_or_materialize(repository, new_commit, &options, false, false)?; + let (_, old) = resolve_or_materialize_matching_profile( + repository, + old_commit.clone(), + &options, + false, + false, + )?; + let (history, new) = resolve_or_materialize_matching_profile( + repository, new_commit, &options, false, false, + )?; (history, old, new) } }; @@ -1894,7 +1945,7 @@ fn execute_build( let parsed = parse_build_command(command, args).map_err(usage)?; let commit = repository.resolve(&parsed.revision).map_err(runtime)?; let options = if let Some(source) = &parsed.profile_from { - HistoryBuildOptions::from_compatible_profile( + HistoryBuildOptions::from_rebuild_profile( stored_profile(repository, source).map_err(runtime)?, ) .map_err(runtime)? @@ -1937,7 +1988,7 @@ fn execute_build( } else { false }; - let (_history, published) = resolve_or_materialize( + let (_history, published) = resolve_or_materialize_matching_profile( repository, commit, &options, diff --git a/crates/compass-cli/tests/history_cli.rs b/crates/compass-cli/tests/history_cli.rs index fab7b33b..53913ccc 100644 --- a/crates/compass-cli/tests/history_cli.rs +++ b/crates/compass-cli/tests/history_cli.rs @@ -51,10 +51,29 @@ fn current_history_profile() -> Result() - .parse::()?, - artifacts: new_artifacts, - completion: CompletionEvidence { - extraction_succeeded: true, - allow_partial: false, - semantic_files_expected: 0, - semantic_files_completed: 0, - failed_chunks: 0, - }, - make_preferred: true, - })?; - let head = repository.resolve("HEAD")?; - let current = history - .preferred(&head)? - .ok_or("missing current preferred realization")?; - assert!(history.compare_and_set_preferred(&head, Some(¤t.id), &incompatible.id)?); - drop(history); - let mismatch = run(compass, directory.path(), &["diff", "HEAD~1", "HEAD"])?; - assert_eq!(mismatch.status.code(), Some(1)); - assert!(String::from_utf8_lossy(&mismatch.stderr).contains("incompatible graph engines")); Ok(()) } diff --git a/crates/compass-cli/tests/review_cli.rs b/crates/compass-cli/tests/review_cli.rs index 2ca173cb..040cad91 100644 --- a/crates/compass-cli/tests/review_cli.rs +++ b/crates/compass-cli/tests/review_cli.rs @@ -6,7 +6,7 @@ use compass_history::{ }; use compass_pr_intelligence::{GateState, MergeOutcome, PullRequestReport, RiskBand}; -const SYNTHETIC_OLDER_COMPASS_VERSION: &str = "0.0.0"; +const SYNTHETIC_ENGINE_IDENTITY: &str = "historical-engine"; fn git(root: &Path, arguments: &[&str]) -> Result> { let output = Command::new("git") @@ -52,7 +52,7 @@ fn publish_historical_base(root: &Path, commit: &str) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; @@ -205,13 +205,13 @@ fn local_review_rebuilds_a_comparable_pair_from_an_older_profile() ); assert!(history.list(Some(&base))?.iter().any(|realization| { realization.version.build_profile.value("compass_version") - == Some(SYNTHETIC_OLDER_COMPASS_VERSION) + == Some(SYNTHETIC_ENGINE_IDENTITY) })); Ok(()) } #[test] -fn local_review_upgrades_an_older_persisted_profile_after_a_current_graph_build() +fn local_review_rebuilds_a_persisted_profile_after_a_current_graph_build() -> Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; @@ -242,7 +242,7 @@ fn local_review_upgrades_an_older_persisted_profile_after_a_current_graph_build( .profile .and_then(|profile| profile.value("compass_version").map(str::to_owned)) .as_deref(), - Some(SYNTHETIC_OLDER_COMPASS_VERSION) + Some(SYNTHETIC_ENGINE_IDENTITY) ); let reviewed = run( @@ -280,9 +280,9 @@ fn local_review_upgrades_an_older_persisted_profile_after_a_current_graph_build( Ok(()) } -#[test] -fn local_review_reconciles_existing_compatible_realizations() --> Result<(), Box> { +fn assert_review_reconciles_existing_realizations( + noncurrent_head: bool, +) -> Result<(), Box> { let directory = tempfile::tempdir()?; initialize(directory.path())?; git(directory.path(), &["checkout", "--quiet", "-b", "feature"])?; @@ -302,16 +302,20 @@ fn local_review_reconciles_existing_compatible_realizations() git(directory.path(), &["commit", "--quiet", "-m", "target"])?; let base = git(directory.path(), &["rev-parse", "HEAD"])?; publish_historical_base(directory.path(), &base)?; - let built = run( - directory.path(), - &["history", "build", &head, "--code-only"], - )?; - assert!( - built.status.success(), - "stdout={} stderr={}", - String::from_utf8_lossy(&built.stdout), - String::from_utf8_lossy(&built.stderr) - ); + if noncurrent_head { + publish_historical_base(directory.path(), &head)?; + } else { + let built = run( + directory.path(), + &["history", "build", &head, "--code-only"], + )?; + assert!( + built.status.success(), + "stdout={} stderr={}", + String::from_utf8_lossy(&built.stdout), + String::from_utf8_lossy(&built.stderr) + ); + } let reviewed = run( directory.path(), @@ -344,6 +348,18 @@ fn local_review_reconciles_existing_compatible_realizations() Ok(()) } +#[test] +fn local_review_reconciles_existing_different_engine_profiles() +-> Result<(), Box> { + assert_review_reconciles_existing_realizations(false) +} + +#[test] +fn local_review_hard_cuts_over_matching_noncurrent_engine_profiles() +-> Result<(), Box> { + assert_review_reconciles_existing_realizations(true) +} + #[test] fn conflicted_review_is_unavailable_without_false_clean_gate() -> Result<(), Box> { diff --git a/docs/guides/versioned-history.md b/docs/guides/versioned-history.md index d48798f4..dd5592e7 100644 --- a/docs/guides/versioned-history.md +++ b/docs/guides/versioned-history.md @@ -355,16 +355,16 @@ There is no profile-mismatch override: unlike profiles do not produce a semantic or exact report. Compass checks graph-engine compatibility explicitly before comparing the complete build profiles. -`compass review` handles an older preferred realization or repository history -profile automatically when its persisted shape remains reconstructable. This -includes comparisons where both revisions already have preferred realizations. -Compass advances the engine fields, validates the complete reconstructed -profile, and rebuilds a current-version pair only when both sides retain -identical user-selected options. It leaves older immutable realizations intact. -A malformed or unsupported profile shape, a version newer than the running -binary, or genuinely different user options remains an explicit compatibility -error. Future running minor lines must re-qualify this migration before -inheriting it. +`compass review` handles a preferred realization or repository history profile +with noncurrent engine fields automatically when its persisted user-option +shape remains reconstructable. This includes comparisons where both revisions +already have preferred realizations. Compass replaces engine-owned fields with +the running contract, validates the complete reconstructed profile, and rebuilds +a current pair only when both sides retain identical user-selected options. It +does not parse, order, or allowlist the persisted Compass release number, and it +leaves historical realizations intact. A malformed or unsupported profile shape +or genuinely different user options remains an explicit compatibility error. +Profile-shape changes use a hard cutover instead of release-specific migrations. ## 7. Export a realization