Claims & the Law
While effect contracts verify what an individual function or locus may do, claims verify architectural invariants across an entire assembled program graph. Declared in a claims { } block on main locus (or in a library seed or reusable constitution), claims are evaluated statically by hale check as hard build errors and compile to zero runtime code.
type Task { id: Int; }type Metric { n: Int; }topic Tasks { payload: Task; }topic Metrics { payload: Metric; }
locus DeltaTriage { params { seen: Int = 0; } bus { subscribe Tasks as on_task; publish Metrics; } fn on_task(t: Task) { self.seen = self.seen + 1; Metrics <- Metric { n: t.id }; }}
locus GammaResearch { params { total: Int = 0; } bus { subscribe Metrics as on_metric; } fn on_metric(m: Metric) { self.total = self.total + m.n; }}
group delta_wing = { DeltaTriage };group gamma_wing = { GammaResearch };
main locus Org { params { triage: DeltaTriage = DeltaTriage { }; research: GammaResearch = GammaResearch { }; } claims { iso_dg: forbid reaches(delta_wing, gamma_wing); }}
fn main() { Org { }; }When a claim fails, hale check rejects the build and prints a witness—the minimal call and bus path violating the invariant:
claim `iso_dg` violated: `delta_wing` reaches `gamma_wing` —witness: `DeltaTriage::on_task` -(publishes "Metrics")-> `GammaResearch::on_metric`For the development workflow of writing architectural claims before implementation, see Claim-Driven Development in Hale.
Syntax Cheat Sheet
Section titled “Syntax Cheat Sheet”Vocabulary Declarations
Section titled “Vocabulary Declarations”| Declaration | Syntax | Description |
|---|---|---|
| Group | group g = { LocusA, fn_b, lib::Item, lib::*, role R, std::log::* } [may_be_empty]; |
Names a set of loci, free functions, imported seed members, API role handlers, or Hale-source stdlib modules. |
Built-in program Group |
program (no declaration needed) |
Implicitly selects every locus and free function declared in the closing entrypoint’s own seed (excluding imports). |
| Domain | domain wing = { delta, gamma }; |
Declares a finite, closed set of partition or tenant identifiers. |
| Indexed Effect Family | effect knowledge(wing); |
Expands a domain into per-member effect classes (knowledge(delta), knowledge(gamma)) and a union wildcard (knowledge(*)). |
Claim Sentences (claims { <name>: <sentence>; })
Section titled “Claim Sentences (claims { <name>: <sentence>; })”| Claim Family | Sentence Syntax | What It Proves |
|---|---|---|
| Reachability Prohibition | forbid reaches(SRC, DST) [via { calls, bus }] [during <phase>] [avoiding <gate>]; |
No path in the chosen edge relation (calls, bus, or both) connects group SRC (optionally filtered to <phase>) to group or effects(C) DST without passing through <gate> (<group> or std::<mod>::*). |
| Direct Boundary Allowlist | only edges A -> B { publish T; subscribe U;}; |
Every direct edge from group A into group B is a bus edge on one of the explicitly listed topics (call edges are never allowed). |
| Capability Budget | bound C <= N on paths from G; |
Every execution path starting from group G invokes carriers of user effect class C (or secret_use) at most N times across calls and bus hops. |
| Bus Wiring Existence | require subscribes(some G, topic T);require publishes(some G, topic T); |
At least one locus in group G declares a subscription (or publication) for topic T in its bus { } block. |
| State Encapsulation | require sealed(all G); |
Every locus in group G is annotated with @sealed. |
| Effect Attribution | require attributed(all C); |
Every function that directly performs built-in effect class C also declares a user-defined purpose class via @effects(is: {...}). |
| Execution Domain Separation | require separate_domains(all G1, all G2); |
Every arranged locus instance in G1 runs on a distinct execution domain (pool or pinned thread) from every arranged instance in G2. |
| Lossless Bus Routing | require no_silent_loss(topic T);require no_silent_loss(all G); |
No boundary on topic T (or on any route touched by group G) is configured to silently drop messages (swallow, drop_old, drop_new, or discard, transport drop). |
| Seed Topic Coverage | cover topic in seed(alias): subscribed_by(some G); |
Every topic declared in the seed imported as alias has at least one subscriber in group G. |
| Keyed Routing Partition | cover keys(topic T [in LO..=HI]): delivered_to(exactly_one G); |
Every permitted key on keyed topic T is delivered to exactly one arranged subscriber instance in group G. |
| Endpoint Cardinality | count publishers(topic T) (== | <= | >=) N;count subscribers(topic T) (== | <= | >=) N; |
The number of distinct declared loci publishing or subscribing to topic T satisfies the comparison against N. |
| Constitution Adoption | adopt ConstitutionName; |
Includes all claim sentences from ConstitutionName (and its extends ancestors) into this claims block. |
Formal Grammar
Section titled “Formal Grammar”From spec/grammar.ebnf:
group_decl = "group" , IDENTIFIER , "=" , "{" , [ group_member , { "," , group_member } , [ "," ] ] , "}" , [ "may_be_empty" ] , ";" ;group_member = IDENTIFIER , { "::" , IDENTIFIER } , [ "::" , "*" ] | "role" , IDENTIFIER ;
domain_decl = "domain" , IDENTIFIER , "=" , "{" , IDENTIFIER , { "," , IDENTIFIER } , [ "," ] , "}" , ";" ;effect_family_decl = "effect" , IDENTIFIER , "(" , IDENTIFIER , ")" , ";" ;
claims_block = "claims" , "{" , { claim_entry } , "}" ; (* inside main locus: the world tier; at top level: the library tier *)claim_entry = ( IDENTIFIER , ":" , claim_form , ";" ) | ( "adopt" , IDENTIFIER , ";" ) ;claim_form = forbid_form | only_edges_form | bound_form | require_form | cover_form | route_form | count_form ;
forbid_form = "forbid" , "reaches" , "(" , claim_set , "," , claim_set , ")" , { via_clause | during_clause | avoiding_clause } ;via_clause = "via" , "{" , via_relation , { "," , via_relation } , [ "," ] , "}" ;via_relation = "calls" | "bus" ;during_clause = "during" , IDENTIFIER ;avoiding_clause = "avoiding" , IDENTIFIER ;
only_edges_form = "only" , "edges" , IDENTIFIER , "->" , IDENTIFIER , "{" , { edge_grant } , "}" ;edge_grant = ( "publish" | "subscribe" ) , topic_ref , ";" ;
bound_form = "bound" , effect_class_ref , "<=" , INT_LIT , "on" , "paths" , "from" , IDENTIFIER ;
require_form = require_endpoint | require_sealed | require_attributed | require_no_silent_loss | require_separate_domains ;require_separate_domains = "require" , "separate_domains" , "(" , "all" , IDENTIFIER , "," , "all" , IDENTIFIER , ")" ;require_endpoint = "require" , ( "subscribes" | "publishes" ) , "(" , "some" , IDENTIFIER , "," , "topic" , topic_ref , ")" ;require_sealed = "require" , "sealed" , "(" , "all" , IDENTIFIER , ")" ;require_attributed = "require" , "attributed" , "(" , "all" , IDENTIFIER , ")" ;require_no_silent_loss = "require" , "no_silent_loss" , "(" , ( ( "topic" , topic_ref ) | ( "all" , IDENTIFIER ) ) , ")" ;
cover_form = "cover" , "topic" , "in" , "seed" , "(" , IDENTIFIER , ")" , ":" , "subscribed_by" , "(" , "some" , IDENTIFIER , ")" ;
route_form = "cover" , "keys" , "(" , "topic" , topic_ref , [ "in" , key_bound , "..=" , key_bound ] , ")" , ":" , "delivered_to" , "(" , "exactly_one" , IDENTIFIER , ")" ;key_bound = [ "-" ] , INT_LIT ;count_form = "count" , ( "publishers" | "subscribers" ) , "(" , "topic" , topic_ref , ")" , ( "==" | "<=" | ">=" ) , INT_LIT ;
claim_set = IDENTIFIER | "effects" , "(" , effect_class_ref , ")" ;topic_ref = IDENTIFIER , { "::" , IDENTIFIER } ;effect_class_ref = IDENTIFIER , [ "(" , ( IDENTIFIER | "*" ) , ")" ] ;All claim introducers (group, domain, claims, forbid, reaches, via, during, avoiding, only, edges, bound, require, cover, count, seed, may_be_empty, and predicate names) are contextual keywords: they are recognized only in grammar position and remain valid as ordinary identifiers elsewhere. Only existing language keywords (bus, publish, subscribe, on, in) are reserved everywhere.
group — the vocabulary
Section titled “group — the vocabulary”group delta_wing = { delta::*, DeltaStore, helper_fn };group gamma_wing = { gamma::Research };group probes = { } may_be_empty;A group names a set of declared program elements (loci, free functions, and imported declarations).
Group Member Forms
Section titled “Group Member Forms”| Member Form | Resolution Semantics |
|---|---|
Name |
A locus or free function declared in the current seed. If a locus and a free function share Name, both are included. |
alias::Name |
A declaration imported under import "..." as alias;, canonicalized through the compiler’s symbol table. |
alias::* |
Every locus and free function declared in the seed imported as alias (types, topics, and constants are ignored). Single-level and trailing-only (a::*::b and a::b::* are rejected). |
role R |
Every API surface handler method callable by a principal holding role R (see Role groups). |
std::m::* |
Every locus and free function in the Hale-source stdlib module std::m (e.g., std::log::*). Native-lowered stdlib modules (std::math) are rejected. Allowed only as a forbid reaches target or avoiding gate. |
program (built-in) |
Implicitly available in every claim without declaration: includes every locus and free function in the closing entrypoint’s own seed (excluding imports). Redeclaring group program is an error. |
Function-Grained Projection and Compile-Time Guards
Section titled “Function-Grained Projection and Compile-Time Guards”Claims evaluate at function granularity. Including a locus in a group projects to all of its member fns, lifecycle hooks (birth, accept, release, run, drain, dissolve), and modes (bulk, harmonic, resolution). A free function projects to itself.
To prevent claims from passing vacuously due to typos or empty sets, hale check rejects:
- Unknown single-segment names or undeclared roles (with a did-you-mean hint).
- Unresolved
alias::Namemembers oralias::*globs over unimported aliases. - A
role Rmember that noapisurface row requires, unless the group is markedmay_be_empty. - Duplicate group declarations.
- Any group that resolves to zero declarations, unless explicitly suffixed with
may_be_empty. - Projection vacuity: A non-empty group whose members contain zero executable methods/hooks/modes (e.g., pure-data loci) when used as a
forbidoronly edgesendpoint or aboundsource.
Role groups
Section titled “Role groups”While requires: [reader] on an api surface row enforces authorization at the transport boundary, a role <name> group member lets you prove what code paths an authorized caller can trigger inside the program:
role reader;role editor includes reader;
type Change { what: String; }type Query { n: Int; }topic TodoChanged { payload: Change; }
locus Todos { params { n: Int = 0; } bus { publish TodoChanged; } fn add(q: Query) -> Int { self.n = self.n + q.n; TodoChanged <- Change { what: "added" }; return self.n; } fn list(q: Query) -> Int { return self.n; }}
locus Feed { params { seen: Int = 0; } bus { subscribe TodoChanged as on_change; } fn on_change(c: Change) { self.seen = self.seen + 1; }}
api Todo { rpc Todos::add requires: [editor]; rpc Todos::list requires: [reader];}
group read_side = { role reader };group changes = { Feed };
main locus App { params { todos: Todos = Todos { }; feed: Feed = Feed { }; } claims { a_reader_publishes_nothing: forbid reaches(read_side, changes); }}fn main() { App { }; }role R selects the specific handler method (not the entire locus) of every surface row across the program that a caller holding R is permitted to invoke:
- Because
editor includes reader,role editorselects bothTodos::addandTodos::list, whereasrole readerselects onlyTodos::list. - Any surface row that requires no role (
requires: []) is callable by all principals and is included in everyrolegroup.
domain and effect families — the data-plane vocabulary
Section titled “domain and effect families — the data-plane vocabulary”domain wing = { delta, gamma };
effect llm;effect knowledge(wing);
@effects(is: {knowledge(delta)})fn read(key: String) -> Idea { ... }domain <name> = { <id1>, <id2>, ... };declares a non-empty, closed set of identifiers. It must appear earlier in the same file than anyeffectfamily referencing it.effect <family>(<domain>);interns an atomic effect class for each domain member (knowledge(delta),knowledge(gamma)) and a composed wildcard classknowledge(*)equal to the union of all members.- Indexed classes work everywhere ordinary effect classes work:
@effects(is:/only:/none:/causes:), composed effects (effect sensitive = { knowledge(delta) };), claim targets (effects(knowledge(delta)),effects(knowledge(*))),bound, and@budget(knowledge(delta) = N). - Adding a new identifier to a
domainautomatically places that new instantiation outside existing@effects(only: {...})allowlists.
claims { } Placement and Scoping Tiers
Section titled “claims { } Placement and Scoping Tiers”A claims { } block can appear in two places:
- World Tier (inside
main locus): Evaluates over the complete, closed application graph. Amain locusmay contain multipleclaims { }blocks; their entries are concatenated. Inline claims resolve group names within the seed that declares the claim (so importing an application seed into a test preserves the application’s own group bindings). - Library Tier (at top level in a library seed with no
main locus): Allows a library seed to state invariants over its own declarations and boundary (see Library-Tier Claims). A seed that declares amain locuscannot also have a top-levelclaims { }block.
Every claim entry is written <name>: <form>;. Claim names must be unique across all blocks in a seed, and every failed claim is a hard compile error.
Claim Sentences Reference
Section titled “Claim Sentences Reference”1. Reachability Prohibition (forbid reaches)
Section titled “1. Reachability Prohibition (forbid reaches)”iso: forbid reaches(delta_wing, gamma_wing);iso_calls: forbid reaches(delta_wing, gamma_wing) via { calls };data_iso: forbid reaches(delta_wing, effects(knowledge(gamma)));quiet_boot: forbid reaches(positions, effects(llm)) during birth;gated: forbid reaches(public_api, ledger) avoiding authorization_gate;logs_only: forbid reaches(program, effects(stdio)) avoiding std::log::*;- Endpoints (
SRC,DST):SRCmust be a group.DSTmay be a group oreffects(<class>)(where<class>is a built-in class, user class, composed class, domain family instantiation,family(*), orclient(Surface)/client(*)). Usingeffects(...)inSRCis an error. If a declaration belongs to bothSRCandDST, it immediately violates the claim as a zero-length path. via { calls, bus }: Selects which edges the reachability walk traverses (omittingviatraverses both):calls: Direct calls,selfmethods, handle calls on typed receivers, interface slot defaults, calls into imported seeds, and calls through Hale-source stdlib bodies.bus: Connects each statically-known publish site in a visited function to every subscribing handler for that topic or matching**wildcard subject (including stdlib loggers likestd::log::LoggerreachingStdoutSink->printlnwhenStdoutSinkis instantiated in the program). Note thatvia { bus }alone only follows publish statements directly insideSRCfunctions; omitvia(or writevia { calls, bus }) to follow calls that lead to a publish.
during <phase>: FiltersSRC’s projection to a specific lifecycle hook (birth,accept,release,run,drain,dissolve), mode (bulk,harmonic,resolution), or method/handler name. Free functions drop out; if the filter leaves zero functions inSRC, compilation fails with an error.avoiding <gate>: Removes all vertices in<gate>(a declared group orstd::<mod>::*such asstd::log::*) from the graph walk. This expresses mandatory interposition: proving thatAcannot reachBwhenGis removed proves that every path fromAtoBpasses throughG.<gate>must be disjoint from bothSRCandDST.
2. Direct Boundary Allowlists (only edges A -> B)
Section titled “2. Direct Boundary Allowlists (only edges A -> B)”grant: only edges gamma_wing -> delta_wing { publish t::ResearchDigest;};Requires every direct edge from group A into group B to match one of the listed topic grants, reporting all un-granted edges if violated:
publish T;andsubscribe T;grant the exact sameA -> Bbus edge on declared topicT.- Direct call edges from
AtoBare never grantable and always violateonly edges A -> B. - Bus edges on raw string literals or wildcard subjects without a declared topic cannot be granted and fail closed if they land in
B. only edges A -> Bchecks only directA -> Bedges; it does not restrictB -> Aedges or multi-hop paths through a third groupC.
3. Capability Budgets (bound C <= N on paths from G)
Section titled “3. Capability Budgets (bound C <= N on paths from G)”one_call: bound llm <= 1 on paths from positions;Bounds the per-invocation call-tree sum of a user-declared effect class C (or secret_use) along any path starting from group G, following both call edges and bus hops:
- If a function in
Gcalls two helpers sequentially and each invokes anllmcarrier once, the path cost is2. - Any carrier reachable inside a loop, recursion, unresolved indirect call, untyped receiver, or computed publish subject has an unbounded cost and violates any finite bound
N. - For built-in dimensions on a single function (
alloc_per_call,stack_bytes,block_points,publish,fanout), use@budget.
4. Wiring, Encapsulation, and Attribution (require)
Section titled “4. Wiring, Encapsulation, and Attribution (require)”wired: require subscribes(some delta_wing, topic t::Tasks);feeds: require publishes(some delta_wing, topic t::Done);confined: require sealed(all vaults);attributed: require attributed(all syscall);require subscribes(some G, topic T)/require publishes(some G, topic T): Verifies that at least one locus inGdeclaressubscribe Torpublish Tin itsbus { }block.require sealed(all G): Verifies that every locus inGcarries@sealed(so itsparamsfields cannot be read or mutated outside its own methods).require attributed(all C): Verifies that every function directly performing built-in effect classCcarries a user-declared effect classification@effects(is: {...}).
5. Execution-Domain Separation (require separate_domains)
Section titled “5. Execution-Domain Separation (require separate_domains)”isolate_io: require separate_domains(all workers, all gateways);Verifies that every arranged locus instance in workers executes on a different runtime domain from every arranged locus instance in gateways:
- Domain identity: Each named cooperative pool is one domain; each
pinnedlocus instance (or replica) is its own domain; an unplaced child locus inherits its parent’s domain (so two unplaced children of the same pinned parent share a domain). - Group selection: Groups select locus declarations directly (not child subtrees; include child declarations in the group if they should be checked). Both groups must select at least one locus with a non-zero instance count (empty groups or zero-instance loci are rejected as
invalid, even withmay_be_empty). - If two instances share a domain, the claim is
violatedand prints both instance paths and their shared domain. If an instance’s placement cannot be statically resolved, the claim reportsuncertified.
6. Lossless Bus Configuration (require no_silent_loss)
Section titled “6. Lossless Bus Configuration (require no_silent_loss)”payments_arrive: require no_silent_loss(topic Payments);money_is_safe: require no_silent_loss(all money);Verifies that no modeled boundary on topic Payments (or on any bus route published or subscribed by a member of money, including routes covered by wildcard patterns like "pay.**") is configured to silently discard messages:
| Route Boundary | Configuration Checked | Violates no_silent_loss When Set To |
Non-Silent Alternatives |
|---|---|---|---|
| Keyed Topic Routing | Topic’s on_unmatched: policy |
swallow (default on keyed topics) |
on_unmatched: fail; or on_unmatched: fallback; (with a where key == _ subscriber) |
| Subscriber Queue | Subscription’s bounded(N, ...) policy |
drop_old or drop_new |
Unbounded subscription queue |
| Publish Call Site | Send statement’s or handler |
or discard |
Plain send (on non-failing topic), or raise, or or wait |
| Transport Binding | External transport loss policy | drop |
listen binding, or connect binding where every send uses or wait |
Example of a route that violates no_silent_loss:
type Payment { account: Int = 0; cents: Int = 0; }
topic Payments { payload: Payment; subject: "payments"; keyed_by account;}
locus Ledger { params { booked: Int = 0; } bus { subscribe Payments as on_payment bounded(64, drop_old) where key == 1; } fn on_payment(p: Payment) { self.booked = self.booked + p.cents; }}
locus Gateway { bus { publish Payments; } fn pay(account: Int, cents: Int) { Payments <- Payment { account: account, cents: cents }; }}
group money = { Ledger, Gateway };
main locus App { params { ledger: Ledger = Ledger { }; gate: Gateway = Gateway { }; } claims { payments_arrive: require no_silent_loss(topic Payments); money_is_safe: require no_silent_loss(all money); } run() { self.gate.pay(1, 500); }}fn main() { App { }; }claim `payments_arrive` violated: route `Payments` is configured to discard silently — topic `Payments` keyed by `account` is `on_unmatched: swallow`; subscriber `Ledger::on_payment` on `Payments` is `bounded(64), on_full: drop_old`To satisfy the claim, configure the topic with on_unmatched: fail;, remove bounded(64, drop_old) from the subscriber, and handle the fallible send with or raise:
topic Payments { payload: Payment; subject: "payments"; keyed_by account; on_unmatched: fail;}// Ledger: subscribe Payments as on_payment where key == 1;// Gateway: Payments <- Payment { account: account, cents: cents } or raise;7. Seed Topic Coverage (cover topic in seed)
Section titled “7. Seed Topic Coverage (cover topic in seed)”no_orphans: cover topic in seed(t): subscribed_by(some positions);Verifies that every topic declared in the seed imported under alias t has at least one subscribing locus in group positions. Reports all uncovered topics on failure.
8. Keyed Routing Partitions (cover keys)
Section titled “8. Keyed Routing Partitions (cover keys)”one_worker: cover keys(topic Orders in 0..=3): delivered_to(exactly_one workers);whole_type: cover keys(topic Orders): delivered_to(exactly_one workers);Verifies that for every permitted key on a keyed_by topic, exactly one arranged subscriber registration in the named group receives that key (neither zero recipients nor overlapping recipients):
type Order { shard: Int = 0; qty: Int = 0; }topic Orders { payload: Order; subject: "orders"; keyed_by shard; }
locus Worker { params { seen: Int = 0; } bus { subscribe Orders as on_order where key == replica; } fn on_order(o: Order) { self.seen = self.seen + o.qty; }}
locus Audit { params { n: Int = 0; } bus { subscribe Orders as on_any; } fn on_any(o: Order) { self.n = self.n + 1; }}
group workers = { Worker };
main locus App { params { w: Worker = Worker { }; audit: Audit = Audit { }; } placement { w: pinned(cores = 0..4, replicas = 4); } bus { publish Orders; } claims { one_worker: cover keys(topic Orders in 0..=5): delivered_to(exactly_one workers); } run() { Orders <- Order { shard: 1, qty: 2 }; }}fn main() { App { }; }Because w has 4 replicas (0..=3) and the claim requires 0..=5, keys 4..=5 have no recipient in workers:
claim `one_worker` violated: not every key of `Orders` reaches exactlyone registration of `workers` — uncovered: no registration of `workers`receives keys 4..=5 (the publish is silently swallowed; its recipientslie outside the group: `Audit::on_any`)Note that Audit (which subscribes to all keys of Orders) does not cause an overlap violation because Audit is outside workers. Changing the interval to in 0..=3 satisfies the claim.
- Key Domain:
in LO..=HIspecifies an inclusive integer range (negative bounds likein -3..=3are valid). Whenin LO..=HIis omitted, the domain is the full key type (Int,Bool, etc.), which requires awhere key == _fallback subscriber on anon_unmatched: fallbacktopic to cover unmatched keys. - Fail-Closed Conditions for
cover keys:
| Condition | Verdict | Reason |
|---|---|---|
Dynamic key filter (where key == self.id) on an instantiated subscriber |
uncertified |
Key filter is not a literal or replica constant. |
Subscribing locus spawned dynamically outside main arrangement |
uncertified |
Instance population is not statically bounded. |
Unmodeled hole hiding subscriptions, counts, or filters on T |
uncertified |
Subscriber set is incomplete. |
No in LO..=HI interval and no known publish site (or an unresolved computed-subject publisher on T’s wire) |
uncertified |
Permitted key domain cannot be established without in LO..=HI. |
Topic T does not declare keyed_by |
invalid |
Topic is unkeyed. |
in LO..=HI used on a non-Int key field (e.g., Bool or String) |
invalid |
Integer interval requires an Int key field. |
| Target group has no locus members | invalid |
Free functions cannot hold subscriptions. |
9. Endpoint Cardinality (count publishers and count subscribers)
Section titled “9. Endpoint Cardinality (count publishers and count subscribers)”single_writer: count publishers(topic t::Tasks) == 1;consumed: count subscribers(topic t::Done) >= 1;capped: count subscribers(topic t::Audit) <= 2;Counts the number of distinct declared loci that publish or subscribe to the topic. To count replicated runtime instances across a multi-binary deployment, use count inside a fleet locus (Multi-Binary Services).
Library-Tier Claims
Section titled “Library-Tier Claims”A library seed (a directory with no main locus) can declare a top-level claims { } block over its own loci, functions, and topics:
type Charge { amount: Int; }topic Charges { payload: Charge; }
locus Wire { params { seen: Int = 0; } bus { subscribe Charges as on_charge; } fn on_charge(c: Charge) { self.seen = self.seen + c.amount; }}
group settlers = { Wire };
claims { single_settle: count subscribers(topic Charges) <= 1; wired: require subscribes(some settlers, topic Charges);}Library-tier claims travel with the import:
- When the library seed is checked standalone (
hale check lib/pay), its claims are evaluated against the library’s own declarations. - When an application imports
lib/pay as pay,pay’s claims are re-evaluated over the merged application graph. If the application adds a second subscriber topay::Charges,hale checkfails the application build citingclaim `pay::single_settle` violatedand pointing to the library’s claim declaration.
Evaluation Model,Verdicts, and Fail-Closed Rules
Section titled “Evaluation Model,Verdicts, and Fail-Closed Rules”Every claim and lowered function certificate (@effects, @budget, @phase_effects) evaluates to one of four verdicts:
| Verdict | Meaning | Required Action |
|---|---|---|
holds |
Proved over all modeled paths. | None (passes build). |
violated |
Disproved; a concrete counterexample path or configuration exists. | Fix the code or configuration identified by the witness. |
uncertified |
Well-formed claim, but the program graph contains an unresolvable edge (indirect call, untyped receiver, computed publish subject). | Refactor the unresolvable call/publish into a statically typed form. |
invalid |
The claim or group declaration itself is malformed (e.g., undeclared group member or empty group without may_be_empty). |
Fix the claim or group syntax. |
Both violated, uncertified, and invalid fail hale check.
How Indirect Calls, Receivers, and Interfaces Are Resolved
Section titled “How Indirect Calls, Receivers, and Interfaces Are Resolved”- Function Pointers & Indirect Calls: A call through a local
let f = target_fn; f()resolves directly totarget_fn. A call through a function-typed parameter or variable fans out conservatively to every function referenced as a value in the program with a matching signature. Only an indirect call with zero matching candidates reportsuncertified. - Method Receivers: The analyzer resolves types for local variables,
selffields, chained struct/locus fields, struct literals, function return values, uniformif/elseexpressions,or-unwrapped expressions, single-slot.get(i)calls, andforloop variables over arrays or capacity slots. Calling a method on an untyped complex receiver reportsuncertifiedwith a hint to bind the receiver to a typed local variable. - Interface Dispatch Fan-Out: Calling a method on an interface type (
handler.handle(ctx)) fans out to all loci in the closed world that structurally satisfy the interface. Reachability walks visit all alternatives; quantitative bounds (bound,@budget) take the maximum across alternatives at a single call site. - Interface Field Narrowing: When an interface call is made through a locus field (
self.h.dep.apply(x)), the compiler narrows the target set to only the concrete loci actually stored into that field across struct literals and default parameters in the closed application (unless the field is assigned dynamically or the seed has nomain locus). - Uninhabited Interfaces: If zero loci in the closed world satisfy an interface, call sites on that interface are dead code, contribute no edges, and are recorded in the topology artifact under
unknowns(uninhabited_interface_call:<iface>.<callee>).
Diagnostics Catalog
Section titled “Diagnostics Catalog”Parse Errors
Section titled “Parse Errors”| Condition | Diagnostic Summary |
|---|---|
claims { } inside a non-main locus |
"only valid inside main locus" |
| Unknown claim verb | Lists the valid claim verbs (forbid, only, bound, require, cover, count, adopt). |
via { } empty or unknown relation |
"must name at least one relation" / "the composable relations are calls and bus" |
Nested glob in group (a::*::b or a::b::*) |
"the glob is trailing-only" |
Empty key interval in cover keys (3..=0) |
"the key interval 3..=0 is empty" |
| Negative bound or count | "expected an integer bound after <=" / "expected an integer after the comparison" |
Empty or duplicate domain |
"has no members" / "declared more than once" |
Effect family over a domain not declared earlier in the file |
"declare domain X = { … }; above the family" |
| Effect-mask overflow (> 54 user atomic classes) | Rejected at the effect declaration (fail-closed). |
Duplicate @budget key or built-in effect class used as user budget key |
"state it once" / points to built-in @budget dimension names. |
Vocabulary & Reference Errors (invalid / uncertified)
Section titled “Vocabulary & Reference Errors (invalid / uncertified)”| Condition | Verdict & Diagnostic Summary |
|---|---|
Unknown group member or undeclared role R |
invalid: "names no declared locus or fn" / "names no declared role" + did-you-mean |
role R not required by any surface row (without may_be_empty) |
invalid: "no surface row requires role R" |
Unresolved alias::Name or alias::* |
invalid: "does not resolve — no imported declaration matches" / "names no import alias" |
| Duplicate group or duplicate claim name | invalid: "declared more than once" |
Empty group without may_be_empty |
invalid: "holds vacuously — say may_be_empty" |
| Projection vacuity (group has no executable methods/hooks/modes) | invalid: "projects to no executable … vertices" |
| Unknown group or undeclared effect class in a claim | invalid: "never declared" + did-you-mean |
effects(...) used in SRC position of forbid reaches |
invalid: "sources must be declared groups" |
Built-in effect class used in bound (other than secret_use) |
invalid: points to @budget dimension spellings |
Unknown topic in grant, require, or count |
invalid: did-you-mean over declared topics |
cover topic in seed(alias) where alias declares no topics |
invalid: "the coverage domain would be empty" |
no_silent_loss(all G) where G neither publishes nor subscribes |
invalid: "names no route" |
separate_domains over a group with no loci or zero-population locus |
invalid: cannot certify separation without arranged instances |
during <phase> matching zero functions in SRC |
invalid: "a claim over an empty phase holds vacuously" |
avoiding <gate> overlapping SRC or DST |
invalid: "masking an endpoint makes the claim weaker than it reads" |
| Unresolvable call, receiver, filter, or dynamic population | uncertified: "cannot be certified: …" + remediation hint |
Claim Violations (violated)
Section titled “Claim Violations (violated)”| Claim Verb | Witness Reported in Diagnostic |
|---|---|
forbid reaches |
Minimal counterexample path (-> for calls, -(publishes "subject")-> for bus hops) with source spans. |
only edges |
Every un-granted A -> B edge alongside the declared grant list. |
bound |
Measured call-tree count + heaviest chain, or the specific unbounded condition (loop, recursion, etc.). |
require subscribes / publishes / sealed / attributed |
The group or class and the missing declaration or annotation. |
require separate_domains |
Both colliding instance paths, their shared execution domain, and source locations. |
require no_silent_loss |
Every silent boundary setting (topic on_unmatched, subscriber queue policy, send or discard, binding drop). |
cover topic in seed |
Every uncovered topic in the imported seed. |
cover keys |
Every uncovered key/interval, or the overlapping subscriber registrations sharing a key. |
count |
Actual count of distinct declared loci and their names. |
One law, many entrypoints
Section titled “One law, many entrypoints”In a multi-binary system, copying a claims { } block across entrypoints risks drift. A constitution (Constitutions) declares a named set of claims once at the top level so multiple main locus entrypoints can adopt it:
type Settlement { n: Int; }topic Settled { payload: Settlement; }
locus Billing { bus { publish Settled; } fn settle(n: Int) { Settled <- Settlement { n: n }; }}
locus Ledger { params { total: Int = 0; } bus { subscribe Settled as on_settled; } fn on_settled(s: Settlement) { self.total = self.total + s.n; }}
locus Research { params { notes: Int = 0; } fn note() { self.notes = self.notes + 1; }}
group billing = { Billing };group research = { Research };
constitution Core { tenant_iso: forbid reaches(billing, research); one_writer: count publishers(topic Settled) == 1;}
main locus App { params { b: Billing = Billing { }; l: Ledger = Ledger { }; r: Research = Research { }; } claims { adopt Core; local_rule: require publishes(some billing, topic Settled); }}
fn main() { App { }; }Extending Constitutions (extends)
Section titled “Extending Constitutions (extends)”A constitution can extend one or more base constitutions using extends:
constitution Dev extends Core { no_real_payments: forbid reaches(app, payment_provider);}Composition is strictly additive (union-only): a derived constitution can add new named claims, but cannot override or weaken an inherited claim name:
constitution B extends A { rule: count publishers(topic T) == 9; }// ^ compile error if `A` already declares `rule`Diamond inheritance (D extends B, C where both B and C extend A) deduplicates A’s clauses by declaration origin automatically.
Binding a constitution to a deployment target
Section titled “Binding a constitution to a deployment target”To apply environment-specific constitutions (such as Dev vs. Prod) without modifying source files, configure [claims] and [environments.<name>] in hale.toml:
[claims]base = "Core" # Enforced in EVERY environment (or set `no_base = true`)
[environments.dev]constitution = "Dev" # Merged with `base` for dev checksentrypoints = ["apps/prober", "apps/dashboard"]
[environments.prod]constitution = "Prod" # Use `source_only = true` if prod adds no extra constitutionentrypoints = ["apps/prober"]Check a single environment, run the full (entrypoint, environment) matrix, or inject a constitution ad-hoc from the CLI:
hale check apps/prober --env prod # Checks apps/prober with Core + Prodhale check --matrix # Checks all (entrypoint, environment) pairs in hale.tomlhale check apps/prober --adopt std::law::Standard # Injects a constitution for one runstd::law::Standard: Built-in standard library constitution that enforces structured logging (logs_structured: forbid reaches(program, effects(stdio)) avoiding std::log::*;), rejecting rawprintlncalls in application code.- Required Group Vocabulary in Adopting Entrypoints: Every group referenced by an adopted constitution (other than the built-in
programgroup) must be declared in the adopting entrypoint (or imported from a shared policy seed). If an entrypoint intentionally omits a component, declare an explicit empty group:
group payment_provider = { } may_be_empty;Checking Multi-Seed Workspaces (--workspace)
Section titled “Checking Multi-Seed Workspaces (--workspace)”Because hale check <dir> checks a single seed (compilation unit), use --workspace to check every seed and *_test.hl file in a repository:
hale check --workspace .hale verify --workspace .--workspace evaluates every seed and test file even if an earlier one fails, skipping vendor/, target/, and dot-directories. Because each seed has its own model, single-seed artifact flags (--dump-topology, --check-topology*) cannot be combined with --workspace.
The Topology Artifact (.hale.topology)
Section titled “The Topology Artifact (.hale.topology)”hale check can export the verified program model, inferred effects, and claim verdicts as a deterministic, cryptographically digested JSON artifact (.hale.topology):
# Export topology to stdout or a file (note: file path requires `=`)hale check app --dump-topology > .hale.topologyhale check app --dump-topology=.hale.topology
# CI Gates:hale check app --check-topology .hale.topology # Exact snapshot gate (model + law + spans)hale check app --check-topology-shape .hale.topology # Model identity gate (`shape_hash` only)| Gate Flag | What It Compares | When to Use |
|---|---|---|
--check-topology <file> |
Whole-file artifact_digest (model, claims, topics, and byte-offset provenance spans). |
Strict review gate where any source motion, claim rename, or model change requires updating the baseline. |
--check-topology-shape <file> |
Model shape_hash (loci, functions, call/bus relations, groups, phases, derived effects, unknowns) plus artifact_digest integrity verification. |
CI gate that ignores line-number shifts, comment edits, and claim renames while catching any structural or effect change. |
Visualizing Diagrams (hale topology graph)
Section titled “Visualizing Diagrams (hale topology graph)”Render deterministic SVG or Mermaid architecture diagrams directly from a committed .hale.topology file without touching source files:
hale topology graph .hale.topology # SVG to stdout (default `system` view)hale topology graph .hale.topology --format mermaidhale topology graph .hale.topology --view claim --claim apart -o claim.svgAvailable --view modes:
system: Loci, functions (with phase and effect badges), topics, and call/publish/subscribe edges.code: Functions and call-graph edges only.bus: Bus topics, publishers, and subscribers only.claim: Highlights the groups and verdict of the claim specified by--claim <name>.residue: Highlights unresolved analysis holes as first-class nodes.
Semantic Model Diffing (hale model diff)
Section titled “Semantic Model Diffing (hale model diff)”Compare two topology artifacts semantically to review structural, contract, effect, and claim changes between revisions:
hale model diff before.topology after.topology --texthale model diff before.topology after.topology # Outputs versioned JSON diffhale model diff before.topology -> after.topologyclassification: model-shape (shape_hash 560bb0… -> 578951…)legend: + added - removed ~ renamed > moved * split/joined ? ambiguous ! changed in placeclasses: identical · source-only (no model or contract change) · contract (a contract or effect moved, shape_hash unmoved) · model-shapedeclarations: ~ locus Worker -> Crew (renamed; shape unchanged) + locus Pager (main.hl bytes 248..253) + topic Alerts (main.hl bytes 71..77)contracts: ! locus App params: +limit: Int ! locus App publishes: +Alertseffects: ! fn Crew::on_e gains publishlaw: + claim quiet: count publishers(topic Out) == 0 [violated]Topology Artifact Schema (1.21)
Section titled “Topology Artifact Schema (1.21)”{ "schema": "1.21", "semantics": 1, "shape_hash": "<fnv1a-64 over the model half>", "sorts": { "loci": ["..."], "fns": ["..."], "topics": ["..."] }, "sealed": [ "<@sealed loci>" ], "relations": { "calls": [ {"from": "...", "to": "...", "loop": true, "unbounded": true, "via_interface": "<iface>"} ], "calls_via_stdlib": [ {"from": "...", "to": "...", "loop": true} ], "publishes": ["..."], "subscribes": ["..."] }, "groups": { "<name>": ["<members as declared>"] }, "labels": { "<fn>": ["<declared effect classes>"] }, "phases": { "<fn>": {"phase": "...", "kind": "hook|method"} }, "seeds": { "<alias>": ["<member decls>"] }, "effects": { "<fn>": ["<derived effect classes>"] }, "supervision": [ {"locus": "...", "child": "...", "err": "...", "ops": ["..."], "retry_bound": 3} ], "unknowns": [ {"fn": "...", "reasons": ["indirect_call | untyped_receiver_call:<callee> | uninhabited_interface_call:<iface>.<callee> | computed_publish"]} ], "endpoint_identity": [ {"verb": "...", "fn": "...", "wire": "...", "topic": "..."} ], "sources": [ {"id": 0, "path": "...", "digest": "..."} ], "provenance": { "calls": ["..."], "publishes": ["..."], "subscribes": ["..."], "decls": {}, "supervision": ["..."] }, "topics": [ {"name": "...", "subject": "...", "shape": "...", "payload_hash": "..."} ], "endpoints": [ {"verb": "publish|subscribe", "subject": "...", "via": "site|declaration"} ], "declares_publish": [ {"locus": "...", "subject": "..."} ], "contracts": [ {"locus": "...", "sealed": false, "params": [], "methods": [], "publishes": [], "subscribes": [], "supervises": [], "instances": []} ], "bindings": [ "<declared bindings: topic, transport, role>" ], "observe": [ {"param": "...", "kind": "...", "backend": "...", "host": "...", "port": 0, "path": "..."} ], "claims": [ {"name": "...", "form": "...", "result": "holds|violated|uncertified|invalid", "ordinal": 0, "source": "Core"} ], "lowered": [ {"subject": "...", "form": "...", "result": "holds", "ordinal": 0} ], "law": { "law_digest": "...", "inputs_digest": "...", "fn_universe": [], "loci": [], "groups": [], "topics": [], "subjects": [], "effect_classes": [], "legacy": [], "issues": [], "rows": [] }, "capabilities": { "exact_calls": true }, "adequacy": { "reachability": "exact", "boundary": "exact", "endpoint": "exact", "bound": "exact", "certificate": "exact", "causes": "exact", "depends": "exact", "budget": "exact", "placement": "exact" }, "evaluation": { "environment": "prod", "roots": [], "closure": [] }, "verdict": "clean | law_failed", "artifact_digest": "<digest over entire document>"}Key artifact sections:
shape_hashvs.artifact_digest:shape_hashhashes only the structural model half (sorts,relations,groups,labels,phases,seeds,effects,supervision,unknowns,endpoint_identity).artifact_digesthashes the entire JSON document to prevent tampering. Bothhale topology graphandhale fleet checkverifyartifact_digest,shape_hash,law_digest, andinputs_digestbefore admitting an artifact.calls_via_stdlib: Collapses any user-to-user call path traversing standard library internals into a single contracted edge so external artifact verifiers can replay reachability without stdlib source.lowered: Records every function-level@effects,@budget, and@phase_effectscontract in its equivalent relational claim form alongside top-levelclaims.
Related Reference
Section titled “Related Reference”- Specification:
spec/verification.md § Claims. - Reusable Claimsets: Constitutions.
- Program Model: The Model.
- Multi-Binary Fleet Claims: Multi-Binary Services.