Expand description
P8.1 (#1512): UnitId and UnitSignature — the load-bearing types this
whole phase’s firewall (R3.14) is stated in terms of. UnitSignature
projects a UnitTable into design notes §15’s four required-annotation
categories — cross-context types, fn signatures (free functions and
methods), handler signatures plus storage, capability sets — with every
body/body-adjacent field excluded ([DECISION B],
design/tracks/incrementality.md §3.4/Q4) rather than merely ignored by
the comparison: body/requires/ensures never reach FnSignature,
body never reaches HandlerSignature, init/annotations never
reach StoreFieldSignature — the type system, not a comparison
function’s own discipline, makes “no body reachable from UnitSignature”
a fact.
Stability is proved by comparing UnitSignature::canonical, not the raw
struct: every included fragment still carries its own Span (Ident,
Param, every TypeRef variant, TypeDecl’s own trivia/
documentation), and editing a body shifts every later declaration’s own
spans in the same file (the byte-offset cascade PR #1509’s bot review
caught one level up, in UnitSignature’s own design). [DECISION C]
extends contract.rs’s canon_type/service_normal_form (ADR 0200) —
already a span-free canonical rendering, proven correct by
contract_hash.rs’s own no-false-positive fixture — to reach the new
signature shapes here, rather than inventing a second erasure scheme.
The R3.14 firewall has two directions, and this module is responsible
for both: a body edit must not move UnitSignature::canonical (the
exclusions above), and a genuine signature edit MUST move it — a false
“unchanged” verdict silently skips recomputing every downstream
consumer, the more expensive failure mode for an incrementality firewall.
PR #1517’s own bot review caught three real gaps on the second direction
before this module first merged: HandlerKind was dropped entirely
(so renaming an HTTP route or changing on GET to on POST didn’t move
the form), methods never reached UnitSignature at all (UnitTable.fns
only holds free functions — FnName::Method is filed under
UnitTable.methods instead, symbols.rs:596-619), and a capability
declaration’s own operation signatures (CapabilityDecl.ops, itself
already body-free — “signature only; no body”, ast.rs:564) were never
projected, so retyping a capability op left every consumer’s own
UnitSignature unchanged even though its compiled contract with that
capability’s provider did change. All three are fixed here; see each
type’s own doc comment for what closed the gap.
Deliberately still excluded, named explicitly so a future reviewer
doesn’t have to re-derive it: ProviderDecl.provider_name (an internal
selector used only in tests/config to pick an implementation — never part
of a Cap.op(...) call site, so it carries no information a consumer’s
own compile depends on) and handler-position annotations (@cache, …,
Handler.annotations: Vec<Annotation>) — an AnnotationArg.value is an
arbitrary Expr, and canonicalising Expr the way canon_type already
does for TypeRef is real, unscoped work this slice does not take on;
flagged for whichever future slice needs it, the same “flag for the slice
that will actually pin it” discipline this phase has applied throughout.
ServiceProtocol::Events’s own pattern/schema_dispatch fields (the
structural payload filter and via schema(N) clause) are captured only
as “present or absent”, not rendered field-by-field, for the same
Expr/pattern-canonicalisation reason — narrower than ideal, but strictly
better than the pre-fix state where the whole protocol was invisible.
unit_signature_for’s only caller is tests/unit_signature_stability.rs
(P8.2), and that is deliberate: this module is R3.14’s own proof — the
firewall stated as a type and checked by a test — not a production path.
Phase 8’s two structural consumers-to-be (ProjectGraph, P8.3, and the
DefId-keyed Body/TypeOf queries, P8.5) were built beside it and
deleted on 2 September 2026 (#1537, ADR in that PR’s pending file): nothing
called them and no scheduler existed to. This module stays because
R3.15’s trigger (#1523 — keystroke-to-diagnostic latency attributed by
level) presupposes a unit level to attribute to; the gated
incremental_query_types probe certifies it is present and its
stability test exists.
Structs§
- Capability
OpSignature - A body-free mirror of
bynk_syntax::ast::CapabilityOp— already “signature only; no body” in its own doc comment (ast.rs:564), so nothing here needs excluding beyonddocumentation/span/trivia. Added by PR #1517’s own bot review (finding #3): acapabilitydeclaration’s own ops are the abstract signature every consumer compiles itsCap.op(...)call sites against, notProviderDecl.ops(which carry a realbody: Blockeach — a provider’s own implementation, correctly excluded, same as any other body). - Capability
Signature - The capability-set category (design notes §15’s fourth): what a unit
exports (
table.exported_capabilities, already plain strings), what a unit itself declares (table.capabilities’s own op signatures — added by PR #1517’s own bot review), and what each provider/service declares it needs (given/default_given). EveryCapRefcollapses to its rendered name (context.capability, or barecapabilitywhen local) — its ownSpanis dropped, matching every other category’s span-erasure discipline. - FnSignature
- [DECISION B]:
bynk_syntax::ast::FnDecl, body-free. Excludesbody,requires,ensures(all body-adjacent —requires/ensuresscope over parameters and the result but are not part of design notes §15’s own required-annotation list, per Q4) anddocumentation. Used for both free functions (UnitSignature::fns) and methods (UnitSignature::methods). - Handler
Signature - [DECISION B]:
Handler, body-free. Excludesbody,by_clause(an actor binder, resolved at the call boundary rather than part of the wire-visible shape),annotations(see this module’s own doc comment — deferred, needsExprcanonicalisation) anddocumentation.givenis kept — it’s this handler’s own slice of the capability-set category.kindis kept (seeHandlerKindSignature) — dropping it was PR #1517’s own bot review finding #1. - Method
Table Signature - The
instance/staticssplit PR #1517’s bot review found missing entirely:UnitTable.fns(whatUnitSignature::fnsprojects) only ever holdsFnName::Freeentries —FnName::Methodis filed underUnitTable.methods: HashMap<String, MethodTable>instead (symbols.rs:596-619), keyed by the attached type’s name, with instance and static methods in their own sub-maps (mirroringcrate::resolver::MethodTable’s own shape exactly, rather than flattening into one map keyed by method name alone — an instance and a static method can share a name for the same type without colliding inMethodTableitself, so flattening here would silently drop one). - Param
Signature - [DECISION B]:
Param, body-free (it already is — kept as its own type only soFnSignature/HandlerSignaturedon’t reach back intobynk-syntax::ast::Paramand risk a future field being added there and silently flowing through unexamined). - Store
Field Signature - [DECISION B]:
bynk_syntax::ast::StoreField, body-free. Excludesinitandannotations(both body-adjacent per Q4 — annotations govern the field’s own storage behaviour, not its externally-relevant shape) anddocumentation. - UnitId
- [DECISION A]: a stable, hashable identity for a unit, reusing the
unit-name
StringeveryUnitTable/combined_types_forcaller already keys on today, rather than introducing a fresh interned-integer scheme — noindex_veccrate (or hand-rolled equivalent) exists anywhere in this codebase, confirmed by grep.UnitSignatureonly needsUnitIdto be a stable, hashable, comparable identity for R3.14’s own proof; it does not need to be dense orIndexVec-compatible. P8.3 resolved the fork this left open by adaptingProjectGraphto the string-keyedUnitId(ADR 0415); that graph was deleted by #1537, so if R3.15’s trigger ever fires, ADR 0415’s recorded shape is what a rebuild adapts to — this type stays a string newtype either way. - Unit
Signature - P8.1 (#1512): a unit’s stable signature — everything about it that must survive an edit inside a function/handler body untouched (R3.14, the phase’s firewall).
Enums§
- Handler
Kind Signature - A closed, body-free mirror of
HandlerKind— every fieldHandlerKinditself carries (HttpMethod, a route/cron-expressionString) is already a plain value with nothing body-adjacent to exclude. PR #1517’s own bot review: dropping this entirely let two handlers with the same params and return type but different HTTP methods or routes canonicalise identically — renaming a route is not a body edit, and R3.14’s own firewall must treat it as a real signature change. - Protocol
Signature - A body-free mirror of
ServiceProtocol— added by PR #1517’s own bot review (“worth a look”):from httpvs.from queue(...)vs.from websocket(...)changes a service’s entire external surface, and nothing about that fact is a body or body-adjacent to a handler.Events’ ownpattern/schema_dispatchare collapsed toboolpresence (see this module’s own doc comment for why — deferredExpr/pattern canonicalisation) rather than dropped outright.
Functions§
- unit_
signature_ for - Builds a
UnitSignaturefornamefromtable, reusingcombined_types(typically a freshcombined_types_for(name, ..)call, mirrored here as a parameter rather than recomputed so a caller building both alongside each other pays for one traversal, not two).