derive_district_profile is now a thin wrapper over the shared derive_at_metres_with_riparian core at survey-cell-centre world metres — one derive core, two position sets. The batch pseudo-grid and the true D-243 district grid were two coordinate systems sharing one bare (i32,i32) type; the new SurveyCellPos newtype re-keys every batch product (BodyWorldState.districts, Layer1Output.survey_basin_dirs) so the compiler rejects cross-namespace passing. Fixes two latent same-position divergences the T-1174 investigation surfaced: three inconsistent latitude conventions collapse into the core's single inverse mapping, and the region-climate baseline now floor-divides true world metres instead of collapsing the whole body onto region (0,0)'s baseline — batch climate becomes latitude/region graded (D-245 direction: every changed believability metric increased). Binding preservations per D-256(c): basin_direction rides a post-call override with the true L1 D8 survey-cell aggregate (layer1's map re-keyed to SurveyCellPos, identity lookup — a floor-divide lookup against the pseudo-keyed map would have silently defaulted every cell North); the riparian verdict comes from near_perennial_water_at, never the empty-slice default (which would have flipped riverside vegetation_class). Quarter-skeleton morphology_zone now resolves at the settlement's exact world position via derive_at_metres at work-item execution (where TerrainAnalysisCache lives), replacing the survey-cell-centre map lookup (D-256(d)); settlement_district_pos fixed to true-district floor-division in passing (same doc/impl mismatch class). Second pixel-vs-metre conflation fixed in aliveness_probe's anchor-walk math. Window path byte-unchanged (window_derivation_golden 6/6 byte- identical); derivation_harness golden untouched; believability golden regenerated. Full lib + integration suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
6.4 KiB
Rust
150 lines
6.4 KiB
Rust
//! Believability regression harness (T-1083) — the D-245 acceptance-gate enforcer.
|
|
//!
|
|
//! D-245 makes "the nature layers read alive *anywhere*" the deliverable of the
|
|
//! cascade's nature half. This harness is its repeatable instrument: for a set of
|
|
//! validation bodies at fixed seeds it runs the real deterministic cascade, computes
|
|
//! the [`BelievabilityReport`] (contrast + coherence — never marginal per-tile counts,
|
|
//! the T-1083 lesson), and:
|
|
//!
|
|
//! 1. **Determinism** — derives each body twice and asserts the reports are identical
|
|
//! (the D-010 contract; the strongest invariant, platform-independent).
|
|
//! 2. **Golden baseline** — snapshots the reports so any future cascade change that
|
|
//! moves believability (a regression *or* an improvement, e.g. when T-1080/T-1082
|
|
//! land) is caught and must be acknowledged. Regenerate with
|
|
//! `UPDATE_GOLDEN=1 cargo test --test believability_harness`.
|
|
//! 3. **D-245 criteria (advisory → strict)** — prints each criterion's pass/fail per
|
|
//! body. Per D-245 the gate starts *budgeted + advisory* (thresholds are Q-123-TBD);
|
|
//! set `BELIEVABILITY_STRICT=1` to make unmet criteria *fail* the test (they do
|
|
//! today — that is the point: T-1080/T-1081/T-1082 are the open work to flip them).
|
|
//!
|
|
//! Distinct from `derivation_harness` (T-1031), which checks the binding *laws*; this
|
|
//! is its believability sibling — laws are necessary, this is the sufficiency gate.
|
|
//!
|
|
//! Bodies whose committed data (systems.db / heightmap) is absent are **skipped** with
|
|
//! a logged note (matching `derivation_harness`), so the harness is robust in trimmed
|
|
//! checkouts. The golden is an x86_64 reference (the cascade has f32 warp paths, per
|
|
//! `cascade_golden`).
|
|
|
|
use settled_reach_server::atlas::believability::{
|
|
analyze, cascade_snapshot_for_body, evaluate_criteria, seed_to_u64, BelievabilityReport,
|
|
};
|
|
|
|
const GOLDEN_FILE: &str = "tests/golden/believability.json";
|
|
|
|
/// `(label, body_id, seed)` — lore-anchored validation bodies spanning the climate
|
|
/// extremes the gate must handle: a temperate ocean world (should read lush) and a
|
|
/// frozen ice world (must read as a living *cold* landscape, not blank — D-245).
|
|
const VALIDATION_BODIES: &[(&str, &str, &str)] = &[
|
|
("Arbour (temperate/ocean)", "GJ338Bd", "believability-v1"),
|
|
("Edict (frozen/ice)", "GJ244Ad", "believability-v1"),
|
|
];
|
|
|
|
/// Run the cascade + analyze for one body, or `None` if its committed data is absent.
|
|
fn report_for(body_id: &str, seed: &str) -> Option<BelievabilityReport> {
|
|
let world_seed = seed_to_u64(seed);
|
|
match cascade_snapshot_for_body(world_seed, body_id) {
|
|
Ok((snapshot, params)) => {
|
|
let bws = snapshot.into_body_world_state();
|
|
Some(analyze(
|
|
world_seed,
|
|
body_id,
|
|
&bws.districts,
|
|
bws.heightmap_width,
|
|
bws.heightmap_height,
|
|
params.body_radius_km,
|
|
))
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[believability] SKIP {body_id}: {e}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn believability_determinism_and_golden() {
|
|
let mut reports: Vec<BelievabilityReport> = Vec::new();
|
|
|
|
for (label, body_id, seed) in VALIDATION_BODIES {
|
|
let Some(first) = report_for(body_id, seed) else {
|
|
continue;
|
|
};
|
|
// Determinism (D-010): a second full cascade + analyze must match exactly.
|
|
let second = report_for(body_id, seed).expect("body resolved once, must resolve again");
|
|
assert_eq!(
|
|
first, second,
|
|
"[{label}] non-deterministic believability report — D-010 broken"
|
|
);
|
|
|
|
// Structural sanity that holds regardless of believability quality or platform.
|
|
assert!(
|
|
first.district_count > 0,
|
|
"[{label}] cascade produced no districts"
|
|
);
|
|
assert!(
|
|
first.coherence.water_districts_wet <= first.coherence.water_districts,
|
|
"[{label}] wet water districts exceed total"
|
|
);
|
|
|
|
// Advisory D-245 criteria report (strict via BELIEVABILITY_STRICT=1).
|
|
let strict = std::env::var("BELIEVABILITY_STRICT").is_ok();
|
|
let crit = evaluate_criteria(&first);
|
|
let passes = crit.iter().filter(|c| c.pass).count();
|
|
eprintln!(
|
|
"[believability] {label}: {passes}/{} D-245 criteria pass",
|
|
crit.len()
|
|
);
|
|
for c in &crit {
|
|
eprintln!(
|
|
" [{}] {} — {}",
|
|
if c.pass { "PASS" } else { "FAIL" },
|
|
c.name,
|
|
c.detail
|
|
);
|
|
if strict {
|
|
assert!(
|
|
c.pass,
|
|
"[{label}] D-245 criterion failed (strict): {}",
|
|
c.name
|
|
);
|
|
}
|
|
}
|
|
|
|
reports.push(first);
|
|
}
|
|
|
|
if reports.is_empty() {
|
|
eprintln!("[believability] no validation bodies available — skipping golden");
|
|
return;
|
|
}
|
|
|
|
// ── Golden baseline ─────────────────────────────────────────────────────
|
|
let golden_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN_FILE);
|
|
let actual_json = serde_json::to_string_pretty(&reports).expect("serialize reports") + "\n";
|
|
|
|
if std::env::var("UPDATE_GOLDEN").is_ok() {
|
|
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
|
|
std::fs::write(&golden_path, &actual_json).expect("write golden");
|
|
eprintln!("Golden written: {}", golden_path.display());
|
|
return;
|
|
}
|
|
|
|
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
|
|
panic!(
|
|
"Golden not found: {}.\n\
|
|
First run: UPDATE_GOLDEN=1 cargo test --test believability_harness\n{e}",
|
|
golden_path.display()
|
|
)
|
|
});
|
|
|
|
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
|
|
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
|
|
assert!(
|
|
actual_v == golden_v,
|
|
"Believability golden mismatch — the cascade moved believability (regression OR \
|
|
improvement). If intended (e.g. T-1080/T-1082 landed), update:\n\
|
|
UPDATE_GOLDEN=1 cargo test --test believability_harness\n\nActual:\n{}",
|
|
actual_json.trim()
|
|
);
|
|
}
|