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).