渾儀 / 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). The exposed surface includes named public re-exports: a
pub use crate::infra::DbPool;(and its aliased / grouped / facade-chained forms) republishes the forbidden type on the module's surface, somust_not_exposereacts to it by default; a glob re-export reacts when its root resolves in/under the forbidden set. (Re-export observation is a default-on behavior-changing bugfix — it closes a real false negative, so a previously-green adopter may newly react to a genuine leak; adopt via the baseline/warn rung as with any tightened reaction.) - Trait-impl exposure (opt-in depth) —
.including_trait_impls()extends signature-coupling from a module's items to a traitimplblock's impl-site-authored positions: the trait reference's generic arguments, theSelftype, associated type/const bindings, the impl's own generics /where-clause (including const-generic parameter types), and each method's return type. Trait-dictated positions (method parameters and the receiver) stay out of scope — they belong to the trait definition, which signature-coupling already governs. - Trait-impl locality — a trait may only be implemented in declared locations.
- Visibility — a module must not declare bare
pubitems. - Forbidden-marker — a module's types must not acquire a forbidden trait/derive.
- Dyn-trait — a module's public API must not expose trait-object (
dyn) syntax (the type-shape complement of signature-coupling: internaldynis fine; leaking dynamic dispatch across the declared seam is the violation). Two depths:must_not_expose_dyn()is shape-only (any exposeddynreacts), andmust_not_expose_dyn_of([...])is operand-scoped (only adynwhose principal trait resolves into the named set reacts — e.g. forbiddyn crate::Portwhile allowingdyn std::error::Error). An empty operand set degenerates to shape-only (anydyn), never a no-op; auto-trait markers (Send) are never operands; a principal trait outside the resolver's coverage (a bare std trait, macro/glob re-export) is the stated bound, never a silent pass of a resolvable operand. - Impl-trait — a module's public API must not return a written
impl Trait(RPIT), the existential complement of dyn-trait's dynamic dispatch: an RPIT at a seam leaks an unnameable type the caller cannot name or store, and silently commits to its auto-traits. Two depths:must_not_expose_impl_trait()is shape-only (any returnedimpl Traitreacts), andmust_not_expose_impl_trait_of([...])is operand-scoped (only a returnedimpl Traitwhose principal trait resolves into the named set reacts — e.g. allowimpl Iteratorbut forbidimpl crate::Port), an empty set degenerating to shape-only. Governs return positions only: argument-positionimpl Trait(APIT) is universal, not a leak, andasync fn's implicitimpl Futureis a distinct, out-of-scope existential form — both stated bounds, never silent misses; auto-trait markers are never operands. - Async-exposure — a module's public API must not declare an
async fn, the implicit existential complement of impl-trait: anasync fnleaks a compiler-insertedimpl Futureand commits the seam to async.must_not_expose_async_fn()is shape-only (any publicasync fnat the seam reacts), over public free fns / inherent methods / trait method declarations (trait-impl methods and private items excluded). It governs theasync fnsugar; a written-> impl Futureis impl-trait's domain. The finding is an owner-qualified item identity (async fn <Ty>::name(…)) so two same-named async fns never collide under the baseline. Declarative = "this seam is synchronous" by anchor scoping (a sync-core/async-edges layering), not a blanket "no async".
use ;
// exposure: my-app's public API must not leak crate::infra::DbPool
let expose = in_crate
.module
.must_not_expose
.because;
// impl locality: only crate::commands::* may impl Command
let locality = in_crate
.trait_
.only_implemented_in
.because;
// visibility: the `internal` module exposes no `pub`
let visibility = in_crate
.module
.must_not_declare_pub
.because;
// forbidden marker: domain types must not derive Serialize
let marker = in_crate
.module
.must_not_acquire
.because;
// dyn-trait (shape-only): the core's public seam must be statically dispatched
let dyn_boundary = in_crate
.module
.must_not_expose_dyn
.because;
// dyn-trait (operand-scoped): forbid leaking a dyn of a *named* trait, allowing others
let dyn_operand_boundary = in_crate
.module
.must_not_expose_dyn_of
.because;
// impl-trait (shape-only): the core's public seam must not return an existential impl Trait (RPIT)
let impl_trait_boundary = in_crate
.module
.must_not_expose_impl_trait
.because;
// impl-trait (operand-scoped): forbid returning an existential of a *named* trait, allowing others
let impl_trait_operand_boundary = in_crate
.module
.must_not_expose_impl_trait_of
.because;
// async-exposure: the core's public seam must be synchronous (no implicit impl Future)
let async_boundary = in_crate
.module
.must_not_expose_async_fn
.because;
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 (save a re-export glob whose root resolves in/under the
forbidden set, which reacts), cross-crate re-exports, macro-generated names, and
types knowable only through inference (e.g. a return-position impl Trait hiding a concrete
type). A #[path]-remapped module remains outside single-module resolution; 渾儀 does not govern
it through a same-named conventional orphan file. 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.