Expand description
天衡 (Tiānhéng) — reactive architectural governance.
Govern by reaction, not instruction. The balance (天衡, 玉衡): this crate is the
imperative shell + facade. It composes the 三儀 (圭表 static, 渾儀 semantic, 漏刻 runtime CI
face) into one reaction, and exposes a single declared Constitution plus run — the
CLI reaction that turns it into a process exit code (0 clean / 1 enforced violation /
2 constitution-or-scan error).
The functional cores (guibiao, hunyi, louke) do the observation and
comparison; this crate owns the side effects (argument parsing, filesystem, stdout/stderr)
and the composition. The cores must not depend on this shell — a crate-level invariant
Tianheng enforces on itself (tests/self_governance.rs).
One declared source, three projections. An adopter writes one Constitution carrying
every dimension’s boundaries; the static and semantic dimensions project as a CI exit code,
and 漏刻 projects both as a CI exit code (its probe-coverage audit, composed here) and, in
the adopter’s binary, as a runtime event (the prod face, consumed directly via louke).
The runtime boundaries declared here are the same objects the adopter hands to
louke::install at startup — the single source of truth, two faces.
Modules§
- prelude
- The public facade for declaring a constitution and running the reaction. The projection,
baseline, and scanner internals stay in the dimension crates; consumers go through
Constitution/run(andcheckfor the pure static core).
Structs§
- Async
Exposure Boundary - An async-exposure boundary: a module’s public API must not declare an
async fn. The implicit-existential complement ofImplTraitBoundary: anasync fnleaks a compiler-insertedimpl Future(and commits the seam’s contract to an async model), so where impl-trait forbids a written-> impl Future, this forbids theasync fnsugar (observed fromsyn::Signature.asyncness). Governs public free fns, public inherent methods, and public trait method declarations; trait-impl methods (asyncness dictated by the trait) and private items are excluded. Declarative intent by anchor scoping — “this declared seam is synchronous” (a sync-core/async-edges layering), not a blanket “no async”. - Baseline
- A recorded set of accepted violations — a generated observation snapshot, not policy. The gate fails only on violations absent from it.
- Baseline
Entry - One recorded baseline entry: an accepted violation’s identity plus optional
governance-tracking metadata.
owner(who owns this accepted debt) andtracker(the external issue tracking its fix) describe how the accepted violation is tracked after acceptance, not the basis of the law — so they are metadata only, never part of theViolationIdmatch key. There is deliberately noanchorhere: the governance anchor already rides the live boundary→violation (Violation::anchor), so a baseline copy would duplicate and drift from it. - Constitution
- A declared constitution composing every observation dimension’s boundaries — the single
source of truth, in Rust. The static (圭表) boundaries, the semantic (渾儀) bundle, and the
runtime (漏刻) boundaries hang off one builder, so adding a dimension is a field, never a new
runargument. - Crate
Boundary - A boundary attached to one crate target, with a human-readable reason.
- Crate
Target - A crate identified by its package name.
- DynTrait
Boundary - A dyn-trait boundary: a module’s public API must not expose trait-object (
dyn) syntax. The type-shape complement ofSemanticBoundary(signature-coupling): where that forbids an exposed named type, this forbids an exposed type shape — adynnode at any depth in the governed public surface. Internaldynis never a violation — this governs exposure across the declared seam, not internal dynamic dispatch, so it is intent (by anchor scoping), not a lint. Declared in Rust and composed with the other dimensions at the gate. - Forbidden
Marker Boundary - A forbidden-marker boundary: types defined in a module subtree must not acquire a
forbidden trait — by
#[derive(T)]or a hand-writtenimpl T for <a subtree type>. Declared in Rust and composed at the gate. The complement to exposure, impl-locality, and visibility; it delivers the “this layer is notT-able” intent. - Gnomon
Constitution - The governed shape, declared in Rust (the single source of truth).
- Impl
Trait Boundary - An impl-trait boundary: a module’s public API must not return a written
impl Trait(return-positionimpl Trait/ RPIT). The existential complement ofDynTraitBoundary: where that forbids the dynamic-dispatch shape (dyn), this forbids the existential shape — an unnameable type the caller cannot name, store without boxing, or rely on beyond its declared bounds. Governs return positions only: argument-positionimpl Trait(APIT) is universal (a caller-chosen generic), not an existential leak, and is never governed;async fn’s implicitimpl Futureis a distinct compiler-inserted existential, out of scope. Declared in Rust and composed with the other dimensions at the gate. - Module
Boundary - A boundary over the intra-crate module import graph — the layering Cargo cannot
see. Observed from the target crate’s source
usedeclarations (PROJECT.md). - Origin
Entry - An origin registration produced by
register_origin!— aTypeId, the observed origin (module_path!()at the registration site), and the type’s name (for findings). Pass these toinstall. - Report
- All violations from one evaluation.
- Runtime
Boundary - A runtime boundary: only the declared origins may cross the named seam. Declared in Rust (the single source of truth) and installed once at startup; a probe references the seam by name, so the policy lives in this declaration, not at the call site.
- Sans
IoPure - A sans-I/O purity profile: the two source-observable axes of a pure kernel composed into one
declaration — the core reads no ambient clock, and its public API is synchronous. Passed to
Constitution::sans_io_pure, it expands into a 圭表must_not_call_inlineboundary and a 渾儀must_not_expose_async_fnboundary on the module. Both reach the module’s subtree (the clock rule is inherently subtree-wide over the filesystem; the async rule opts in viaincluding_submodules, descending the declared mod-tree), so a pure kernel is sans-I/O throughout, not only at its own seam — though a#[path]-remapped module is the semantic dimension’s stated bound (the async half does not descend one). - Semantic
Boundaries - The 渾儀 (semantic) dimension’s boundaries, gathered so the shell takes the dimension as
one unit rather than one parameter per capability. Each field is one capability’s
boundaries;
check_allevaluates them all with a singlecargo metadataread. - Semantic
Boundary - A semantic boundary: the public API of a module must not expose any forbidden type. Declared in Rust (the single source of truth), alongside — and composed with — the static constitution at the gate. Each dimension owns its own declaration DSL and expresses findings in the shared 璇璣 model; the shell merges them into one reaction.
- Trait
Impl Boundary - A trait-impl-locality boundary: within a target crate, the named trait may be
implemented only inside the declared allowed module location(s). An
impl <Trait> for <Type>block outside them is a violation. Declared in Rust (the single source of truth) and composed with the other dimensions at the gate. This governs impl locality — the complement of exposure (SemanticBoundary) and of the static import boundary. It governs only the target crate’s own impl sites; it makes no claim about downstream crates (that would be external trait sealing, an essential gap). - Unsafe
Boundary - An unsafe-confinement boundary: within a target crate,
unsafe(blocks,unsafe fn/impl/trait,unsafe extern) may appear only under the declared subtree(s); a site outside all of them reacts. Declared in Rust (the single source of truth), composed at the gate. - Violation
- One violated boundary.
severityis the producing boundary’s severity, so the exit-code decision and the report can treat enforce and warn findings apart.baselinedis set when baseline gating records the violation in a baseline; a baselined violation does not fail the reaction. - Violation
Id - A violation’s identity for baseline matching:
(target, rule, finding). Reason and severity are excluded so editing them does not turn a known violation new. - Visibility
Boundary - A visibility boundary: a governed module must not declare any bare-
pubitems — a declared-visibility hygiene rule for an internal / impl-detail layer. The rule is syntactic (thepubkeyword on the module’s own direct items), not crate- reachability:pub(crate)/pub(super)/pub(in …)/private are allowed, and attribute- derived public surface (#[macro_export],#[no_mangle]) is out of scope (the deferred attribute capability’s domain). Declared in Rust and composed with the other dimensions at the gate.
Enums§
- Boundary
- One boundary, of either kind. Named
Boundary(umbrella) with the crate kind asCrateBoundary, since a module reaction is also a boundary (drift law D2). - Boundary
Kind - Which kind of boundary produced a violation — surfaced in the JSON report so a
consumer need not reverse-engineer the rule string. Not part of the baseline
identity (
ViolationId), so adding it does not invalidate existing baselines.#[non_exhaustive]: a further dimension (e.g. runtime) adds its own kind without breaking consumers. - Dependency
Kind - Which dependency table a crate rule observes. Defaults to
Normal. Mirrors cargo’s fixed set (normal / dev / build), so it is intentionally not#[non_exhaustive]— unlikeRule, this enum will not grow. - Module
Rule - What a module boundary forbids.
- Outcome
- The reaction’s outcome. Exit codes separate architectural drift (1) from a misconfiguration (2), so a mistyped target is not reported as drift.
- Polarity
- The repair direction a boundary-drift violation points to — a different axis from
BoundaryKind, which names which dimension saw it. Derived from the producing rule’s type (known at the reaction site), never observed from code and never declared by the adopter.#[non_exhaustive]: the axis is deliberately two-valued, but a future rung stays additive. - Posture
- How a violated boundary reacts in production.
Event(the default) emits a structuredViolation;Panicadditionally aborts — opt-in only, never the default. - Rule
- What a crate boundary forbids. Each variant is a reaction with an observation
source in
cargo metadata; no variant is named for a reaction that does not exist. - Severity
- How strongly a boundary reacts.
Enforcefails the reaction (exit 1);Warnreports the violation as advisory without failing — the first rung of adoption, so a dirty project can observe a boundary before enforcing it. - Source
Kind - A dependency’s declared source kind, classified from
cargo metadata’ssourcefield. The vocabulary of theRule::RestrictDependencySourcesToallowlist. LikeDependencyKind, it mirrors a fixed cargo distinction (a declared source is a registry, a git, or a path), so it is intentionally not#[non_exhaustive]: it will not grow. - Visibility
Ceiling - The maximum declared visibility a governed module’s direct items may carry. An item whose
declared-visibility rank is strictly above the ceiling reacts; at or below it passes.
(
Publicis deliberately not a ceiling — it would never react.)
Traits§
- Tracked
- The supertrait a governed trait carries so a probe can recover the concrete type behind
a
dyn Traitwithout trait upcasting (rust 1.85). Writetrait DomainPort: louke::Tracked; the blanket impl suppliesas_anyfor every'statictype, so no per-type boilerplate is needed. A&dyn Traitover borrowed (non-'static) data cannot be probed —Anyrequires'static(a stated bound).
Functions§
- audit_
probe_ coverage - CI face. Audit probe coverage against the declared
RuntimeBoundaryobjects (the authoritative seam set — the constitution, not a source scan for declarations) by scanning the workspace’ssrc_dirsforassert_boundary!probes. Reacts, with the static dimensions’ exit-code contract, in both directions plus an un-auditable case: - check
- Run the constitution’s boundaries against the Cargo workspace at
manifest_path. - check_
semantic - Run the semantic boundaries against the Cargo workspace at
manifest_path. - constitution_
markdown - Render a constitution as the human- and agent-readable Markdown summary of its declared law —
the same projection
list --format markdownprints, returned as aStringfor library callers (e.g. to generate an agent-context artifact). It composes the same internal projector, so it carries no less than the JSON and never reacts; it adds nothing of its own (no preamble, no trailing newline), so it equals the CLI output byte for byte. - projection_
gate - Gate a checked-in projection against the live one — the 潛移 staleness reaction, reusable by
adopters. Pair it with
super::constitution_markdown: generate a Markdown projection of your constitution into a checked-in file (so your agents read the reacted law in-context, the projection track), keep a short hand-written spine (AGENTS.md-style, the prose track), and gate the projection’s freshness in acargo testso the file can never drift from the declared law — the same mechanism Tianheng runs on its ownAGENTS.self-law.md. - run
- Run the unified constitution’s boundaries against a Cargo workspace and return the
process exit code. The one
Constitutioncarries every dimension — static (圭表), semantic (渾儀), and the runtime (漏刻) CI probe-coverage audit — which this gate composes into one reaction. A dimension with no declared boundaries contributes nothing.argsare the full process arguments (the program name is skipped internally, like a realmain). Passstd::env::args()from a binary. - workspace_
member_ src_ dirs - Resolve every workspace member’s source-root directory from the target workspace at
manifest_path, so a caller (the 天衡 shell, composing the 漏刻 runtime CI audit) can hand resolved&Paths to a dimension that must stay std-only and never readcargo metadataitself. Each root is the parent of the member’slib(elseproc-macro, elsebin) targetsrc_path— the same resolution the semantic dimension uses (both through 星表), not themanifest_dir/srcshortcut (which would silently miss a custom layout). An unreadable workspace is a constitution error, never a silent empty set.