louke/lib.rs
1//! 漏刻 (Lòukè) — the **runtime** observation dimension of Tianheng.
2//!
3//! Where 圭表 reads imports and 渾儀 reads the AST — both at CI time, against source —
4//! 漏刻 reacts at **runtime, in your binary**, against **live objects**: it sees the
5//! concrete type behind a `dyn Trait` crossing an architectural seam, which static and
6//! semantic analysis structurally cannot.
7//!
8//! Two faces:
9//! - **Prod face.** Declare `RuntimeBoundary::at("seam").only_origins([…])` and
10//! [`install`] it once at startup; opt a type into an **observed** origin with
11//! [`register_origin!`] (it captures `module_path!()` at the registration site — the
12//! origin is *where the type is registered*, not a free label); place
13//! [`assert_boundary!`]`("seam", obj)` at the seam. The probe reads the live object's
14//! concrete origin and reacts **fail-closed** (an unknown origin is not in the allowlist,
15//! so it reacts). The default reaction emits a [`xuanji::Violation`] event; `panic` is
16//! opt-in — a governance tool must never crash production on a false positive.
17//! - **CI face** (the non-default `audit` feature). `audit_probe_coverage` takes the **declared
18//! `RuntimeBoundary` objects** as
19//! the authoritative seam set and scans the workspace's source for `assert_boundary!` probes,
20//! so every declared seam has a probe (and every probe a declared seam) — closing the
21//! "declared but never enforced" gap at CI time. A non-literal probe seam is reacted to, not
22//! silently skipped. Declarations come from the objects, never a source scan, so an
23//! unconventionally spelled declaration cannot hide a seam. It lives in a dedicated `audit`
24//! module, compiled only under the feature.
25//!
26//! The hot path is std-only and lock-free (a write-once registry); the `TypeId`→origin lookup
27//! uses a fold-hasher rather than the default SipHash, while the tiny fixed seam-name map is an
28//! ordinary `std` map (its cost is negligible against the `TypeId` lookup). `serde_json` (via
29//! 璇璣) is used only on the cold path (emitting an event). 漏刻 depends on 璇璣 (`xuanji`) alone.
30//!
31//! Govern by reaction, not instruction.
32
33#![forbid(unsafe_code)]
34#![deny(missing_docs)]
35
36use std::any::{Any, TypeId};
37use std::collections::HashMap;
38use std::hash::{BuildHasherDefault, Hasher};
39use std::sync::OnceLock;
40
41pub use xuanji::{BoundaryKind, Outcome, Polarity, Report, Severity, Violation};
42
43// CI face (the non-default `audit` feature): the probe-coverage audit + source scanner, in its
44// own module so a prod dependency on louke compiles none of it. The prod face (the declaration
45// DSL, the write-once registry, and the fail-closed probe reaction) stays in this root module.
46#[cfg(feature = "audit")]
47mod audit;
48#[cfg(feature = "audit")]
49pub use audit::audit_probe_coverage;
50
51/// The canonical runtime seam-origin rule label — written **once** here and referenced by
52/// both the prod reaction (the crate's internal `check_crossing`) and the 天衡 shell's `list`
53/// projection (`tianheng` depends on `louke`, so importing this is the allowed direction). Editing it
54/// in one place updates every projection. The specific allowed-origin set is a per-boundary
55/// detail layered on at each site, not part of this rule-family label.
56pub const RUNTIME_SEAM_RULE: &str = "only declared origins may cross the seam";
57
58/// The full runtime seam **rule line** — the canonical [`RUNTIME_SEAM_RULE`] label with the
59/// per-boundary allowed-origin set folded in (`… (only origins: A, B)`). Written **once** here and
60/// shared by the prod reaction (`check_crossing`'s violation rule) and the 天衡 shell's
61/// `list --format text` projection, so the human-readable line the two render never drifts. The JSON
62/// projection deliberately keeps the label bare and carries the origins as a separate field, so it
63/// does not use this.
64pub fn runtime_seam_rule_line(allowed_origins: &[&str]) -> String {
65 format!(
66 "{RUNTIME_SEAM_RULE} (only origins: {})",
67 allowed_origins.join(", ")
68 )
69}
70
71// --- Tracked: the trait-level instrumentation -------------------------------
72
73/// The supertrait a governed trait carries so a probe can recover the concrete type behind
74/// a `dyn Trait` without trait upcasting (rust 1.85). Write `trait DomainPort: louke::Tracked`;
75/// the blanket impl supplies `as_any` for every `'static` type, so no per-type boilerplate is
76/// needed. A `&dyn Trait` over **borrowed** (non-`'static`) data cannot be probed — `Any`
77/// requires `'static` (a stated bound).
78pub trait Tracked: Any {
79 /// Recover `&dyn Any` (and thus the concrete `TypeId`) from a trait object.
80 fn as_any(&self) -> &dyn Any;
81}
82
83impl<T: Any> Tracked for T {
84 fn as_any(&self) -> &dyn Any {
85 self
86 }
87}
88
89// --- The fold-hasher: TypeId is already a hash; never SipHash ----------------
90
91/// A std-only hasher that folds the written bytes. A `TypeId` is already a good hash, so
92/// folding avoids SipHash's per-lookup cost (the overhead-spike's only trap). `write` is
93/// implemented for the general byte path (not only `write_u64`/`write_u128`), since a
94/// `TypeId`'s hash may route through either across toolchains.
95#[derive(Default)]
96struct FoldHasher(u64);
97
98impl Hasher for FoldHasher {
99 fn finish(&self) -> u64 {
100 self.0
101 }
102 fn write(&mut self, bytes: &[u8]) {
103 for &b in bytes {
104 self.0 = self.0.rotate_left(8) ^ b as u64;
105 }
106 }
107 fn write_u64(&mut self, i: u64) {
108 self.0 ^= i;
109 }
110 fn write_u128(&mut self, i: u128) {
111 self.0 ^= i as u64 ^ (i >> 64) as u64;
112 }
113}
114
115type TidMap<V> = HashMap<TypeId, V, BuildHasherDefault<FoldHasher>>;
116
117// --- Declaration DSL --------------------------------------------------------
118
119/// How a violated boundary reacts in production. `Event` (the default) emits a structured
120/// `Violation`; `Panic` additionally aborts — opt-in only, never the default.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum Posture {
123 /// Emit a `Violation` event to the sink and continue. The default.
124 Event,
125 /// Emit the event, then panic — opt-in only (`enforce` severity).
126 Panic,
127}
128
129impl Posture {
130 /// A stable lower-case label for projection (`list`/`--format json`).
131 pub fn as_str(&self) -> &'static str {
132 match self {
133 Posture::Event => "event",
134 Posture::Panic => "panic",
135 }
136 }
137}
138
139/// A runtime boundary: only the declared **origins** may cross the named **seam**. Declared
140/// in Rust (the single source of truth) and installed once at startup; a probe references the
141/// seam by name, so the policy lives in this declaration, not at the call site.
142#[derive(Debug, Clone)]
143pub struct RuntimeBoundary {
144 seam: &'static str,
145 allowed: Vec<&'static str>,
146 reason: String,
147 severity: Severity,
148 posture: Posture,
149 anchor: Option<String>,
150}
151
152impl RuntimeBoundary {
153 /// Begin a boundary at the named runtime seam.
154 pub fn at(seam: &'static str) -> RuntimeSeamDraft {
155 RuntimeSeamDraft { seam }
156 }
157
158 /// The governed seam name.
159 pub fn seam(&self) -> &str {
160 self.seam
161 }
162
163 /// The origins allowed to cross the seam.
164 pub fn allowed_origins(&self) -> &[&'static str] {
165 &self.allowed
166 }
167
168 /// The human-readable reason (the repair hint).
169 pub fn reason(&self) -> &str {
170 &self.reason
171 }
172
173 /// Attach a durable governance anchor (e.g. `"ADR-014"`) — a stable pointer into the
174 /// project's governance, distinct from the free-text `reason`. Optional; a boundary with
175 /// none projects and reacts exactly as before.
176 pub fn with_anchor(mut self, anchor: &str) -> Self {
177 self.anchor = Some(anchor.to_string());
178 self
179 }
180
181 /// The durable governance anchor recorded with the boundary, if any.
182 pub fn anchor(&self) -> Option<&str> {
183 self.anchor.as_deref()
184 }
185
186 /// The declared severity. The CI face reacts to a declared-but-unprobed seam at this
187 /// severity (a `warn` boundary yields an advisory, not a CI failure).
188 pub fn severity(&self) -> Severity {
189 self.severity
190 }
191
192 /// The declared production reaction posture (`Event` default, `Panic` opt-in). Exposed so the
193 /// `list` projection is faithful — a `panic_on_violation` boundary must not project identically
194 /// to a default event-only one.
195 pub fn posture(&self) -> Posture {
196 self.posture
197 }
198}
199
200/// A boundary awaiting its allowed-origin set.
201#[doc(hidden)]
202pub struct RuntimeSeamDraft {
203 seam: &'static str,
204}
205
206impl RuntimeSeamDraft {
207 /// Allow only the given origins (origin labels — typically a `module_path!()` captured by
208 /// [`register_origin!`]) to cross this seam.
209 pub fn only_origins<I>(self, origins: I) -> RuntimeBoundaryDraft
210 where
211 I: IntoIterator<Item = &'static str>,
212 {
213 RuntimeBoundaryDraft {
214 seam: self.seam,
215 allowed: origins.into_iter().collect(),
216 severity: Severity::Enforce,
217 posture: Posture::Event,
218 }
219 }
220}
221
222/// A boundary awaiting severity/posture (optional) and a reason.
223#[doc(hidden)]
224pub struct RuntimeBoundaryDraft {
225 seam: &'static str,
226 allowed: Vec<&'static str>,
227 severity: Severity,
228 posture: Posture,
229}
230
231impl RuntimeBoundaryDraft {
232 /// Make this advisory (`warn`): violations are reported but never panic, regardless of
233 /// posture — the first rung of adoption.
234 pub fn warn(mut self) -> Self {
235 self.severity = Severity::Warn;
236 self
237 }
238
239 /// Opt into panicking on an `enforce`-severity violation (default is event-only).
240 pub fn panic_on_violation(mut self) -> Self {
241 self.posture = Posture::Panic;
242 self
243 }
244
245 /// Finish the boundary with its human-readable reason (the repair hint).
246 pub fn because(self, reason: &str) -> RuntimeBoundary {
247 RuntimeBoundary {
248 seam: self.seam,
249 allowed: self.allowed,
250 reason: reason.to_string(),
251 severity: self.severity,
252 posture: self.posture,
253 anchor: None,
254 }
255 }
256}
257
258/// An origin registration produced by [`register_origin!`] — a `TypeId`, the **observed**
259/// origin (`module_path!()` at the registration site), and the type's name (for findings).
260/// Pass these to [`install`].
261#[derive(Debug, Clone)]
262pub struct OriginEntry {
263 type_id: TypeId,
264 origin: &'static str,
265 type_name: &'static str,
266}
267
268impl OriginEntry {
269 /// Construct an origin entry. Prefer [`register_origin!`], which captures the call-site
270 /// `module_path!()` so the origin is observed, not hand-asserted.
271 pub fn new(type_id: TypeId, origin: &'static str, type_name: &'static str) -> Self {
272 OriginEntry {
273 type_id,
274 origin,
275 type_name,
276 }
277 }
278}
279
280// --- The write-once registry ------------------------------------------------
281
282struct Seam {
283 allowed: Vec<&'static str>,
284 reason: String,
285 severity: Severity,
286 posture: Posture,
287 anchor: Option<String>,
288}
289
290struct OriginInfo {
291 origin: &'static str,
292 type_name: &'static str,
293}
294
295struct Registry {
296 origins: TidMap<OriginInfo>,
297 seams: HashMap<&'static str, Seam>,
298}
299
300static REGISTRY: OnceLock<Registry> = OnceLock::new();
301
302/// Install the runtime constitution **once at startup**: the declared boundaries and the
303/// origin registrations ([`register_origin!`]). The registry is **write-once** so the probe
304/// hot path reads it without a lock; calling `install` a second time is a constitution error
305/// (it fails loud rather than silently replacing the law).
306pub fn install<B, O>(boundaries: B, origins: O)
307where
308 B: IntoIterator<Item = RuntimeBoundary>,
309 O: IntoIterator<Item = OriginEntry>,
310{
311 let mut seams = HashMap::new();
312 for b in boundaries {
313 // A seam declared twice is an observable misconfiguration: a silent overwrite would let
314 // the last declaration shadow the earlier law (a declared boundary that never enforces —
315 // the one bug this tool forbids). Fail loud, like a constitution error.
316 if seams.contains_key(b.seam) {
317 panic!(
318 "louke: runtime seam '{}' declared more than once — each seam is declared exactly \
319 once (a duplicate would silently shadow the earlier boundary)",
320 b.seam
321 );
322 }
323 seams.insert(
324 b.seam,
325 Seam {
326 allowed: b.allowed,
327 reason: b.reason,
328 severity: b.severity,
329 posture: b.posture,
330 anchor: b.anchor,
331 },
332 );
333 }
334 let mut origin_map: TidMap<OriginInfo> = TidMap::default();
335 for e in origins {
336 // Same type registered twice (e.g. two `register_origin!` sites) would silently keep the
337 // last origin — fail loud rather than let an observed origin be silently replaced.
338 if origin_map.contains_key(&e.type_id) {
339 panic!(
340 "louke: an origin for type '{}' was registered more than once — each type \
341 registers its origin exactly once",
342 e.type_name
343 );
344 }
345 origin_map.insert(
346 e.type_id,
347 OriginInfo {
348 origin: e.origin,
349 type_name: e.type_name,
350 },
351 );
352 }
353 if REGISTRY
354 .set(Registry {
355 origins: origin_map,
356 seams,
357 })
358 .is_err()
359 {
360 panic!("louke: install called twice — the runtime constitution is write-once");
361 }
362}
363
364// --- The pure core ----------------------------------------------------------
365
366/// The pure reaction, testable without process-global state: resolve the seam (an undeclared
367/// seam is a constitution error), resolve the crossing type's origin (an unregistered type
368/// has none), and match the allowlist **fail-closed** — an origin not in the allowlist, or
369/// an unknown origin, reacts. Returns the `Violation` to react with **and the seam's posture**
370/// (already resolved here, so the caller need not look the seam up a second time), or `None` when
371/// clean.
372fn check_crossing(
373 seam: &str,
374 type_id: TypeId,
375 registry: &Registry,
376) -> Result<Option<(Violation, Posture)>, String> {
377 let s = registry.seams.get(seam).ok_or_else(|| {
378 // Reason-led, aligned with the CI-audit twin: name the intent (an undeclared seam is
379 // never enforced) before the mechanics. Keeps the `undeclared runtime seam '{seam}'`
380 // substring the prod contract and tests depend on.
381 format!(
382 "an undeclared seam is never enforced — declare the RuntimeBoundary or fix the \
383 probe's seam name: probe references undeclared runtime seam '{seam}'"
384 )
385 })?;
386
387 let info = registry.origins.get(&type_id);
388 let origin = info.map(|i| i.origin);
389 let allowed = match origin {
390 Some(o) => s.allowed.contains(&o),
391 // Fail-closed: a type that never registered an origin is not in the allowlist.
392 None => false,
393 };
394 if allowed {
395 return Ok(None);
396 }
397
398 let finding = match info {
399 Some(i) => format!("{} ({})", i.origin, i.type_name),
400 // An unregistered type recorded neither an origin nor a name, so the only per-type datum a
401 // crossing carries is its `TypeId` (from `Any`; a concrete type NAME is unrecoverable from a
402 // `&dyn Any` on stable Rust). Append it so two DISTINCT unregistered types crossing the same
403 // seam produce distinct `(target, rule, finding)` identities — otherwise baselining one
404 // silently masks the other (a false negative). The `<unregistered origin>` prefix is kept so
405 // the substring the prod contract/tests depend on still holds; the TypeId is stable within a
406 // build (a hash of the type's identity).
407 None => format!("<unregistered origin> {type_id:?}"),
408 };
409 // The rule family is the canonical `RUNTIME_SEAM_RULE` label; the allowed-origin set is the
410 // per-boundary detail appended here, so the prod reaction and the `list` projection share one
411 // rule label (the shell's `runtime` projection references the same const).
412 let rule = runtime_seam_rule_line(&s.allowed);
413 Ok(Some((
414 Violation::new(
415 BoundaryKind::Runtime,
416 seam.to_string(),
417 rule,
418 finding,
419 s.reason.clone(),
420 s.severity,
421 )
422 .with_anchor(s.anchor.clone())
423 .with_polarity(Polarity::AllowlistGap),
424 s.posture,
425 )))
426}
427
428// --- The probe reaction (hot path) ------------------------------------------
429
430#[doc(hidden)]
431pub fn __react(seam: &'static str, type_id: TypeId) {
432 let registry = REGISTRY.get().unwrap_or_else(|| {
433 panic!("louke: assert_boundary!(\"{seam}\", …) ran before louke::install")
434 });
435 match check_crossing(seam, type_id, registry) {
436 Ok(None) => {}
437 Ok(Some((violation, posture))) => {
438 emit(&violation);
439 // `warn` is always event-only; panic only on an opted-in enforce violation.
440 if posture == Posture::Panic && violation.severity == Severity::Enforce {
441 panic!(
442 "louke: runtime boundary '{seam}' violated by {}",
443 violation.finding
444 );
445 }
446 }
447 // An undeclared seam (or probe-before-install) is a constitution error: fail loud,
448 // never silently pass — the runtime analogue of exit 2.
449 Err(message) => panic!("louke constitution error: {message}"),
450 }
451}
452
453// --- The reaction sink ------------------------------------------------------
454
455#[allow(clippy::type_complexity)]
456static SINK: OnceLock<Box<dyn Fn(&Violation) + Send + Sync>> = OnceLock::new();
457
458/// Install the sink that receives runtime `Violation` events (a logger, an audit pipeline).
459/// Set once at startup; the default sink (used if none is installed) prints the violation as
460/// JSON to stderr.
461pub fn set_sink<F>(sink: F)
462where
463 F: Fn(&Violation) + Send + Sync + 'static,
464{
465 if SINK.set(Box::new(sink)).is_err() {
466 panic!("louke: set_sink called twice — the sink is set once at startup");
467 }
468}
469
470fn emit(violation: &Violation) {
471 match SINK.get() {
472 Some(sink) => sink(violation),
473 // The default sink runs on every violation under the default `Event` posture, *before*
474 // the opt-in panic gate — so it must never itself panic. `eprintln!` panics if the stderr
475 // write fails (a closed/broken pipe, `… 2>&1 | consumer` after the consumer exits), which
476 // would crash the production process on a reaction — the exact failure the crate's
477 // no-panic-on-false-positive invariant forbids. Write directly and ignore a write error.
478 None => {
479 use std::io::Write;
480 let _ = writeln!(
481 std::io::stderr(),
482 "louke: runtime boundary violated\n{}",
483 xuanji::pretty_json(&violation.to_json())
484 );
485 }
486 }
487}
488
489// --- Macros -----------------------------------------------------------------
490
491/// Register a type's **observed** origin: `register_origin!(PostgresRepo)` captures
492/// `module_path!()` at the call site (so the origin is *where the type is registered*, not a
493/// self-asserted label) and yields an [`OriginEntry`] to pass to [`install`]. Declarative —
494/// no proc-macro, no `syn`.
495#[macro_export]
496macro_rules! register_origin {
497 ($ty:ty) => {
498 $crate::OriginEntry::new(
499 ::std::any::TypeId::of::<$ty>(),
500 ::std::module_path!(),
501 ::std::any::type_name::<$ty>(),
502 )
503 };
504}
505
506/// Probe a runtime seam: `assert_boundary!("domain-entry", obj)` reads `obj`'s concrete
507/// origin (via the [`Tracked`] supertrait on its trait) and reacts fail-closed against the
508/// seam's allowlist. `obj` must be a reference to a `dyn Trait` whose trait carries
509/// `: louke::Tracked`.
510#[macro_export]
511macro_rules! assert_boundary {
512 ($seam:expr, $obj:expr) => {
513 $crate::__react($seam, $crate::Tracked::as_any($obj).type_id())
514 };
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 // Build a registry directly (the pure core needs no globals — so tests never touch the
522 // process-global write-once REGISTRY/SINK and can run in parallel).
523 fn registry(
524 seams: &[(&'static str, &[&'static str], Severity)],
525 origins: &[(TypeId, &'static str, &'static str)],
526 ) -> Registry {
527 let mut s = HashMap::new();
528 for (seam, allowed, severity) in seams {
529 s.insert(
530 *seam,
531 Seam {
532 allowed: allowed.to_vec(),
533 reason: "r".to_string(),
534 severity: *severity,
535 posture: Posture::Event,
536 anchor: None,
537 },
538 );
539 }
540 let mut o: TidMap<OriginInfo> = TidMap::default();
541 for (tid, origin, name) in origins {
542 o.insert(
543 *tid,
544 OriginInfo {
545 origin,
546 type_name: name,
547 },
548 );
549 }
550 Registry {
551 origins: o,
552 seams: s,
553 }
554 }
555
556 struct Domain;
557 struct Infra;
558
559 #[test]
560 fn an_allowed_origin_passes() {
561 let reg = registry(
562 &[("seam", &["app::domain"], Severity::Enforce)],
563 &[(TypeId::of::<Domain>(), "app::domain", "Domain")],
564 );
565 assert!(
566 check_crossing("seam", TypeId::of::<Domain>(), ®)
567 .unwrap()
568 .is_none()
569 );
570 }
571
572 #[test]
573 fn a_disallowed_origin_reacts() {
574 let reg = registry(
575 &[("seam", &["app::domain"], Severity::Enforce)],
576 &[(TypeId::of::<Infra>(), "app::infra", "Infra")],
577 );
578 let (v, _posture) = check_crossing("seam", TypeId::of::<Infra>(), ®)
579 .unwrap()
580 .unwrap();
581 assert_eq!(v.kind, BoundaryKind::Runtime);
582 assert!(v.finding.contains("app::infra"));
583 // This is the prod default-sink violation (emitted via `to_json`). An origin-assertion
584 // violation names an origin, not a source file, so its `file` is `None` and the
585 // emitted JSON carries `file: null` — the additive, non-breaking effect of the shared
586 // `to_json` gaining a `file` key, asserted here on the default-sink path.
587 assert!(
588 v.file.is_none(),
589 "an origin-assertion violation has no source file"
590 );
591 assert!(
592 v.to_json()["file"].is_null(),
593 "the prod default-sink JSON carries file: null"
594 );
595 }
596
597 #[test]
598 fn an_unknown_origin_reacts_fail_closed() {
599 let reg = registry(&[("seam", &["app::domain"], Severity::Enforce)], &[]);
600 let (v, _posture) = check_crossing("seam", TypeId::of::<Infra>(), ®)
601 .unwrap()
602 .unwrap();
603 assert!(v.finding.contains("<unregistered origin>"), "{}", v.finding);
604 }
605
606 #[test]
607 fn the_runtime_rule_line_is_shared_by_reaction_and_projection() {
608 // The folded `… (only origins: …)` wording lives once in `runtime_seam_rule_line`; the prod
609 // reaction (`check_crossing`) and the shell's text projection both call it, so the two
610 // human-readable renderings cannot drift (the twin-drift bug class).
611 assert_eq!(
612 runtime_seam_rule_line(&["app::domain", "app::api"]),
613 "only declared origins may cross the seam (only origins: app::domain, app::api)",
614 );
615 // The reaction's violation `rule` is exactly that formatter's output.
616 let reg = registry(&[("seam", &["app::domain"], Severity::Enforce)], &[]);
617 let (v, _) = check_crossing("seam", TypeId::of::<Infra>(), ®)
618 .unwrap()
619 .unwrap();
620 assert_eq!(v.rule, runtime_seam_rule_line(&["app::domain"]));
621 }
622
623 #[test]
624 fn distinct_unregistered_types_stay_distinct_findings() {
625 // Two DIFFERENT unregistered types crossing the
626 // same seam must not share one Violation identity — otherwise baselining one silently masks
627 // the other's later crossing (a false negative). The TypeId discriminant keeps them distinct.
628 let reg = registry(&[("seam", &["app::domain"], Severity::Enforce)], &[]);
629 let a = check_crossing("seam", TypeId::of::<Infra>(), ®)
630 .unwrap()
631 .unwrap()
632 .0;
633 let b = check_crossing("seam", TypeId::of::<Domain>(), ®)
634 .unwrap()
635 .unwrap()
636 .0;
637 assert!(a.finding.contains("<unregistered origin>"));
638 assert!(b.finding.contains("<unregistered origin>"));
639 assert_ne!(
640 a.id(),
641 b.id(),
642 "distinct unregistered types must have distinct Violation ids: {} vs {}",
643 a.finding,
644 b.finding
645 );
646 }
647
648 #[test]
649 fn an_undeclared_seam_is_a_constitution_error() {
650 let reg = registry(&[], &[]);
651 let err = check_crossing("ghost", TypeId::of::<Domain>(), ®).unwrap_err();
652 assert!(err.contains("undeclared runtime seam 'ghost'"), "{err}");
653 }
654
655 #[test]
656 fn the_builder_carries_posture_and_severity() {
657 let b = RuntimeBoundary::at("s")
658 .only_origins(["app::domain"])
659 .panic_on_violation()
660 .warn()
661 .because("r");
662 assert_eq!(b.seam(), "s");
663 assert_eq!(b.allowed_origins(), &["app::domain"]);
664 }
665
666 #[test]
667 fn the_fold_hasher_distinguishes_types() {
668 let mut m: TidMap<u8> = TidMap::default();
669 m.insert(TypeId::of::<Domain>(), 1);
670 m.insert(TypeId::of::<Infra>(), 2);
671 assert_eq!(m.get(&TypeId::of::<Domain>()), Some(&1));
672 assert_eq!(m.get(&TypeId::of::<Infra>()), Some(&2));
673 assert_eq!(m.len(), 2);
674 }
675}