use super::render::{coverage_report, report_sarif, violations_text, violations_text_styled};
use super::term_color::Style;
use super::{
Coverage, boundary_params, constitution_markdown, dispatch, dyn_trait_text, impl_trait_text,
list_document, list_markdown, merge_outcomes, nearest_manifest_from, projection_gate,
report_json, runtime_text, semantic_text, trait_impl_text, visibility_text,
};
use crate::prelude::*;
use serde_json::Value;
use std::path::PathBuf;
fn violation(target: &str, rule: &str, finding: &str, file: Option<&str>) -> Violation {
Violation::new(
BoundaryKind::Crate,
target.to_string(),
rule.to_string(),
finding.to_string(),
format!("reason-for-{target}"),
Severity::Enforce,
)
.with_file(file.map(str::to_string))
}
fn enforce_violation(kind: BoundaryKind, finding: &str) -> Violation {
Violation::new(
kind,
"target".to_string(),
"rule".to_string(),
finding.to_string(),
"reason".to_string(),
Severity::Enforce,
)
}
#[test]
fn merge_combines_violations_from_both_dimensions() {
let static_outcome = Outcome::Violations(Report::new(vec![enforce_violation(
BoundaryKind::Crate,
"serde",
)]));
let semantic_outcome = Outcome::Violations(Report::new(vec![enforce_violation(
BoundaryKind::Semantic,
"crate::infra::DbPool",
)]));
let merged = merge_outcomes(static_outcome, semantic_outcome);
match merged {
Outcome::Violations(report) => assert_eq!(report.violations.len(), 2),
other => panic!("expected merged violations, got {other:?}"),
}
}
#[test]
fn merge_is_clean_only_when_both_are_clean() {
assert_eq!(
merge_outcomes(Outcome::Clean, Outcome::Clean),
Outcome::Clean
);
}
#[test]
fn a_semantic_constitution_error_supersedes_static_violations() {
let static_outcome = Outcome::Violations(Report::new(vec![enforce_violation(
BoundaryKind::Crate,
"serde",
)]));
let semantic_outcome = Outcome::ConstitutionError("module 'crate::ghost' not found".into());
let merged = merge_outcomes(static_outcome, semantic_outcome);
assert!(matches!(merged, Outcome::ConstitutionError(_)));
assert_eq!(
merged.exit_code(),
2,
"a constitution error supersedes (exit 2)"
);
}
#[test]
fn a_static_constitution_error_wins_when_both_error() {
let merged = merge_outcomes(
Outcome::ConstitutionError("bad static crate".into()),
Outcome::ConstitutionError("bad semantic module".into()),
);
assert!(
matches!(merged, Outcome::ConstitutionError(message) if message == "bad static crate"),
"the static error is checked first and wins deterministically",
);
}
#[test]
fn semantic_text_lists_each_boundary() {
let boundary = SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("the domain API must not leak infrastructure types");
let text = semantic_text(&[boundary]);
assert!(text.contains("module crate::domain in app"), "{text}");
assert!(text.contains("must not expose: crate::infra"), "{text}");
}
#[test]
fn including_trait_impls_projects_into_text_json_and_markdown() {
let boundary = SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.including_trait_impls()
.because("no infra leak even via impl-site contracts");
let text = semantic_text(std::slice::from_ref(&boundary));
assert!(
text.contains("must not expose: crate::infra (including trait impls)"),
"{text}"
);
let c = Constitution::new("app").signature_boundary(boundary);
let json = serde_json::to_string(&list_document(&c)).expect("json");
assert!(json.contains("\"including_trait_impls\":true"), "{json}");
let md = constitution_markdown(&c);
assert!(
md.contains("(forbidden: crate::infra; including_trait_impls: true)"),
"{md}"
);
}
#[test]
fn a_bare_boundary_omits_the_opt_in_from_every_projection() {
let boundary = SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("the domain API must not leak infrastructure types");
let text = semantic_text(std::slice::from_ref(&boundary));
assert!(!text.contains("including trait impls"), "{text}");
let c = Constitution::new("app").signature_boundary(boundary);
let json = serde_json::to_string(&list_document(&c)).expect("json");
assert!(!json.contains("including_trait_impls"), "{json}");
let md = constitution_markdown(&c);
assert!(!md.contains("including_trait_impls"), "{md}");
}
#[test]
fn an_anchored_semantic_boundary_projects_its_anchor_only_when_set() {
let anchored = SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("the domain API must not leak infrastructure types")
.with_anchor("ADR-014");
let c = Constitution::new("app").signature_boundary(anchored);
let json = serde_json::to_string(&list_document(&c)).expect("json");
assert!(json.contains("\"anchor\":\"ADR-014\""), "{json}");
assert!(constitution_markdown(&c).contains("ADR-014"));
assert!(
semantic_text(std::slice::from_ref(
c.semantic_boundaries().signature.first().unwrap()
))
.contains("anchor: ADR-014"),
"text projection must surface the anchor line, like json and markdown"
);
let bare = SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("the domain API must not leak infrastructure types");
let bare_c = Constitution::new("app").signature_boundary(bare);
let bare_json = serde_json::to_string(&list_document(&bare_c)).expect("json");
assert!(!bare_json.contains("anchor"), "{bare_json}");
assert!(
!semantic_text(std::slice::from_ref(
bare_c.semantic_boundaries().signature.first().unwrap()
))
.contains("anchor"),
"a boundary without an anchor emits no anchor line in text"
);
}
#[test]
fn markdown_params_exclude_exactly_the_structural_base_keys() {
let boundary = serde_json::json!({
"kind": "semantic", "target": "app", "crate": "app",
"rule": "expose", "severity": "enforce", "reason": "why",
"forbidden": ["Foo"], "anchor": "ADR-014",
});
let params = boundary_params(&boundary);
for structural in ["kind", "target", "crate", "rule", "severity", "reason"] {
assert!(
!params.contains(&format!("{structural}:")),
"structural base key `{structural}` leaked into markdown params: {params}"
);
}
assert!(
params.contains("forbidden:"),
"a rule param must surface: {params}"
);
assert!(
params.contains("anchor:"),
"anchor is a non-structural param: {params}"
);
}
#[test]
fn report_sarif_carries_the_anchor_in_a_result_property_bag() {
let anchored =
violation("core", "rule", "serde", None).with_anchor(Some("ADR-014".to_string()));
let outcome = Outcome::Violations(Report::new(vec![anchored]));
let sarif: Value = serde_json::from_str(&report_sarif(&outcome)).expect("valid SARIF");
assert_eq!(
sarif["runs"][0]["results"][0]["properties"]["anchor"],
"ADR-014"
);
let bare = Outcome::Violations(Report::new(vec![violation("core", "rule", "serde", None)]));
let bare_sarif: Value = serde_json::from_str(&report_sarif(&bare)).expect("valid SARIF");
assert!(
bare_sarif["runs"][0]["results"][0]
.get("properties")
.is_none()
);
}
#[test]
fn report_sarif_merges_anchor_and_polarity_into_one_property_bag() {
let both = violation("core", "rule", "serde", None)
.with_anchor(Some("ADR-014".to_string()))
.with_polarity(Polarity::AllowlistGap);
let sarif: Value =
serde_json::from_str(&report_sarif(&Outcome::Violations(Report::new(vec![both]))))
.expect("valid SARIF");
let props = &sarif["runs"][0]["results"][0]["properties"];
assert_eq!(props["anchor"], "ADR-014");
assert_eq!(props["polarity"], "allowlist_gap");
let pol_only = violation("core", "rule", "serde", None).with_polarity(Polarity::DenyBreach);
let sarif: Value =
serde_json::from_str(&report_sarif(&Outcome::Violations(Report::new(vec![
pol_only,
]))))
.expect("valid SARIF");
let props = &sarif["runs"][0]["results"][0]["properties"];
assert_eq!(props["polarity"], "deny_breach");
assert!(props.get("anchor").is_none());
let neither = Outcome::Violations(Report::new(vec![violation("core", "rule", "serde", None)]));
let sarif: Value = serde_json::from_str(&report_sarif(&neither)).expect("valid SARIF");
assert!(sarif["runs"][0]["results"][0].get("properties").is_none());
}
#[test]
fn sarif_fingerprints_file_less_violations_by_their_full_identity() {
let same_rule = "deny external dependencies";
let same_finding = "serde";
let same_reason = "keep the graph lean";
let mk = |target: &str| {
Violation::new(
BoundaryKind::Crate,
target.to_string(),
same_rule.to_string(),
same_finding.to_string(),
same_reason.to_string(),
Severity::Enforce,
)
};
let outcome = Outcome::Violations(Report::new(vec![mk("web"), mk("cli")]));
let sarif: Value = serde_json::from_str(&report_sarif(&outcome)).expect("valid SARIF");
let results = sarif["runs"][0]["results"]
.as_array()
.expect("results array");
assert_eq!(
results.len(),
2,
"two violations differing only in target are two results: {results:?}"
);
let fp = |r: &Value| {
r["partialFingerprints"]["tianhengViolationId/v1"]
.as_str()
.expect("a fingerprint string")
.to_string()
};
let (fp0, fp1) = (fp(&results[0]), fp(&results[1]));
assert_ne!(
fp0, fp1,
"target-differing violations must get distinct fingerprints: {fp0} vs {fp1}"
);
let joined = format!("{fp0}{fp1}");
assert!(
joined.contains("web") && joined.contains("cli"),
"the fingerprint carries the discriminating target: {joined}"
);
}
#[test]
fn dyn_trait_text_lists_each_boundary() {
let boundary = DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn()
.because("the core seam is statically dispatched");
let text = dyn_trait_text(&[boundary]);
assert!(text.contains("module crate::core in app"), "{text}");
assert!(text.contains("must not expose dyn"), "{text}");
assert!(
text.contains("the core seam is statically dispatched"),
"{text}"
);
}
#[test]
fn dyn_trait_boundary_projects_into_list_document_and_markdown() {
let c = Constitution::new("app").dyn_trait_boundary(
DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn()
.because("the core seam is statically dispatched"),
);
let doc = list_document(&c);
let arr = doc
.get("dyn_trait_boundaries")
.and_then(Value::as_array)
.expect("dyn_trait_boundaries projected");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["rule"], "must not expose dyn");
assert!(
arr[0].get("forbidden").is_none(),
"shape-only: no forbidden set"
);
let md = list_markdown(&doc);
assert!(md.contains("## Dyn-trait boundaries"), "{md}");
assert!(md.contains("must not expose dyn"), "{md}");
assert!(
md.contains("the core seam is statically dispatched"),
"{md}"
);
}
#[test]
fn async_exposure_boundary_projects_into_list_document_and_markdown() {
let c = Constitution::new("app").async_exposure_boundary(
AsyncExposureBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_async_fn()
.because("the core seam is synchronous; async lives at the edges"),
);
let doc = list_document(&c);
let arr = doc["async_exposure_boundaries"]
.as_array()
.expect("projected");
assert_eq!(arr[0]["rule"], "must not expose async fn");
assert_eq!(arr[0]["target"], "crate::core");
let md = list_markdown(&doc);
assert!(md.contains("## Async-exposure boundaries"), "{md}");
assert!(md.contains("must not expose async fn"), "{md}");
}
#[test]
fn impl_trait_boundary_projects_into_list_document_and_markdown() {
let c = Constitution::new("app").impl_trait_boundary(
ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait()
.because("the core seam must return named types, not an existential"),
);
let doc = list_document(&c);
let arr = doc["impl_trait_boundaries"].as_array().expect("projected");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["rule"], "must not expose impl trait");
assert_eq!(arr[0]["target"], "crate::core");
let md = list_markdown(&doc);
assert!(md.contains("## Impl-trait boundaries"), "{md}");
assert!(md.contains("must not expose impl trait"), "{md}");
}
#[test]
fn operand_scoped_impl_trait_boundary_projects_its_forbidden_operands() {
let c = Constitution::new("app").impl_trait_boundary(
ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait_of(["crate::ports::Port"])
.because("the core seam must not return an existential Port"),
);
let doc = list_document(&c);
let arr = doc["impl_trait_boundaries"].as_array().expect("projected");
assert_eq!(arr[0]["rule"], "must not expose impl trait");
assert_eq!(arr[0]["forbidden"][0], "crate::ports::Port");
let md = list_markdown(&doc);
assert!(
md.contains("forbidden: crate::ports::Port"),
"the operand set surfaces as a param:\n{md}"
);
let text = impl_trait_text(&[ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait_of(["crate::ports::Port"])
.because("the core seam must not return an existential Port")]);
assert!(
text.contains("must not expose impl trait of: crate::ports::Port"),
"operand set must surface in text:\n{text}"
);
}
#[test]
fn operand_scoped_dyn_boundary_projects_its_forbidden_operands() {
let c = Constitution::new("app").dyn_trait_boundary(
DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn_of(["crate::ports::Port"])
.because("the core seam must not leak a dyn Port"),
);
let doc = list_document(&c);
let arr = doc["dyn_trait_boundaries"].as_array().expect("projected");
assert_eq!(arr[0]["rule"], "must not expose dyn");
assert_eq!(
arr[0]["forbidden"][0], "crate::ports::Port",
"an operand-scoped boundary projects its forbidden operand set"
);
let md = list_markdown(&doc);
assert!(
md.contains("forbidden: crate::ports::Port"),
"the operand set surfaces as a generic param:\n{md}"
);
let text = dyn_trait_text(&[DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn_of(["crate::ports::Port"])
.because("the core seam must not leak a dyn Port")]);
assert!(
text.contains("must not expose dyn of: crate::ports::Port"),
"operand set must surface in text:\n{text}"
);
}
#[test]
fn trait_impl_text_lists_each_boundary() {
let boundary = TraitImplBoundary::in_crate("app")
.trait_("crate::command::Command")
.only_implemented_in("crate::commands")
.and_in("crate::builtins")
.because("Command impls live with the registry");
let text = trait_impl_text(&[boundary]);
assert!(
text.contains("trait crate::command::Command in app"),
"{text}"
);
assert!(
text.contains("must only be implemented in the declared location(s)"),
"{text}"
);
assert!(text.contains("crate::commands, crate::builtins"), "{text}");
}
#[test]
fn visibility_text_lists_each_boundary_and_is_empty_when_none() {
assert_eq!(visibility_text(&[]), "");
let boundary = VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.because("internal is an impl detail");
let text = visibility_text(&[boundary]);
assert!(text.contains("module crate::internal in app"), "{text}");
assert!(text.contains("must not declare pub items"), "{text}");
let sealed = VisibilityBoundary::in_crate("app")
.module("crate::deep")
.max_visibility(VisibilityCeiling::Super)
.because("sealed to its parent");
let sealed_text = visibility_text(&[sealed]);
assert!(
sealed_text.contains("must not declare items more visible than pub(super)"),
"{sealed_text}"
);
}
#[test]
fn merge_folds_a_trait_impl_violation_into_the_report() {
let static_outcome = Outcome::Violations(Report::new(vec![enforce_violation(
BoundaryKind::Crate,
"serde",
)]));
let trait_impl_outcome = Outcome::Violations(Report::new(vec![enforce_violation(
BoundaryKind::Semantic,
"crate::domain (impl for Foo)",
)]));
let merged = merge_outcomes(static_outcome, trait_impl_outcome);
match merged {
Outcome::Violations(report) => assert_eq!(report.violations.len(), 2),
other => panic!("expected merged violations, got {other:?}"),
}
}
fn fixture(name: &str) -> String {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
.join("Cargo.toml")
.to_string_lossy()
.into_owned()
}
fn workspace_manifest() -> Option<PathBuf> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml");
if path.exists() {
return Some(path);
}
assert!(
std::env::var_os("TIANHENG_WORKSPACE_TESTS").is_none(),
"workspace manifest expected but absent while TIANHENG_WORKSPACE_TESTS is set — \
the dispatch gate must not silently skip in CI"
);
None
}
fn example_constitution() -> Constitution {
Constitution::new("example").boundary(
CrateBoundary::crate_("example-core")
.deny_external_dependencies()
.because("example-core must stay dependency-light"),
)
}
fn run_args(args: &[&str]) -> u8 {
dispatch(&example_constitution(), args.iter().map(|s| s.to_string()))
}
#[test]
fn the_trait_impl_dimension_is_wired_through_dispatch() {
let Some(manifest) = workspace_manifest() else {
return; };
let boundary = TraitImplBoundary::in_crate("xuanji")
.trait_("crate::NoSuchTrait")
.only_implemented_in("crate::nowhere")
.because("wiring check");
let code = dispatch(
&Constitution::new("wiring").trait_impl_boundary(boundary),
[
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
],
);
assert_eq!(
code, 2,
"an unresolvable trait anchor reaches exit 2 through dispatch"
);
}
#[test]
fn the_visibility_dimension_is_wired_through_dispatch() {
let Some(manifest) = workspace_manifest() else {
return; };
let boundary = VisibilityBoundary::in_crate("xuanji")
.module("crate::no_such_module")
.must_not_declare_pub()
.because("wiring check");
let code = dispatch(
&Constitution::new("wiring").visibility_boundary(boundary),
[
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
],
);
assert_eq!(
code, 2,
"an unresolvable visibility module anchor reaches exit 2 through dispatch"
);
}
#[test]
fn the_runtime_dimension_is_wired_through_dispatch() {
let Some(manifest) = workspace_manifest() else {
return; };
let args = || {
[
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
]
};
let boundary = RuntimeBoundary::at("a-seam-no-probe-covers")
.only_origins(["app::domain"])
.because("wiring check");
let code = dispatch(&Constitution::new("wiring").runtime(boundary), args());
assert_eq!(
code, 1,
"a declared-but-unprobed runtime seam reaches exit 1 through dispatch"
);
assert_eq!(
dispatch(&Constitution::new("wiring"), args()),
0,
"an empty constitution over a probe-free workspace is clean (the audit runs, finds nothing)"
);
}
#[test]
fn an_orphan_probe_reacts_with_no_declared_boundary() {
if workspace_manifest().is_none() {
return;
}
let args = [
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
fixture("orphan_probe"),
];
assert_eq!(
dispatch(&Constitution::new("empty"), args),
1,
"an orphan `assert_boundary!` probe with no declared boundary reacts at CI"
);
let clean_args = [
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
fixture("clean"),
];
assert_eq!(
dispatch(&Constitution::new("empty"), clean_args),
0,
"a probe-free workspace under an empty constitution stays clean"
);
}
#[test]
fn the_crate_source_rule_is_wired_through_dispatch() {
let Some(manifest) = workspace_manifest() else {
return; };
let args = || {
[
"tianheng".to_string(),
"check".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
]
};
let c = Constitution::new("wiring").boundary(
CrateBoundary::crate_("guibiao")
.restrict_dependency_sources_to([SourceKind::Registry])
.because("wiring check"),
);
assert_eq!(
dispatch(&c, args()),
1,
"a path dependency under a Registry-only source rule reaches exit 1 through dispatch"
);
assert_eq!(
dispatch(&Constitution::new("wiring"), args()),
0,
"an empty constitution over the same workspace is clean"
);
}
#[test]
fn the_runtime_audit_reports_the_declared_unprobed_seam() {
let Some(manifest) = workspace_manifest() else {
return; };
let src_dirs = crate::workspace_member_src_dirs(&manifest).expect("resolve src dirs");
let boundary = RuntimeBoundary::at("a-seam-no-probe-covers")
.only_origins(["app::domain"])
.because("wiring check");
let outcome = crate::audit_probe_coverage(&[boundary], &src_dirs);
match outcome {
Outcome::Violations(report) => assert!(
report
.violations
.iter()
.any(|v| v.target == "a-seam-no-probe-covers"
&& v.finding.contains("no assert_boundary! probe")),
"the declared-unprobed seam must be the reported finding: {:?}",
report.violations
),
other => panic!("expected a violation naming the unprobed seam, got {other:?}"),
}
}
#[test]
fn list_document_covers_every_populated_dimension() {
let empty = Constitution::new("empty");
let doc = list_document(&empty);
assert!(
doc.get("semantic_boundaries").is_none(),
"empty adds no key: {doc}"
);
assert!(
doc.get("runtime_boundaries").is_none(),
"empty adds no key: {doc}"
);
let full = Constitution::new("full")
.boundary(
CrateBoundary::crate_("core")
.deny_external_dependencies()
.because("core stays light"),
)
.signature_boundary(
SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("no infra leak"),
)
.trait_impl_boundary(
TraitImplBoundary::in_crate("app")
.trait_("crate::Command")
.only_implemented_in("crate::commands")
.because("impls live with the registry"),
)
.visibility_boundary(
VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.because("internal is private"),
)
.forbidden_marker_boundary(
ForbiddenMarkerBoundary::in_crate("app")
.module("crate::domain")
.must_not_acquire("serde::Serialize")
.because("domain is not wire"),
)
.runtime(
RuntimeBoundary::at("domain-entry")
.only_origins(["app::domain"])
.because("only domain crosses"),
);
let doc = list_document(&full);
for (key, kind, target) in [
("semantic_boundaries", "semantic", "crate::domain"),
("trait_impl_boundaries", "semantic", "crate::Command"),
("visibility_boundaries", "semantic", "crate::internal"),
("forbidden_marker_boundaries", "semantic", "crate::domain"),
("runtime_boundaries", "runtime", "domain-entry"),
] {
let arr = doc[key]
.as_array()
.unwrap_or_else(|| panic!("{key} must be an array: {doc}"));
assert!(!arr.is_empty(), "{key} must be non-empty: {doc}");
assert_eq!(arr[0]["kind"], kind, "{key}[0] kind: {}", arr[0]);
assert_eq!(arr[0]["target"], target, "{key}[0] target: {}", arr[0]);
}
let text = runtime_text(full.runtime_boundaries());
assert!(text.contains("seam domain-entry"), "{text}");
}
#[test]
fn markdown_projection_covers_every_dimension_the_json_document_emits() {
let full = Constitution::new("app")
.boundary(
CrateBoundary::crate_("app")
.deny_external_dependencies()
.because("the core stays dependency-light"),
)
.signature_boundary(
SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("the domain API must not leak infra"),
)
.trait_impl_boundary(
TraitImplBoundary::in_crate("app")
.trait_("crate::command::Command")
.only_implemented_in("crate::commands")
.because("Command impls live with the registry"),
)
.visibility_boundary(
VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.because("internal is private"),
)
.forbidden_marker_boundary(
ForbiddenMarkerBoundary::in_crate("app")
.module("crate::domain")
.must_not_acquire("serde::Serialize")
.because("domain is not wire"),
)
.dyn_trait_boundary(
DynTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_dyn()
.because("the core seam is statically dispatched"),
)
.impl_trait_boundary(
ImplTraitBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_impl_trait()
.because("the core seam returns named types"),
)
.async_exposure_boundary(
AsyncExposureBoundary::in_crate("app")
.module("crate::core")
.must_not_expose_async_fn()
.because("the core seam is synchronous"),
)
.runtime(
RuntimeBoundary::at("domain-entry")
.only_origins(["app::domain"])
.because("only domain crosses"),
);
let doc = list_document(&full);
let md = list_markdown(&doc);
for (key, heading) in [
("boundaries", "## Static boundaries"),
("semantic_boundaries", "## Semantic boundaries"),
("trait_impl_boundaries", "## Trait-impl-locality boundaries"),
("visibility_boundaries", "## Visibility boundaries"),
(
"forbidden_marker_boundaries",
"## Forbidden-marker boundaries",
),
("dyn_trait_boundaries", "## Dyn-trait boundaries"),
("impl_trait_boundaries", "## Impl-trait boundaries"),
("async_exposure_boundaries", "## Async-exposure boundaries"),
("runtime_boundaries", "## Runtime boundaries"),
] {
assert!(
doc.get(key)
.and_then(Value::as_array)
.is_some_and(|a| !a.is_empty()),
"fixture must populate {key} so this guard is not vacuous: {doc}"
);
assert!(
md.contains(heading),
"Markdown must carry a `{heading}` section for `{key}` — it under-projects:\n{md}"
);
}
let json_dimensions = doc
.as_object()
.expect("list_document is a JSON object")
.keys()
.filter(|key| key.ends_with("boundaries"))
.count();
assert_eq!(
json_dimensions, 9,
"list_document emits {json_dimensions} dimensions but this coverage guard enumerates 9; \
a new dimension must be wired into list_markdown's section table and added here"
);
}
fn full_constitution() -> Constitution {
Constitution::new("full")
.boundary(
CrateBoundary::crate_("core")
.deny_external_dependencies()
.because("core stays light"),
)
.signature_boundary(
SemanticBoundary::in_crate("app")
.module("crate::domain")
.must_not_expose("crate::infra")
.because("no infra leak"),
)
.trait_impl_boundary(
TraitImplBoundary::in_crate("app")
.trait_("crate::Command")
.only_implemented_in("crate::commands")
.because("impls live with the registry"),
)
.visibility_boundary(
VisibilityBoundary::in_crate("app")
.module("crate::internal")
.must_not_declare_pub()
.because("internal is private"),
)
.forbidden_marker_boundary(
ForbiddenMarkerBoundary::in_crate("app")
.module("crate::domain")
.must_not_acquire("serde::Serialize")
.because("domain is not wire"),
)
.runtime(
RuntimeBoundary::at("domain-entry")
.only_origins(["app::domain"])
.because("only domain crosses"),
)
}
#[test]
fn list_markdown_covers_every_dimension_with_target_rule_and_reason() {
let md = list_markdown(&list_document(&full_constitution()));
assert!(md.contains("# Constitution: full"), "{md}");
for heading in [
"## Static boundaries",
"## Semantic boundaries",
"## Trait-impl-locality boundaries",
"## Visibility boundaries",
"## Forbidden-marker boundaries",
"## Runtime boundaries",
] {
assert!(md.contains(heading), "missing {heading} in:\n{md}");
}
for needle in [
"core", "core stays light", "crate::domain", "crate::infra", "no infra leak", "crate::Command", "crate::commands", "crate::internal", "serde::Serialize", "domain-entry", "app::domain", "only domain crosses", ] {
assert!(md.contains(needle), "missing '{needle}' in:\n{md}");
}
}
#[test]
fn constitution_markdown_equals_the_cli_projection_byte_for_byte() {
let c = full_constitution();
assert_eq!(constitution_markdown(&c), list_markdown(&list_document(&c)));
}
#[test]
fn markdown_foregrounds_the_reason_before_rule_and_classification() {
let c = Constitution::new("t").boundary(
CrateBoundary::crate_("core")
.deny_external_dependencies()
.because("the gravity-bearing principle text"),
);
let md = constitution_markdown(&c);
let r = md
.find("the gravity-bearing principle text")
.expect("reason");
let rule = md.find("**rule**").expect("rule");
let kind = md.find("**kind**").expect("kind");
assert!(
r < rule && rule < kind,
"reason must lead, then rule, then classification:\n{md}"
);
}
#[test]
fn markdown_projects_a_dependency_source_boundary_with_its_allowed_sources() {
let c = Constitution::new("t").boundary(
CrateBoundary::crate_("infra")
.restrict_dependency_sources_to([SourceKind::Registry, SourceKind::Path])
.because("infra must publish to crates.io"),
);
let md = constitution_markdown(&c);
assert!(
md.contains("restrict dependency sources to"),
"the source rule label surfaces:\n{md}"
);
assert!(
md.contains("allowed_sources: registry, path"),
"the allowed source kinds surface as a generic param:\n{md}"
);
}
#[test]
fn markdown_reasonless_boundary_has_no_blockquote_or_orphan_blank_line() {
let c = Constitution::new("t").boundary(
CrateBoundary::crate_("core")
.deny_external_dependencies()
.because(""),
);
let md = constitution_markdown(&c);
assert!(!md.contains("\n> "), "no blockquote when no reason:\n{md}");
assert!(
md.contains("### `core`\n- **rule**"),
"heading immediately followed by the rule bullet:\n{md}"
);
}
#[test]
fn report_text_leads_with_reason_and_shows_the_offending_file() {
let report = Report::new(vec![violation(
"crate::core",
"must not import crate::adapter",
"crate::adapter::Db",
Some("src/core/mod.rs"),
)]);
let text = violations_text(&report);
let reason = text.find("Reason:").expect("reason");
let boundary = text.find("Boundary:").expect("boundary");
let rule = text.find("Rule:").expect("rule");
let found = text.find("Found:").expect("found");
let file = text.find("File:").expect("file");
let reaction = text.find("Reaction:").expect("reaction");
assert!(
reason < boundary && boundary < rule && rule < found && found < file && file < reaction,
"order must be reason → boundary → rule → found → file → reaction:\n{text}"
);
assert!(
text.contains("File:\n src/core/mod.rs"),
"the offending file is shown as the repair location:\n{text}"
);
}
#[test]
fn plain_render_carries_no_ansi_escapes() {
let report = Report::new(vec![violation("crate::core", "rule", "finding", None)]);
let text = violations_text(&report);
assert!(
!text.contains('\u{1b}'),
"plain render must contain no ANSI escape:\n{text:?}"
);
}
#[test]
fn constitution_error_machine_output_carries_no_ansi() {
let outcome = Outcome::ConstitutionError("module 'crate::ghost' not found".into());
let json = report_json(&outcome, &[], None);
let sarif = report_sarif(&outcome);
assert!(
!json.contains('\u{1b}'),
"the JSON projection of a constitution error must carry no ANSI escape:\n{json}"
);
assert!(
!sarif.contains('\u{1b}'),
"the SARIF projection of a constitution error must carry no ANSI escape:\n{sarif}"
);
}
#[test]
fn style_error_is_plain_identity_and_active_wraps() {
let msg = "Tianheng constitution error: module 'crate::ghost' not found";
assert_eq!(Style::PLAIN.error(msg), msg, "PLAIN error is the identity");
let active = Style::ACTIVE.error(msg);
assert!(
active.contains('\u{1b}'),
"ACTIVE error carries ANSI escapes"
);
assert!(
active.contains(msg),
"ACTIVE error wraps the identical text"
);
}
#[test]
fn active_render_colours_around_the_text_without_changing_it() {
let report = Report::new(vec![violation("crate::core", "rule", "finding", None)]);
let styled = violations_text_styled(&report, Style::ACTIVE);
assert!(
styled.contains('\u{1b}'),
"active render must carry ANSI escapes"
);
assert!(
styled.contains("reason-for-crate::core"),
"the reason text survives styling"
);
let plain = violations_text(&report);
let strip = |s: &str| {
s.replace('\u{1b}', "")
.replace("[0m", "")
.replace("[1m", "")
.replace("[1;31m", "")
.replace("[1;33m", "")
};
assert_eq!(
strip(&styled),
strip(&plain),
"stripping the escapes yields the plain report byte-for-byte"
);
}
#[test]
fn report_text_omits_the_file_element_when_absent() {
let report = Report::new(vec![violation("crate::x", "rule", "finding", None)]);
let text = violations_text(&report);
assert!(
!text.contains("File:"),
"no file element when the violation carries none:\n{text}"
);
}
#[test]
fn report_text_shows_anchor_and_repair_polarity_after_the_located_facts() {
let report = Report::new(vec![
violation(
"crate::core",
"must not import crate::adapter",
"crate::adapter::Db",
Some("src/core/mod.rs"),
)
.with_anchor(Some("ADR-014".to_string()))
.with_polarity(Polarity::DenyBreach),
]);
let text = violations_text(&report);
let file = text.find("File:").expect("file");
let anchor = text.find("Anchor:").expect("anchor");
let repair = text.find("Repair:").expect("repair");
let reaction = text.find("Reaction:").expect("reaction");
assert!(
file < anchor && anchor < repair && repair < reaction,
"order must be … file → anchor → repair → reaction:\n{text}"
);
assert!(text.contains("Anchor:\n ADR-014"), "anchor shown:\n{text}");
assert!(
text.contains("Repair:\n deny_breach"),
"repair polarity shown as the boundary's repair direction:\n{text}"
);
}
#[test]
fn report_text_omits_anchor_and_repair_when_absent() {
let report = Report::new(vec![violation("crate::x", "rule", "finding", None)]);
let text = violations_text(&report);
assert!(
!text.contains("Anchor:"),
"no anchor line when the violation carries none:\n{text}"
);
assert!(
!text.contains("Repair:"),
"no repair line when the violation carries no polarity:\n{text}"
);
}
#[test]
fn report_text_groups_violations_by_boundary() {
let report = Report::new(vec![
violation("z-crate", "r1", "f", None),
violation("a-crate", "r1", "f", None),
violation("a-crate", "r0", "f", None),
]);
let text = violations_text(&report);
assert!(
text.find("Boundary:\n a-crate").unwrap() < text.find("Boundary:\n z-crate").unwrap(),
"the a-crate group precedes z-crate:\n{text}"
);
assert!(
text.find("\n r0").unwrap() < text.find("\n r1").unwrap(),
"within a-crate, r0 precedes r1:\n{text}"
);
}
#[test]
fn json_projection_is_unchanged_by_the_text_grouping() {
let outcome = Outcome::Violations(Report::new(vec![
violation("z-crate", "r", "f", None),
violation("a-crate", "r", "f", None),
]));
let json = report_json(&outcome, &[], None);
assert!(
json.find("z-crate").unwrap() < json.find("a-crate").unwrap(),
"JSON keeps input order (z before a), unaffected by the text grouping:\n{json}"
);
}
#[test]
fn sarif_projects_violations_with_file_level_locations_and_no_region() {
let outcome = Outcome::Violations(Report::new(vec![
violation(
"crate::core",
"must not import crate::adapter",
"crate::adapter::Db",
Some("src/core/mod.rs"),
),
violation("dep-crate", "deny external", "serde", None),
]));
let doc: serde_json::Value =
serde_json::from_str(&report_sarif(&outcome)).expect("valid SARIF JSON");
assert_eq!(doc["version"], "2.1.0");
assert_eq!(doc["runs"][0]["tool"]["driver"]["name"], "tianheng");
let results = doc["runs"][0]["results"].as_array().expect("results array");
assert_eq!(results.len(), 2, "one result per non-baselined violation");
assert_eq!(results[0]["level"], "error");
assert_eq!(results[0]["ruleId"], "must not import crate::adapter");
assert!(
results[0]["message"]["text"]
.as_str()
.unwrap()
.contains("reason-for-crate::core")
);
assert_eq!(
results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/core/mod.rs"
);
assert!(
results[0]["locations"][0]["physicalLocation"]["region"].is_null(),
"no region — the line is not observed, never fabricated"
);
assert!(
results[1]["locations"].is_null(),
"a file-less violation projects no location"
);
}
#[test]
fn semantic_violation_projects_its_file_in_json_and_sarif() {
let single_module = Violation::new(
BoundaryKind::Semantic,
"crate::domain".to_string(),
"must not expose".to_string(),
"crate::infra::Db exposed by fn crate::domain::leak".to_string(),
"domain must not expose infra".to_string(),
Severity::Enforce,
)
.with_file(Some("src/domain.rs".to_string()));
let whole_crate_scan = Violation::new(
BoundaryKind::Semantic,
"crate::Command".to_string(),
"must be implemented only in the allowed locations".to_string(),
"crate::plugins (impl for crate::plugins::P)".to_string(),
"Command impls live in crate::allowed".to_string(),
Severity::Enforce,
)
.with_file(Some("src/plugins.rs".to_string()));
let file_less = Violation::new(
BoundaryKind::Crate,
"dep-crate".to_string(),
"deny external".to_string(),
"serde".to_string(),
"core must stay dependency-light".to_string(),
Severity::Enforce,
);
let outcome = Outcome::Violations(Report::new(vec![
single_module,
whole_crate_scan,
file_less,
]));
let json: serde_json::Value =
serde_json::from_str(&report_json(&outcome, &[], None)).expect("valid JSON");
assert_eq!(json["violations"][0]["file"], "src/domain.rs");
assert_eq!(json["violations"][1]["file"], "src/plugins.rs");
assert!(
json["violations"][2]["file"].is_null(),
"a crate-dependency violation has no single source file"
);
let sarif: serde_json::Value =
serde_json::from_str(&report_sarif(&outcome)).expect("valid SARIF");
let results = sarif["runs"][0]["results"]
.as_array()
.expect("results array");
assert_eq!(
results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/domain.rs"
);
assert_eq!(
results[1]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/plugins.rs"
);
assert!(
results[0]["locations"][0]["physicalLocation"]["region"].is_null(),
"no region — the line is not observed for a semantic violation either"
);
assert!(
results[2]["locations"].is_null(),
"a file-less violation projects no SARIF location"
);
}
#[test]
fn sarif_clean_is_empty_and_constitution_error_marks_execution_unsuccessful() {
let clean: serde_json::Value = serde_json::from_str(&report_sarif(&Outcome::Clean)).unwrap();
assert!(
clean["runs"][0]["results"].as_array().unwrap().is_empty(),
"clean → empty results"
);
let err: serde_json::Value =
serde_json::from_str(&report_sarif(&Outcome::ConstitutionError("bad law".into()))).unwrap();
assert_eq!(
err["runs"][0]["invocations"][0]["executionSuccessful"],
serde_json::Value::Bool(false),
"a constitution error marks the invocation unsuccessful (required by SARIF)"
);
assert!(
err["runs"][0]["invocations"][0]["toolExecutionNotifications"][0]["message"]["text"]
.as_str()
.unwrap()
.contains("bad law")
);
}
#[test]
fn sarif_exits_like_json() {
if workspace_manifest().is_none() {
return;
}
for format in ["json", "sarif"] {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("violating"),
"--format",
format,
]),
1,
"violating fixture exits 1 under --format {format}"
);
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
"--format",
format,
]),
0,
"clean fixture exits 0 under --format {format}"
);
}
}
#[test]
fn list_rejects_the_check_only_sarif_format() {
assert_eq!(run_args(&["tianheng", "list", "--format", "sarif"]), 2);
}
#[test]
fn list_markdown_empty_constitution_has_a_title_but_no_sections() {
let md = list_markdown(&list_document(&Constitution::new("empty")));
assert!(md.contains("# Constitution: empty"), "{md}");
assert!(
!md.contains("\n## "),
"no dimension sections expected:\n{md}"
);
}
#[test]
fn list_accepts_markdown_format() {
assert_eq!(run_args(&["tianheng", "list", "--format", "markdown"]), 0);
assert_eq!(run_args(&["tianheng", "list", "--format=markdown"]), 0);
}
#[test]
fn check_rejects_the_list_only_markdown_format() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
"--format",
"markdown",
]),
2
);
}
#[test]
fn the_runtime_projection_distinguishes_posture() {
let event = Constitution::new("c").runtime(
RuntimeBoundary::at("s")
.only_origins(["app::a"])
.because("default event"),
);
let panicking = Constitution::new("c").runtime(
RuntimeBoundary::at("s")
.only_origins(["app::a"])
.panic_on_violation()
.because("opt-in panic"),
);
let ej = list_document(&event)["runtime_boundaries"][0].clone();
let pj = list_document(&panicking)["runtime_boundaries"][0].clone();
assert_eq!(ej["posture"], "event", "default posture is event: {ej}");
assert_eq!(pj["posture"], "panic", "opt-in posture is panic: {pj}");
assert_ne!(ej, pj, "posture must make the two projections differ");
assert!(
runtime_text(panicking.runtime_boundaries()).contains("posture: panic"),
"the text projection names the posture too"
);
}
#[test]
fn both_baseline_flags_exit_2() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
"--baseline",
"a.json",
"--write-baseline",
"b.json",
]),
2
);
}
#[test]
fn unknown_format_exits_2() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
"--format",
"yaml",
]),
2
);
}
#[test]
fn flag_missing_its_value_is_a_usage_error() {
for flag in [
"--manifest-path",
"--baseline",
"--write-baseline",
"--format",
] {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
flag
]),
2,
"{flag} without a value must exit 2",
);
}
}
#[test]
fn list_needs_no_manifest_path_and_exits_0() {
assert_eq!(run_args(&["tianheng", "list"]), 0);
}
#[test]
fn list_json_exits_0() {
assert_eq!(run_args(&["tianheng", "list", "--format", "json"]), 0);
}
#[test]
fn list_unknown_format_is_a_usage_error() {
assert_eq!(run_args(&["tianheng", "list", "--format", "yaml"]), 2);
}
#[test]
fn misspelled_flag_fails_loud_instead_of_being_ignored() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("violating"),
"--write-baselin",
"out.json",
]),
2
);
}
#[test]
fn unknown_flag_exits_2() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"--manifest-path",
&fixture("clean"),
"--frobnicate",
]),
2
);
}
#[test]
fn stray_positional_exits_2() {
assert_eq!(
run_args(&[
"tianheng",
"check",
"stray",
"--manifest-path",
&fixture("clean")
]),
2
);
}
#[test]
fn list_unknown_flag_exits_2() {
assert_eq!(run_args(&["tianheng", "list", "--bogus"]), 2);
}
#[test]
fn list_rejects_check_only_flags() {
for args in [
&["tianheng", "list", "--manifest-path", "Cargo.toml"][..],
&["tianheng", "list", "--baseline", "b.json"][..],
&["tianheng", "list", "--write-baseline", "b.json"][..],
&["tianheng", "list", "--warn-uncovered"][..],
] {
assert_eq!(
run_args(args),
2,
"a check-only flag supplied to list must exit 2: {args:?}",
);
}
}
#[test]
fn the_coverage_advisory_names_each_uncovered_crate_only_under_the_flag() {
let coverage = Coverage {
total: 3,
uncovered: vec!["alpha".to_string(), "beta".to_string()],
};
let quiet = coverage_report(&coverage, false);
assert!(
quiet.contains("2 of 3 workspace crate(s) have no boundary"),
"the summary line always prints: {quiet}"
);
assert!(
!quiet.contains("Tianheng advisory"),
"no per-crate advisory without the flag: {quiet}"
);
let loud = coverage_report(&coverage, true);
assert!(loud.contains("Uncovered crate:\n alpha"), "{loud}");
assert!(loud.contains("Uncovered crate:\n beta"), "{loud}");
assert_eq!(
loud.matches("warning only — CI not failed.").count(),
2,
"one warning-only advisory per uncovered crate: {loud}"
);
let covered = Coverage {
total: 3,
uncovered: vec![],
};
let all_clear = coverage_report(&covered, true);
assert!(
all_clear.contains("all 3 workspace crate(s) have a boundary"),
"{all_clear}"
);
assert!(
!all_clear.contains("Tianheng advisory"),
"a covered workspace emits no advisory even under the flag: {all_clear}"
);
}
#[test]
fn warn_uncovered_never_changes_the_exit_code() {
if workspace_manifest().is_none() {
return;
}
let clean = fixture("clean");
let with = [
"tianheng",
"check",
"--manifest-path",
clean.as_str(),
"--warn-uncovered",
];
let without = ["tianheng", "check", "--manifest-path", clean.as_str()];
assert_eq!(
dispatch(&Constitution::new("empty"), with),
0,
"an uncovered-but-clean workspace stays exit 0 under --warn-uncovered (advisory only)",
);
assert_eq!(
dispatch(&Constitution::new("empty"), without),
0,
"…and without the flag too: coverage never decides the exit code",
);
}
#[test]
fn nearest_manifest_walks_up_to_the_nearest_cargo_toml() {
let root = std::env::temp_dir().join(format!("tianheng-nearest-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let outer = root.join("outer");
let inner = outer.join("inner");
let leaf = inner.join("a").join("b");
std::fs::create_dir_all(&leaf).expect("mkdir leaf");
std::fs::write(outer.join("Cargo.toml"), "[workspace]\n").expect("write outer manifest");
assert_eq!(
nearest_manifest_from(leaf.clone()),
Some(outer.join("Cargo.toml")),
"the ascent finds the one Cargo.toml above the leaf",
);
std::fs::write(inner.join("Cargo.toml"), "[workspace]\n").expect("write inner manifest");
assert_eq!(
nearest_manifest_from(leaf.clone()),
Some(inner.join("Cargo.toml")),
"the nearest manifest wins over a farther ancestor",
);
assert_eq!(
nearest_manifest_from(inner.clone()),
Some(inner.join("Cargo.toml")),
"the start dir counts as its own nearest manifest",
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn write_baseline_preserves_hand_added_metadata_across_regeneration() {
let path = std::env::temp_dir().join(format!(
"tianheng-baseline-merge-{}.json",
std::process::id()
));
let path_str = path.to_str().expect("utf-8 temp path");
let _ = std::fs::remove_file(&path);
let outcome = Outcome::Violations(Report::new(vec![violation("core", "rule", "serde", None)]));
assert_eq!(super::write_baseline(&outcome, path_str), 0);
let first = std::fs::read_to_string(&path).expect("baseline written");
assert!(
!first.contains("owner"),
"fresh baseline has no metadata: {first}"
);
let annotated = first.replace(
"\"finding\": \"serde\"",
"\"finding\": \"serde\",\n \"owner\": \"team-core\",\n \"tracker\": \"ISSUE-7\"",
);
std::fs::write(&path, &annotated).expect("annotate");
assert_eq!(super::write_baseline(&outcome, path_str), 0);
let rewritten = std::fs::read_to_string(&path).expect("baseline rewritten");
let doc: Value = serde_json::from_str(&rewritten).expect("valid baseline json");
assert_eq!(doc["violations"][0]["owner"], "team-core");
assert_eq!(doc["violations"][0]["tracker"], "ISSUE-7");
let _ = std::fs::remove_file(&path);
}
#[test]
fn projection_gate_reacts_to_missing_stale_and_regenerates_on_bless() {
let dir = std::env::temp_dir().join(format!("tianheng-gate-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("sub").join("law.md");
let hint = "BLESS=1 cargo test";
let err = projection_gate("live", &path, hint, false).unwrap_err();
assert!(
err.contains("law.md") && err.contains(hint),
"missing must name path + hint: {err}"
);
projection_gate("live", &path, hint, true).expect("bless writes");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "live");
projection_gate("live", &path, hint, false).expect("fresh passes");
let err = projection_gate("different", &path, hint, false).unwrap_err();
assert!(
err.contains("law.md") && err.contains(hint) && err.contains("stale"),
"stale must name path + hint: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}