hunyi 0.1.1

渾儀 (Hunyi) — the semantic observation dimension of Tianheng: declare in Rust how a module's public surface must behave — what types its API must not expose, where a trait may be implemented, that a module declares no bare pub, and which markers a type must not acquire — observed via the AST (syn), reacted in CI. The semantic complement of the static import boundary; the syn dependency is quarantined here, never in the core.
Documentation

渾儀 / hunyi

渾環察象,形義無隱。The armillary discerns the figure; neither form nor meaning can hide.

The semantic observation dimension of Tianheng — the armillary.

渾儀 (the armillary sphere) observes meaning via the AST (syn) — what the static use-scan structurally cannot see: public signatures, impl Trait for Type, visibility, and attributes/derives. It is the semantic companion to the static import boundary. The heavy syn dependency is quarantined here, never in the static core or the reaction model.

Built capabilities (each passing Tianheng's capability-admission test — declarative, no essential gap, anchorable):

  • Signature-coupling (flagship) — a module's public API must not expose a forbidden type (depending on it internally is fine; leaking it across the public surface is the violation).
  • Trait-impl locality — a trait may only be implemented in declared locations.
  • Visibility — a module must not declare bare pub items.
  • Forbidden-marker — a module's types must not acquire a forbidden trait/derive.
use hunyi::{SemanticBoundary, TraitImplBoundary, VisibilityBoundary, ForbiddenMarkerBoundary};

// exposure: my-app's public API must not leak crate::infra::DbPool
let expose = SemanticBoundary::in_crate("my-app")
    .module("crate::api")
    .must_not_expose("crate::infra::DbPool")
    .because("the API must not leak the database pool");

// impl locality: only crate::commands::* may impl Command
let locality = TraitImplBoundary::in_crate("my-app")
    .trait_("crate::Command")
    .only_implemented_in("crate::commands")
    .because("commands are registered in one place");

// visibility: the `internal` module exposes no `pub`
let visibility = VisibilityBoundary::in_crate("my-app")
    .module("crate::internal")
    .must_not_declare_pub()
    .because("internal is crate-private by contract");

// forbidden marker: domain types must not derive Serialize
let marker = ForbiddenMarkerBoundary::in_crate("my-app")
    .module("crate::domain")
    .must_not_acquire("serde::Serialize")
    .because("domain types must not be wire-coupled");

Stated bounds (never silently passed): local pub use re-export chains — including multi-hop and as-aliased ones — are followed to the item they name. What syn's syntactic view cannot see is glob imports, cross-crate re-exports, macro-generated names, and types knowable only through inference (e.g. a return-position impl Trait hiding a concrete type). These gaps are stated, not silently passed; an unresolvable anchor is a constitution error, never a silent pass. Explicitly rejected as false-negative engines: Send/Sync (inferred auto-traits), external trait sealing, and transitive effect-purity.

Most adopters consume the semantic dimension through the tianheng shell, which composes these boundaries into one reaction.

License

Licensed under either of Apache-2.0 or MIT, at your option.