Skip to content

Flow Contribution Bundle — `Rigor::FlowContribution`

Status: Public read shape (v0.0.9 group B). This document fixes the surface every flow-contribution producer (built-in narrowing rules today, RBS::Extended annotations and plugin authors after v0.1.0) hands the analyzer at a single call edge. The merge policy that consumes these bundles is owned by ADR-2 § “Plugin Contribution Merging”; v0.0.9 ships only the bundle struct itself — the merger lands alongside the plugin API in v0.1.0.

contribution = Rigor::FlowContribution.new(
return_type: Rigor::Type::Combinator.constant_of(42),
truthy_facts: [...],
falsey_facts: [...],
post_return_facts: [...],
mutations: [...],
invalidations: [...],
exceptional: nil,
role_conformance: [...],
provenance: Rigor::FlowContribution::Provenance.new(
source_family: "plugin.my-gem",
plugin_id: "my-gem",
node: ast_node,
descriptor: cache_descriptor
)
)

Every keyword argument is optional. A slot left unset means “this contribution does not assert anything in that dimension” and the merge policy treats it as absent. Bundles are frozen on construction; collection slots are duped and frozen so callers cannot mutate them after the fact.

The eight content slots match ADR-2 § “Flow Contribution Bundle”:

SlotTypeMeaning
return_typetype carrier or nilNormal-edge return type. Plugins MAY narrow within the selected RBS contract; an incompatible return becomes a conflict diagnostic per the merge policy.
truthy_factsArray or nilFacts that hold only on the truthy control-flow edge. Edge-local: a truthy-edge fact does NOT imply its falsey-edge complement unless the contribution explicitly supplies it.
falsey_factsArray or nilDual of truthy_facts.
post_return_factsArray or nilFacts that hold after the call returns normally on every edge. The carrier for assertion-style contributions (%a{rigor:v1:assert ...}).
mutationsArray or nilReceiver and argument mutation effects. Conflicts with pure-style declarations are diagnostics.
invalidationsArray or nilTargeted fact invalidations beyond what mutations already implies.
exceptionaleffect tag or nilNon-returning, raising, or unreachable effect.
role_conformanceArray or nilCapability-role conformance facts the contribution provides.

The shape of the values inside the collection slots is intentionally not pinned in v0.0.9. The merger that lands in v0.1.0 will define a tagged element form; until then contributions are free to use the analyzer-internal narrowing representation that already drives built-in rules.

Rigor::FlowContribution::Provenance = Data.define(
:source_family, # Symbol or String — :builtin / :rbs_extended / "plugin.<id>" / ...
:plugin_id, # String or nil
:node, # AST node or nil — the Prism node carrying the annotation
:descriptor # Rigor::Cache::Descriptor or nil — cache slice this contribution attaches to
)
Rigor::FlowContribution::Provenance.builtin
# => #<data Provenance source_family=:builtin, plugin_id=nil, node=nil, descriptor=nil>

source_family mirrors Rigor::Analysis::Diagnostic#source_family so attribution composes cleanly: a diagnostic produced by a plugin contribution carries the same source-family string the contribution declared. Cache invalidation runs through descriptor per ADR-2 § “Registration, Configuration, and Caching” and the Rigor::Cache::Descriptor schema.

  • == compares the bundle structurally (every content slot plus provenance). hash is consistent with ==.
  • to_h returns a Hash keyed by every slot name plus :provenance (whose value is the Provenance Data’s to_h).
  • empty? is true when every content slot is nil or an empty collection. Provenance does NOT count toward emptiness — an empty bundle still carries source attribution.

Internal producers may surface their contributions as FlowContributions alongside their typed-Data carriers. The typed carriers (PredicateEffect, AssertEffect, …) keep serving the analyzer-internal narrowing / dispatch machinery; the bundle is the public packaging for the v0.1.0 contribution merger and for diagnostic / documentation surfaces that want a single shape across producers.

Rigor::RbsExtended.read_flow_contribution(method_def) -> FlowContribution | nil

Section titled “Rigor::RbsExtended.read_flow_contribution(method_def) -> FlowContribution | nil”

Rolls up every recognised RBS::Extended directive on a single RBS method definition into one bundle:

DirectiveBundle slot
rigor:v1:predicate-if-true ...truthy_facts (each entry an RbsExtended::PredicateEffect)
rigor:v1:predicate-if-false ...falsey_facts (PredicateEffects)
rigor:v1:assert ...post_return_facts (each entry an AssertEffect)
rigor:v1:assert-if-true ...post_return_facts (AssertEffect with condition: :if_truthy_return)
rigor:v1:assert-if-false ...post_return_facts (AssertEffect with condition: :if_falsey_return)
rigor:v1:return: ...return_type (a Rigor::Type)

Slots that have no contributions stay nil (rather than empty collections) so the bundle’s #empty? rule applies cleanly.

provenance is the shared Rigor::RbsExtended::RBS_EXTENDED_PROVENANCE:

Rigor::FlowContribution::Provenance.new(
source_family: :rbs_extended,
plugin_id: nil,
node: nil,
descriptor: nil
)

source_family: :rbs_extended lines up with the diagnostic- provenance prefix introduced in v0.0.8 slice 5, so a diagnostic sourced from an RBS::Extended directive can carry the same attribution string.

param: <name> directives are intentionally NOT bundled — they refine the call’s signature contract rather than its flow facts and do not fit ADR-2 § “Flow Contribution Bundle” slot semantics. Callers that care about parameter contracts keep using RbsExtended.read_param_type_overrides / RbsExtended.param_type_override_map.

ADR-2 mentions an analyzer-internal flattening of each bundle into a tagged element list keyed by (target, flow edge, effect kind). That representation is the implementation surface the merge policy consumes; it shipped in v0.1.0 as FlowContribution#to_element_list, alongside the merger (see flow-contribution-merger.md). The element-list form is an analyzer-internal surface — plugin authors should still construct FlowContribution bundles and not rely on the element-list shape directly.

The constructor surface and slot names are stable as a v0.0.x public read shape. Adding a new slot is a public-API expansion that should be accompanied by an ADR-2 amendment plus a schema-version note in this document. Renaming or removing a slot is a breaking change that requires a major-version bump.

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.