Skip to main content

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>(), &reg)
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>(), &reg)
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>(), &reg)
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>(), &reg)
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>(), &reg)
630            .unwrap()
631            .unwrap()
632            .0;
633        let b = check_crossing("seam", TypeId::of::<Domain>(), &reg)
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>(), &reg).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}