Preference-Aware Notification Fanout

Table of contents
  1. Preference-Aware Notification Fanout
    1. Intent
    2. Summary
    3. Composes
    4. Composition logic
      1. Composition state
      2. Configuration
      3. Primitive policies
      4. Action wiring
      5. Replay semantics
      6. The load-bearing wiring decision
    5. Composition-level invariants
    6. Examples
      1. Walkthrough — one invocation, all three dispositions
      2. Frequency cap firing
      3. Best-effort cap overshoot and reconciliation
      4. Statutory window firing — no stored quiet hours
      5. Marketing newsletter — channel opt-out under CAN-SPAM
      6. Rejection paths
      7. Fail-closed gate — preference store outage
      8. Partial failure
      9. Regulated adversarial scenarios
    7. Generation acceptance
      1. Record-clearable checks
      2. Externally-clearable checks
    8. Edge cases and explicit non-goals
    9. Standards references
    10. Status
    11. Lineage notes

A composition: when an event fires against a named scope, every currently-Active subscriber for that scope receives exactly one recorded disposition — a Notification record shaped to their stated delivery preferences, a named delivery failure, or a classified suppression. Composes Subscription, Notification, Preference, and Event Log directly. The preference-shaped superset of Notification Fanout: the same query-then-create loop, with a shaping gate between the subscriber query and each create that consults the subscriber’s in-effect Preference record — suspensions suppress, quiet hours suppress, frequency caps suppress, channel choices route — and every suppression lands in the Event Log with its reason and the preference record that produced it.


Intent

Notification Fanout answers who should receive this? and produces one delivery record per Active subscriber. What it cannot answer is how does this subscriber want delivery shaped — and is right now even an acceptable moment? The Preference atom records the answers — channels, frequency limits, quiet hours, format — but by its own declaration it does not deliver, does not consult subscriptions, and does not interpret its own opaque values: “the composing fanout pattern” is named in Preference’s text as the interpreter. The interpretation point — the moment a fanout invocation reads a subscriber’s in-effect preference record and renders a delivery-or-suppress verdict — belongs to no atom. It is this composition’s reason to exist.

The stakes of that interpretation point are regulatory, not cosmetic. The TCPA (Telephone Consumer Protection Act, codified at section 227 of title 47 of the United States Code (U.S.C.) — the US federal law restricting unsolicited calls and text messages) attaches per-message statutory liability to a text sent inside a recipient’s quiet window; CAN-SPAM (the US Controlling the Assault of Non-Solicited Pornography And Marketing Act, 15 U.S.C. §7701 et seq.) requires honored opt-outs in commercial email. A fanout loop that consults preferences “usually” is a fanout loop that delivers inside quiet windows whenever a code path forgets to check. This composition makes the check structural: the shaping gate sits between Subscription.subscribers_for and each Notification.create, every subscriber passes through it on every disposition — the original invocation’s pass and any later journaled retry — and the gate’s verdict is recorded whether it delivers or suppresses. A suppressed subscriber is not a skipped subscriber — the suppression is a first-class disposition, classified by reason, written to the Event Log, and queryable later by the regulator, the disputing recipient, or the breach investigator.

This composition does not compose Notification Fanout. Notification Fanout’s fanout action exposes no per-subscriber hook; wrapping it cannot insert the shaping gate between the query and the creates without a breaking change to a grounded composition. The precedent is Reservation Lifecycle (C9), the pool-arithmetic superset of Idempotent Reservation: re-wire the same atoms plus more, rather than wrap the smaller composition. Notification Fanout remains the unshaped sibling — deployments that need fanout without preference shaping use it directly — and this composition mirrors its loop, its failure isolation, and its fanout_id correlation discipline, widening the bipartition {created, failed} to the trichotomy {created, failed, suppressed}.

Two boundary declarations the constituents themselves draw, restated here because the composing layer must honor the sequencing. First, legal permission is not evaluated here: whether the system may communicate with the principal at all is the Consent pattern’s question, and Preference’s own blockquote draws that line. A deployment whose deliveries require lawful basis sequences the Consent gate before invoking this composition; Consent and Consent & Preference Management (C2) are named peers, not constituents. Second, attribution is not provided here: a deployment that must answer who triggered this fanout under credential composes Audit Trail or Actor Identity alongside; the bare composition records dispositions, not initiators.


Summary

Preference-Aware Notification Fanout connects an event to everyone who wants to hear about it — delivered the way each of them asked. It combines four simpler patterns: one that records who is interested in which kind of event (Subscription), one that records each person’s delivery wishes — preferred channels, “no more than N a day”, “nothing between 10pm and 7am”, plain text versus rich format (Preference), one that creates a per-recipient delivery record and tracks its outcome (Notification), and one that keeps a permanent ordered journal of everything that happened (Event Log). When an event fires, the composition asks who is subscribed, then — for each subscriber — reads their current preferences and decides: deliver on these channels in this format, or do not deliver, for a named reason (their preferences are paused, it is inside their quiet hours, they have hit their frequency limit, or they opted out of every channel). Every subscriber gets exactly one of three recorded outcomes — delivered, failed, or suppressed-with-reason — and nothing is silently dropped: each suppression is written to the journal with the reason and the preference record that caused it, and even a crash that interrupts a fanout mid-way leaves a journal trail from which the gap is detected and repaired. That journal entry is what lets the deployment prove, from records alone, that it honored quiet hours and frequency limits — the proof US telemarketing law (TCPA) and commercial-email law (CAN-SPAM) effectively demand. The composition does not decide whether contacting the person is legal at all (a separate consent pattern owns that, and runs first), and it does not transport anything — it produces the shaped delivery records a transport layer consumes.


Composes

  • Subscription — provides the Active subscriber set via the subscribers_for(event_scope) query, and the point query subscribed(subscriber_ref, event_scope) that redispose uses to re-verify the audience across its unbounded retry horizon. The composition reads but never writes the subscription store; those two declared queries are the only Subscription surfaces invoked.
  • Notification — provides the per-recipient delivery record via create(recipient_ref, payload), and the status_of(notification_id) query that redispose reads to adjudicate transport-failure retries. The composition creates at most one live Notification record per subscriber per invocation, with the shaped envelope carried in the payload. Delivery outcome tracking (deliver / fail / expire) belongs to the deployment’s transport layer, not to this composition — the composition only reads those outcomes at the retry boundary.
  • Preference — provides each principal’s in-effect delivery-shaping record via current_for(principal_ref) (and read(preference_id) for audit reconstruction). The composition reads but never writes the preference store. The composition is the interpreter Preference’s own spec names: it gives the opaque channel_preferences, frequency_limit, quiet_hours, and format values their operational meaning at disposition time, under deployment-declared interpretation rules (see Configuration).
  • Event Log — provides the durable disposition journal via append(data) and the replay surface via read(query). Every fanout invocation and every per-subscriber disposition is appended; the frequency-cap accounting is derived by reading delivery events back (see Composition state). Where Notification Fanout treats Event Log as an optional caller-composed enrichment, this composition makes it a constituent: the no-silent-disposition guarantee (Invariant 5) is unachievable without it.

Adjacent patterns, not constituents: Notification Fanout (the unshaped sibling — same loop without the gate); Consent / Consent & Preference Management (legal permission, sequenced before this composition by the deployment); Audit Trail (attribution and tamper-evidence for deployments whose dispositions need sealing); Duplicate Prevention (at-most-once fanout semantics under retry — see Edge cases).


Composition logic

Composition state

The composition carries two state elements. Both are derived indexes per execution-contract.md §Composition state — read-path acceleration over constituent truth, rebuildable at any time from a constituent’s declared read surface, excluded from every action’s atomicity surface, and carrying no consistency claim of their own. Nothing here is extraction-pending: every fact in both elements is reconstructible through the named rebuild procedures below.

Both rebuild procedures run through Event Log’s declared read(query) surface, whose exact query shape Event Log’s own spec leaves to implementation policy. The predicates the rebuilds need — by event type, by fanout_id, by principal_ref, by decided_at range — are therefore not guaranteed by the atom; they are supplied by the deployment-declared journal_query_capability (see Configuration), the named source every derivation below rests on.

  • delivery_count_index — map from principal_ref to the (decided_at, channels) of that principal’s recent delivery dispositions, consulted by the frequency-cap evaluation (the channels ride along so a deployment whose frequency_limit_interpretation declares channel-scoped caps can partition the same entry by channel — see Edge cases, Per-channel frequency caps). Classification: derived index. Derivation: the Event Log store, through its declared read(query) surface under the declared journal_query_capability. Rebuild procedure: EventLog.read(query: fanout.created events where principal_ref = P and decided_at within the deployment-interpreted frequency window) — every fact in the index is a fanout.created event’s (principal_ref, decided_at, channels) triple, so the index regenerates from empty by one query per principal (or one time-ranged query for all principals). Populated by: each committed delivery disposition (action wiring step 6). Read by: the shaping_disposition evaluation (step 5). Removed by: timestamps aging out of the interpreted window. A missing or lost entry is a rebuild trigger, not data loss. Deliberately outside the atomicity surface: the index’s population is evidence the truth-bearing writes committed, never a peer write — which is exactly why frequency-cap safety (Invariant 4) is stated in conditional form rather than as an unconditional bound; the gap between reading the count and committing the delivery is a time-of-check-to-time-of-use race, the load-bearing hazard the formal layer verifies (see Lineage notes §Formal model).
  • fanout_dispositions — map from fanout_id to the invocation’s disposition lists, consulted by audit queries. Classification: derived index. Derivation: the Event Log store, through read(query) under the declared journal_query_capability. Rebuild procedure: EventLog.read(query: events where fanout_id = F) — the fanout.initiated event carries the invocation’s scope, payload digest, and fired_at; the per-subscriber fanout.created / fanout.suppressed / fanout.create-failed events carry the dispositions; the map is their grouping by fanout_id. Populated by: the same appends. Read by: Generation acceptance traversals and operator queries. Removed by: cache eviction at the deployment’s discretion (any horizon, any policy) — every evicted fact regenerates from the journal, so the index is bounded by operational choice, not by the spec.

The Subscription store (who is subscribed), the Notification store (delivery records and outcomes), the Preference store (preference records and their lifecycle), and the Event Log store (the disposition journal) are owned by their constituent instances. The composition duplicates none of them.

Configuration

Deployment-settable knobs. The first three are interpretation rules — Preference stores its shaping values opaque and names the composing fanout pattern as interpreter, so the deployment must declare, per instance, how each opaque value is read. Each interpretation rule must be a pure, deterministic, total function of the stored value (plus the injected now where time-dependent): the same stored value and the same inputs always yield the same interpretation, or disposition replayability (Invariant 7) fails. The interpretation configuration is versioned, never mutated: any change to an interpretation rule, the default shape, the statutory window, or a policy knob produces a new config_version; each invocation journals the version in force in its fanout.initiated entry, and replay evaluates under the journaled version, so a legitimate configuration change between disposition and replay can never masquerade as a gate defect. The deployment retains every version’s rules for its audit horizon — the same deployment-owned, append-only discipline Preference’s instance configuration records establish for its channel set (Preference, Store instance model). Undeclared interpretations fail closed: when the gate’s precedence walk reaches the rule that would consult a field whose interpretation the deployment has not declared — or reaches the no-record deliver path with no declared channel_interpretation / default shape to deliver by — the subscriber’s disposition is failed with cause interpretation-undeclared (Action wiring step 5; raised at the consuming rule, so a higher-precedence suppression such as suspended wins first): never a silent deliver past an unevaluable value, and never a suppression reason that claims an evaluation that did not happen. “Field” here means the four interpreted preference fields (channel_preferences, frequency_limit, quiet_hours, format); metadata is stored opaque, never interpreted by this composition, and never triggers the rule. A deployment whose preference records can carry a field must declare that field’s interpretation; running without it is a standing configuration nonconformance the failed dispositions make visible.

  • quiet_hours_interpretation — the rule mapping a stored quiet_hours value to a predicate over the injected now: is this instant inside the principal’s quiet window? Must resolve the window in the recipient-local time the stored value declares (e.g., the timezone field in {start: "22:00", end: "07:00", timezone: "America/Los_Angeles"}). Regulated deployments under the TCPA must configure the interpretation so that, at minimum, the windows the regulation presumes (no calls/texts before 8am or after 9pm recipient-local time, 47 CFR — Code of Federal Regulations — §64.1200(c)(1)) evaluate as quiet when the principal’s stored value declares them. No default; a record carrying quiet_hours under an undeclared interpretation fails closed per the preamble rule.
  • frequency_limit_interpretation — the rule mapping a stored frequency_limit value to one or more (window, cap) pairs (e.g., {per_day: 5} → at most 5 deliveries per rolling 24 hours). Declares whether windows are rolling or calendar-aligned, and which timezone anchors calendar alignment. No default; fails closed per the preamble rule.
  • channel_interpretation — the rule mapping a stored channel_preferences map to the deliverable channel set for a disposition (e.g., values "opt-out" exclude the channel; everything else includes it), and mapping a stored format value to the envelope’s format field — including the format applied when a record supplies channel_preferences but no format (absence is no-preference, per Preference’s Behavior; this rule supplies the deployment default for the absent dimension). Includes the default shape — the channel set and format used wherever the deliver path has no stated channels: the deliver-unshaped path (see no_record_policy) and any record that carries preference fields but no channel_preferences (step 5.v). The declared default shape must have a non-empty channel set: an empty default would make every no-record deliver verdict collapse into suppress(channel-opt-out) — a suppression whose recorded reason names an opt-out no one stated; a deployment that wants no-record principals suppressed declares no_record_policy = suppress and gets the honest no-record reason instead. No default interpretation; the default shape is also deployment-declared.
  • no_record_policydeliver-unshaped | suppress. What the disposition is when Preference.current_for(principal_ref) returns none: deliver using the declared default shape, or suppress with reason no-record. deliver-unshaped is well-formed only when channel_interpretation’s default shape is declared; set without one, the no-record deliver path fails closed as fail(interpretation-undeclared) (rule v) rather than delivering silently. This is the fanout-on-no-record policy Preference’s Generation acceptance Check 6 requires the deployment to disclose; this composition surfaces it as a named knob so the disclosure is configuration, not narration. Default: deliver-unshaped (a principal who never stated preferences has not opted out; deployments whose regulatory posture requires opt-in suppress instead).
  • cap_policydrop | hold. Whether a frequency-cap suppression is terminal for this invocation (drop) or marked retry-eligible (hold) in the suppression event, signaling the deployment’s re-fanout machinery that the principal may be retried after the window relents. The composition itself never schedules a retry — hold is a classification on the record, not a queue (see Edge cases). Default: drop.
  • quiet_window_policydrop | hold. Same two values, same semantics, for quiet-window suppressions. Default: hold (a quiet-window suppression is the canonical “deliver later” case; the marking lets the deployment’s scheduler find it).
  • cap_serializationserialized-per-principal | best-effort. Declares whether the host serializes the frequency-cap evaluation and the delivery commit per principal — i.e., whether two concurrent invocations evaluating the same principal’s cap are forced to observe each other’s committed deliveries. Under serialized-per-principal, Invariant 4’s bound is unconditional within the declared window — which additionally obligates the implementation to evaluate against a count consistent with the journal at commit time: a missing or stale delivery_count_index entry must be rebuilt from the journal before the evaluation, or the declared capability is violated. This is conformant with the derived-index rule, not an exception to it: the index remains a rebuildable projection making no consistency claim of its own — the serialized evaluation path simply performs the rebuild inside the serialization boundary, which is exactly the guarantee a rebuild can honor. This per-principal serialization governs the cap path wherever it runs — the original fanout_shaped pass and every redispose re-evaluation alike; redispose’s own per-(fanout_id, principal_ref) serialization (its step 3) secures Invariant 9’s forward bound and does not subsume it, so two concurrent cap evaluations for the same principal — whichever action raised them — must still serialize against each other under this knob. Under best-effort, concurrent invocations can each read count = cap − 1 and both deliver — the bound holds at quiescence with overshoot bounded by the number of concurrent invocations, and the overshoot is detectable from the Event Log (see Invariant 4). Default: none — the deployment must declare one; regulated deployments whose frequency caps carry legal force (TCPA frequency restrictions) must declare serialized-per-principal.
  • journal_query_capability — the deployment’s declared guarantee that its Event Log instance’s read(query) supports the predicates this composition’s derivations and audits require: filter by event type, by fanout_id, by principal_ref, and by decided_at / wall-time range. Event Log’s own spec leaves the query shape to implementation policy — its Edge cases assign payload-field lookup to a separate Reverse Index pattern (forthcoming) — so this capability cannot be inherited from the atom; the deployment declares it directly, and every rebuild procedure, acceptance check, adversarial-scenario query, and the action surface itself (redispose steps 2–3 read the journal by fanout_id and by latest-event-per-principal at runtime) rests on it. When the Reverse Index pattern lands, it becomes the natural constituent home for this capability and the declaration thins to naming that peer. Required; no default; verification is an externally-clearable check (the declared predicates demonstrably work against the deployed instance).
  • payload_digest_function — the digest function applied to payload_content for the fanout.initiated entry and the consistency check (Generation acceptance Check 6). Deterministic; collision resistance appropriate to the deployment’s audit stakes. Required; no default.
  • reconciliation_window — the bound within which the deployment’s reconciliation resolves journal gaps and best-effort cap overshoots (the liveness arms of the invocation→dispositions relation and Invariant 4). A duration; required — the relation’s liveness arm binds every deployment, not only best-effort ones (a crash can gap any deployment’s journal). The setting rule: short enough that the liveness claims mean something to the deployment’s audit regime — for regulated deployments, no longer than the reporting or response period of the obligation the journal evidences; a window so long it never binds in practice is a vacuous declaration an auditor should flag. Disclosed with the other knobs.
  • invocation_duration_bound — the maximum wall-time a single fanout_shaped invocation’s loop may span. The one now is injected once at the seam (Clock semantics), so a long loop evaluates its last subscriber’s quiet/statutory window against a now read at invocation start; Invariant 3’s quiet-window safety for that late-loop subscriber rests on this bound being small enough that the injected now resolves the same quiet/statutory window when the subscriber is evaluated as it did at fired_at. A duration; required for any deployment carrying Invariant 3’s regulated (TCPA) arm — set no larger than the safety margin of the narrowest quiet or statutory window the deployment must honor; a deployment may declare it unbounded only by disclosing that no statutorily time-restricted channel is in scope (the same posture as omitting statutory_quiet_window). The mitigation where the bound would otherwise be material is a transport-layer re-check before dispatch (Clock semantics). Disclosed with the other knobs.
  • statutory_quiet_window — optional: a deployment-declared (window, channel set) pair evaluated for every disposition regardless of the principal’s stored preferences — the structural home for quiet windows the law imposes rather than the principal states. The TCPA’s 8am–9pm recipient-local presumption is not preference-conditional, so the single setting rule is: required for any deployment whose deliveries on a statutorily time-restricted channel fall within the restriction’s scope — for the TCPA, solicitations on voice or SMS (Short Message Service — text messaging); a deployment using those channels solely for messages outside the restriction’s scope (e.g., genuinely transactional alerts) may omit the knob, and that omission is itself a disclosed legal posture, not a silent default. Step 5.v excludes the declared channels while the injected now is inside the window, and an envelope emptied by the exclusion suppresses as quiet-window. Resolved recipient-locally under the same clock capability as quiet_hours_interpretation.
  • clock_tolerance — two declared components, because two distinct clock disciplines are consumed: (a) the cross-store skew bound — the maximum divergence expected between the host’s injected now (which stamps fired_at / decided_at) and the constituent stores’ own write clocks (Subscription’s subscribed_at / cancelled_at, Preference’s set_at / deleted_at) — consumed by Generation acceptance Checks 1 and 3; and (b) the accounting-clock regression bound — the host clock’s own maximum backward step across invocations — consumed by Invariant 4’s window arithmetic. A deployment may declare one number for both only when it genuinely bounds both; a zero-skew deployment with a 2-second time-synchronization step has a zero (a) and a nonzero (b). Required; mirrors Preference’s clock-tolerance disclosure discipline.

Primitive policies

Composition-boundary validation for both actions’ string-typed inputs:

  • event_scope — non-null, non-empty string (rejection: invalid-request). Opaque; no normalization, no case folding; exact-match when passed to Subscription.subscribers_for. Validated before any id is generated or constituent called. The composition imposes no semantic length cap, but the composing layer must bound event_scope — and the deployment must size the Event Log instance’s payload constraint (default 64 KB) for the fanout.initiated entry at its maximum fanout scope, since the entry journals the full queried list — so that the invocation entry fits; an oversized entry surfaces as journal-rejected at step 4, a definitive rejection, not a retryable outage.
  • payload_content — non-null (rejection: invalid-request). Opaque; carried unchanged into every created Notification’s envelope. Schema validation, size limits, and content restrictions belong to the composing layer before invoking the action. Note the envelope distinction: payload_content is the content element; the composition wraps it per recipient in an envelope {content, channels, format} whose channel and format fields vary by disposition (see Action wiring step 6 and Invariant 8).
  • fanout_id (as redispose input) — non-null, non-empty (rejection: invalid-request); opaque, byte-exact comparison against journaled ids; a non-empty value matching no fanout.initiated entry is not-known, not invalid-request (redispose steps 1–2). principal_ref and payload_content carry the same rules on redispose as on fanout_shaped.
  • principal_ref / subscriber_ref equality — the composition equates Subscription’s subscriber_ref and Preference’s principal_ref byte-exactly, with no normalization. Preference declares exactly this posture for principal_ref (exact equality, no canonicalization); Subscription stores and matches subscriber_ref as an opaque value it never parses or normalizes but states equality semantics explicitly only for event_scope — so the byte-exact rule here is this composition’s own declared policy, conservative against both constituents. The deployment must therefore register subscriptions and preference records under one canonical reference per person — the unified principal namespace obligation. A deployment that subscribes User-42 but records preferences under user-42 gets none from current_for and the no_record_policy path, silently unshaped or suppressed. The namespace coincidence is a deployment-configuration capability the records alone cannot prove; it is routed to an externally-clearable check (Generation acceptance).

Action wiring

The composition exposes three surfaces: the fanout itself; redispose — the journaled, gate-evaluated retry surface for a single principal of a prior invocation; and a composition-introduced reconciliation surface (reconcile_gaps / reconcile_overshoots, defined after redispose) that owns the journal-gap and cap-overshoot repair writes.

fanout_shaped(event_scope, payload_content) → {fanout_id, created: [(principal_ref, notification_id), ...], failed: [(principal_ref, cause), ...], suppressed: [(principal_ref, reason, preference_id | none), ...]} | rejected(invalid-request | subscribers-unavailable | journal-rejected)

  1. Validate inputs. event_scope non-empty; payload_content non-null. Either failure → rejected(invalid-request). No id is generated; no constituent is called.
  2. Generate fanout_id — opaque, system-generated, invocation-unique (a direct effect: entropy.generate()). The correlation handle binding the invocation’s journal entries together. Unlike Notification Fanout — where fanout_id is ephemeral unless the caller composes Event Log — here the id is always durable: step 4 journals it.
  3. Query the subscriber set: Subscription.subscribers_for(event_scope). If the subscription store is unavailable (infrastructure failure at the read), return rejected(subscribers-unavailable); nothing has been written; the generated fanout_id is discarded.
  4. Journal the invocation: EventLog.append({type: "fanout.initiated", fanout_id, event_scope, queried: [subscriber_ref, ...], config_version, payload_digest, fired_at}), where queried is the exact subscriber list step 3 returned (journaled so that coverage accounting and crash-gap retry read the invocation’s true scope from the journal — never from a runtime reconstruction against Subscription’s stores, whose declared runtime queries are current-state only), config_version identifies the deployment’s interpretation-configuration version in force (Configuration preamble), fired_at is the host-injected now of the invocation, and payload_digest is the declared payload_digest_function applied to payload_content (the digest, not the content, is journaled — see Edge cases, Payload data in the journal). If the append is rejected (invalid-payload storage-failure), return rejected(journal-rejected): the composition does not run an unjournaled fanout, because every downstream guarantee (Invariants 1, 4, 5, 7) reads from the journal. The single rejection name covers both append outcomes deliberately: both share the same immediate consequence — the invocation must not proceed unjournaled — and the composition keeps its rejection taxonomy at the invocation level rather than re-exporting Event Log’s two codes. The cost is that the caller distinguishes the retryable case (storage-failure — retry succeeds when the store recovers) from the definitive one (invalid-payload — an oversized event_scope, per Primitive policies — rejects identically on retry) only by retrying; deployments that need the distinction at first rejection read it from their Event Log instance’s own telemetry. No Notification record has been created. If the queried subscriber set is empty, the append carries the invocation anyway and the action returns {fanout_id, created: [], failed: [], suppressed: []} — a valid, journaled, empty fanout.
  5. For each subscriber_ref in the queried set, evaluate the shaping gate. Read Preference.current_for(subscriber_ref)record | none. Fail-closed discipline — one pre-gate guard, then two in-gate verdicts. First the pre-gate guard, evaluated before shaping_disposition is invoked at all: if the Preference read itself fails (infrastructure failure — distinct from a successful read returning none), no rule can be evaluated because the record gates every rule, so the subscriber’s disposition is failed with cause preference-unreadable. This is the orchestration’s guard, not a gate verdict — it never reaches shaping_disposition (which is why preference-unreadable is absent from the gate’s codomain), it never routes through no_record_policy, and an outage of the preference store must never deliver unshaped to suspended and quiet-houred principals. Then the gate’s own two in-gate fail verdicts, raised at the precedence point where each input is first consulted — so a higher-precedence suppression that does not depend on it is rendered first and the recorded reason remains the first rule that fired (Invariant 2) — firing only if no earlier rule has already suppressed: interpretation-undeclared (the record carries a field whose interpretation the deployment never declared, Configuration preamble) is raised at the rule that would consult that field — rule iii for quiet_hours, rule iv for frequency_limit, rule v for channel_preferences / format / the no-record default shape; accounting-unreadable (a record carrying frequency_limit whose delivery_count_index entry is unavailable and whose cold-start rebuild read against the Event Log fails) is raised at rule iv. A Suspended record therefore suppresses as suspended (rule i) without ever consulting the count or any interpretation, even during an Event Log outage; no fail-closed cause ever yields a deliver verdict. Otherwise, assemble the evaluation inputs: the record (or its absence), the injected now, the principal’s recent delivery history from delivery_count_index — supplied as the (decided_at, channels) set, or as an explicit unavailable marker when the index misses and the cold-start rebuild read fails (the marker is consulted only if rule iv is reached, per above) — and the Configuration’s interpretation rules (each carrying, per field, whether its interpretation is declared). Evaluate shaping_disposition — the composition-introduced pure gate whose signature, inputs, and purity contract are defined in §The load-bearing wiring decision (Mechanism) — which renders the verdict by the declared precedence order: earlier rules win, and evaluation stops at the first suppression:
    1. Suspended: the in-effect record’s status is suspendedsuppress(suspended). A Suspended record suppresses even though the Subscription is Active — pause means pause (Preference Invariant 6 guarantees the paused values are intact; this gate is what makes the pause operative).
    2. No record: current_for returned none → per no_record_policy: suppress(no-record), or fall through to channel selection (rule v) with the declared default shape — where the statutory_quiet_window exclusion still applies, so a no-record principal can still suppress as quiet-window. The deliver-unshaped branch presupposes a declared default shape; a deployment that sets deliver-unshaped without one hits rule v’s fail-closed (fail(interpretation-undeclared)) — a surfaced configuration nonconformance, never a silent deliver.
    3. Quiet window: the record carries quiet_hours → if quiet_hours_interpretation is undeclared, fail(interpretation-undeclared); else if quiet_hours_interpretation(quiet_hours, now) is insidesuppress(quiet-window).
    4. Frequency cap: the record carries frequency_limit → if frequency_limit_interpretation is undeclared, fail(interpretation-undeclared); else if the delivery history is the unavailable marker (index miss plus failed cold-start rebuild), fail(accounting-unreadable); else, for any interpreted (window, cap) pair (channel-scoped pairs counting only history entries naming that channel), the count of recent delivery-history entries falling inside the window meets or exceeds the cap → suppress(frequency-cap).
    5. Channel selection: if the channel_interpretation (or, on a path that needs it, the declared default shape) is undeclared, fail(interpretation-undeclared); otherwise channel_interpretation maps the record’s channel_preferences — or, where the deliver path has no stated channels (the no-record deliver path, and a record that carries other preference fields but no channel_preferences; absence is no-preference, so the deployment default fills the channel dimension), the declared default shape — to the deliverable channel set and format. Channels inside a declared statutory_quiet_window containing the injected now are then excluded from the set (Configuration; the statutory exclusion is recorded in evaluation_inputs). An empty resulting set → suppress(channel-opt-out) if emptied by preference values, suppress(quiet-window) if emptied by the statutory exclusion. Otherwise → deliver(channels, format).

    A rule whose input is absent is skipped (a record with no quiet_hours evaluates no quiet window — absence is no-preference, per Preference’s Behavior). The precedence order is fixed by this spec, not configurable: a deployment that re-orders it changes which reason gets recorded, and cross-deployment audit then cannot interpret suppression events uniformly.

  6. Commit the disposition.
    • Deliver verdict: call Notification.create(subscriber_ref, {content: payload_content, channels, format}), then EventLog.append({type: "fanout.created", fanout_id, principal_ref, notification_id, channels, format, preference_id | none, evaluation_inputs, decided_at})evaluation_inputs here records what the gate observed in rendering the deliver verdict: the observed record status (active none; a deliver verdict is never rendered on suspended), the injected now, where the record carried a frequency_limit the interpreted (window, cap) pairs with the in-window count the gate computed for each, and — where a statutory_quiet_window is declared — the statutory-exclusion evaluation step 5.v performed (the window and the channels it excluded, possibly none). The observed status must be recorded because it is the one field Preference’s Invariant 1 leaves mutable — a replay reading the record later sees its current status (perhaps deleted after supersession), so the status the gate saw lives in the event, not in the record. Recording the observation on the fanout.created (deliver-verdict) events, not only suppressions, is what makes deliver verdicts replayable (Replay semantics) and makes a best-effort overshoot diagnosable: the event shows the count the gate actually saw, which a post-hoc recount of the journal cannot recover once concurrent commits have landed. These two writes are the disposition’s truth-bearing pair and commit under the action’s multi-write atomicity obligation (execution-contract.md §Multi-write atomicity) — the delivery record and its journal evidence land together or not at all for this subscriber. If the pair cannot commit (Notification’s invalid-request, an infrastructure failure on either write — handled at this boundary exactly as Notification Fanout handles them), the subscriber’s disposition becomes failed: append {type: "fanout.create-failed", fanout_id, principal_ref, cause, preference_id | none, decided_at} and add (principal_ref, cause: create-failed) to failed. The gate’s fail-closed outcomes land the same way, with their causes: preference-unreadable (raised before the gate runs) and the gate’s own fail(accounting-unreadable | interpretation-undeclared) verdicts (each raised at the consuming precedence rule, per step 5). If even the failure append is rejected, the subscriber is still added to failed in the returned result, and the journal’s incompleteness for this invocation is detectable as a coverage gap against fanout.initiated (see Invariant 1’s at-quiescence form and Edge cases, Crash and journal-gap reconciliation). The delivery_count_index entry for the principal is updated after the pair commits — outside the pair’s atomicity surface, per Composition state.
    • Suppress verdict: EventLog.append({type: "fanout.suppressed", fanout_id, principal_ref, reason, preference_id | none, evaluation_inputs, retry_eligible, decided_at})evaluation_inputs records what the gate saw (the observed record statusactive suspended none — recorded because status is Preference’s one mutable field and replay cannot recover it from the record later; the injected now; for a cap verdict, the violated (window, cap) pair and the in-window count it computed; for a quiet-window verdict, the interpreted window). retry_eligible is defined for every reason: quiet-window and frequency-cap carry the quiet_window_policy / cap_policy marking (hold → true, drop → false); suspended, no-record, channel-opt-out, and unsubscribed (a reason only redispose produces — see its step 4) are always false — those suppressions relent only by the principal’s own action (a fresh set, a fresh subscribe), never by the passage of time, and even after such an action the suppression stays terminal for this invocation (redispose’s retryability rule forecloses it); the changed state governs future invocations, which is where it takes effect. Add (principal_ref, reason, preference_id | none) to suppressed. A rejected suppression append is the same journal-gap case as above: the entry joins the returned suppressed list and the gap is reconcilable against fanout.initiated.
    • Per-subscriber dispositions are independent: a failure for one subscriber neither aborts nor delays the rest (the Notification Fanout failure-isolation discipline, inherited unchanged). Order across subscribers is unspecified; parallel execution is permitted provided each subscriber’s truth-bearing pair commits independently.
  7. Return {fanout_id, created, failed, suppressed}. The three lists are unordered.

redispose(fanout_id, principal_ref, payload_content) → created(notification_id) | failed(cause) | suppressed(reason, preference_id | none) | rejected(invalid-request | not-known | not-retryable | payload-mismatch | journal-rejected)

The journaled retry surface. Every recovery path in this spec — retrying a failed delivery, re-attempting a hold-marked suppression after its window relents, resolving a crash-gap subscriber — re-disposes through this action, never through a direct Notification.create: a direct create would bypass the gate (delivering inside a quiet window that opened since the original verdict, or past a cap that has since filled) and would produce a Notification record with no journal pair — the exact artifact the breach-forensics scenario treats as structural evidence of bypass. redispose keeps both guarantees: the gate re-evaluates with fresh inputs, and the outcome is journaled under the original invocation’s fanout_id.

  1. Validate: payload_content non-null, principal_ref non-empty, fanout_id non-empty; otherwise rejected(invalid-request).
  2. Resolve the invocation: the (non-empty) fanout_id must resolve (via the journal) to a fanout.initiated entry; otherwise rejected(not-known). If the journal read itself fails (infrastructure), rejected(journal-rejected) — nothing has been evaluated or written. The supplied payload_content must digest (under the declared payload_digest_function) to the entry’s payload_digest; otherwise rejected(payload-mismatch) — the retry is bound to the original content, which is what lets Invariant 8 extend across redispositions.
  3. Check retryability: a principal covered by a fanout.abandoned record for this fanout_id is rejected(not-retryable) regardless of their disposition state — abandonment is terminal on every path. Otherwise the latest journaled disposition event for (fanout_id, principal_ref) must be a fanout.create-failed, a fanout.suppressed with retry_eligible: true, or a fanout.created whose Notification record’s status — read via Notification.status_of(notification_id), the constituent’s declared query surface — is failed or expired (the transport-failure retry path: Notification’s own retry model is a new create per attempt, and this is where that new create routes so the re-send is re-shaped and journaled rather than bypass-shaped; whether a given transport outcome is failed or expired is the deployment’s disclosed fail-vs-expire policy, per Notification’s Generation acceptance); otherwise rejected(not-retryable) — a principal whose latest create’s Notification record is Pending or Delivered is never re-delivered under the same invocation. A principal with no disposition event under this fanout_id is admitted only on the crash-gap path: the principal must appear in the fanout.initiated entry’s journaled queried list — the invocation’s true scope, read from the journal, never reconstructed at runtime against Subscription’s stores; otherwise rejected(not-retryable) — a principal absent from the journaled queried list was never in the invocation’s scope and has no gap to repair. Serialization: this check and step 4’s commit must be serialized per (fanout_id, principal_ref) — against concurrent redispose calls and against the original invocation’s disposition commit for the same principal — a host conformance requirement of the same kind as Preference’s per-principal write serialization. Without it, two concurrent redisposes can both observe a retryable latest event and both deliver; Invariant 9’s forward bound rests on this named obligation, so the requirement is conformance, not configuration (see Edge cases — Cross-store consistency under partial failure).
  4. Re-check the audience, then re-evaluate and commit: first call Subscription.subscribed(principal_ref, entry.event_scope) — the constituent’s declared point query. The original loop accepts query-time staleness because its window is bounded by one invocation’s duration; redispose’s horizon is unbounded (a hold retry may fire days later), so the audience must be re-verified. On not-subscribed, commit fanout.suppressed with reason unsubscribed, retry_eligible: false, preference_id: none, and evaluation_inputs recording the audience re-check outcome (not-subscribed) and the injected now — what was actually evaluated was the subscription point query, not the gate, and the event says so; the principal left the audience between the invocation and the retry, journaled, never silently skipped. Otherwise run steps 5–6 of fanout_shaped for this one principal — a fresh host-injected now, a fresh Preference.current_for read, the current delivery history (the cap evaluation here participates in the cap_serialization per-principal serialization exactly as the original pass does — distinct from this action’s per-(fanout_id, principal_ref) serialization above) — and commit the disposition as there, with one named deviation this step owns: any disposition-event append the Event Log rejects (the deliver pair’s journal half, a suppression event, or a failure event) surfaces as rejected(journal-rejected) with nothing committed — and for the deliver pair nothing committed is enforced by the same cross-store multi-write atomicity boundary step 6 invokes (execution-contract.md §Multi-write atomicity: the Notification.create and its journal evidence land together or not at all, so a rejected journal half rolls the create back rather than stranding a live notification with no journal pair) — where the batch loop must instead degrade to a returned-list entry plus a reconcilable journal gap — the single-principal scope makes the clean rejection achievable, so redispose never widens a journal gap. The committed event carries the original fanout_id plus redisposition: true. The per-principal disposition chain under one fanout_id is therefore append-only, and the latest event is the operative disposition (the accounting rule Check 1 and the invocation→dispositions relation use).

Reconciliation surface (composition-introduced). Two operations the deployment’s scheduler invokes within the declared reconciliation_window to discharge the liveness arms of the invocation→dispositions relation and Invariant 4. They are the composition-introduced, capability-provenanced home for the two reconciliation event types — fanout.abandoned and fanout.cap-overshoot-reconciled — which no constituent action and no fanout_shaped / redispose step writes. Both read the journal through the declared journal_query_capability, compute a pure detector over the read result, and append through EventLog.append with a host-injected now; each carries no composition state — every input is the journal, and every record they write lives in Event Log, the sanctioned history store (execution-contract.md §Composition model: a composition that needs to record that a multi-step sequence occurred composes Event Log), so neither surface introduces a store, an identity model, or an invariant surface of its own. The deployment owns when to invoke them (the reconciliation_window cadence); the composition owns what they write.

reconcile_gaps(fanout_id, payload_content) → {repaired: [(principal_ref, created | failed | suppressed)], abandoned: [principal_ref | all]} | rejected(invalid-request | not-known | payload-mismatch | journal-rejected) — resolve an invocation’s journal gap. Resolve the fanout.initiated entry (else not-known); verify payload_content against its journaled payload_digest under the declared payload_digest_function (else payload-mismatch), since repair re-disposes through redispose, which is content-bound. Reconstruct the queried set from the entry’s journaled queried list, and for each principal with no disposition event — the gap set, per Check 1 — call redispose(fanout_id, principal_ref, payload_content) (its crash-gap admission path, redispose step 3, accepts exactly these principals). For any residue the deployment cannot resolve or chooses not to repair, append {type: "fanout.abandoned", fanout_id, principals | all, reason, decided_at} — the terminal that discharges the relation’s liveness arm for the covered principals (redispose rejects any principal an abandonment record covers, so the terminal is enforced, not advisory). A rejected append surfaces as rejected(journal-rejected) with nothing committed — a single record, no truth-bearing pair to orphan.

reconcile_overshoots(principal_ref) → {recorded: [(config_version, window, cap, committed)]} | rejected(invalid-request | journal-rejected) — discharge Invariant 4’s best-effort liveness arm. Run the overshoot detector: read the principal’s fanout.created events and, under each journaled config_version’s interpreted (window, cap) pairs, flag any window whose committed count exceeds its contemporaneous cap. For each overshoot, append {type: "fanout.cap-overshoot-reconciled", principal_ref, config_version, window, cap, committed, action, decided_at} — the record Generation acceptance Check 2 reads as the reconciliation evidence. Idempotent: an overshoot already carrying a reconciliation record is not re-recorded, so a re-invocation within the window adds nothing.

Replay semantics

Disposition replay — the procedure Invariant 7 and Generation acceptance Check 5 rest on: given a fanout.suppressed or fanout.created event carrying a gate verdict (an unsubscribed suppression carries redispose’s audience re-check instead — there is no gate verdict to replay, only the recorded point-query outcome), re-evaluating the gate with the event’s recorded inputs must reproduce the recorded verdict. The inputs split by mutability. The immutable inputs come from the record: Preference.read(preference_id) returns the value fields (channel_preferences, frequency_limit, quiet_hours, format) exactly as the gate read them (Preference Invariant 1 — every field except status is immutable; none is recorded as such). The mutable and ephemeral inputs come from the event’s own evaluation_inputs: the observed record status (status is the one field Invariant 1 leaves mutable, so the gate’s observation is journaled, never re-read), the injected now, and the observed in-window counts. For replay’s entry point, the gate factors as compute-counts ∘ verdict — the cap rule is a pure function of each (count, cap) pair once counts are computed from the timestamp set — and replay re-enters at the verdict factor with the recorded counts, so it needs no reconstruction of the timestamp set a concurrent commit could have shifted. The interpretation rules come from the deployment’s declared configuration at the version the invocation’s fanout.initiated entry journaled (config_version — versions are append-only and retained for the audit horizon, per the Configuration preamble), so a configuration change after the disposition cannot shift the replay’s ground. Because the gate is pure and the interpretations are required to be pure, deterministic, and total, replay divergence is a finding against exactly one of: the journal entry, the configuration disclosure, or the implementation’s gate.

The load-bearing wiring decision

The decision the composition exists to enforce: every subscriber returned by the query receives exactly one recorded disposition — delivered, failed, or suppressed-with-reason — and the shaping gate that renders the verdict sits structurally between the subscriber query and each create, so no create can bypass it.

Principle. Preference shaping is only worth composing if it is unbypassable and auditable. A gate that filters the subscriber list silently produces an unanswerable audit: a subscriber absent from the created list might have been suppressed by preference, lost to a failure, or skipped by a bug — three different liabilities, indistinguishable. The trichotomy makes the three outcomes structurally distinct, and the per-disposition Event Log append makes each one provable later.

Likely objection. “Why must suppression be recorded at all? Not delivering is the absence of an action — recording every non-delivery bloats the log.” For an unregulated feed, perhaps. But the regulatory questions this composition exists to answer are precisely about non-delivery: prove you did not text this person inside their quiet window is answered by the suppression record showing the gate fired; prove you honored the opt-out is answered the same way. An absence proves nothing; a classified suppression event carrying the reason, the preference record id, and the evaluation inputs proves the gate evaluated and what it saw. The cost is one append per suppression — and the frequency-cap accounting needs the delivery events in the log anyway, so the journal is already load-bearing.

Mechanism. The gate is the composition-introduced pure function shaping_disposition(principal_ref, preference_record | none, now, recent_delivery_history | unavailable, configuration) → deliver(channels, format) | suppress(reason) | fail(accounting-unreadable | interpretation-undeclared) — evaluated once per subscriber per disposition (the original invocation’s pass, and again on each redispose), between the query and the create (Action wiring step 5). The two fail(...) verdicts are the input-specific fail-closed outcomes: the gate raises them at the precedence rule that would consult the unavailable history or the undeclared field, so a higher-precedence suppression short-circuits before an unevaluable lower-precedence input is reached (preference-unreadable is not a gate verdict — without the record the gate is never invoked). Its delivery-history input is the principal’s recent delivery history — the delivery_count_index entry’s (decided_at, channels) pairs, or the unavailable marker — not a pre-computed count: frequency_limit_interpretation may yield several (window, cap) pairs (possibly channel-scoped), and each pair’s count is computed inside the pure function from the one history set. It is pure under the Logic Confinement Principle (execution-contract.md): now is injected by the host at the invocation’s single seam, never read inside; the history is an input from the derived index; the configuration is fixed at evaluation. Purity is what makes Invariant 7 (replayability) checkable: the recorded inputs determine the recorded verdict.

Result. The three lists partition the query result (Invariant 1); every suppression is classified and journaled (Invariant 5); the regulator’s quiet-window query, the disputing recipient’s 3am-text query, and the breach investigator’s bypass query are all answerable from the Event Log and the Preference store with no recourse to developer narration (Regulated adversarial scenarios; Generation acceptance).


Composition-level invariants

These invariants emerge from the composition; no constituent carries them alone. Each carries a Rests on: provenance line per pressure-testing.md §Capability provenance.

Two cross-constituent relations are declared first, per the structural-relation templates (spec-format.md §Cross-cutting authoring conventions):

  • Invocation → dispositions: one-to-many. The invocation side is mandatory — every disposition event names exactly one fanout_id that resolves to a fanout.initiated entry. The disposition side is mandatory at quiescence: every subscriber in the invocation’s queried set has at least one disposition event, and the latest event per (fanout_id, principal_ref) pair is the operative disposition (redispose appends to the chain, never rewrites it) — as safety (no action step skips a subscriber) plus liveness (a disposition missing through partial failure — a crash mid-loop, a rejected append — is surfaced by the coverage check against fanout.initiated and, within the declared reconciliation_window, either re-disposed via redispose or terminally discharged by a journaled fanout.abandoned record written by the composition’s reconcile_gaps surface — see Edge cases, Crash and journal-gap reconciliation), modulo that declared compensation. Never stated as a static “always complete” — a crash between step 4 and step 7 reachably leaves a journaled invocation with fewer dispositions than subscribers, and the spec’s claim is that this state is detectable and bounded, not impossible.
  • Principal → in-effect preference record: one-to-at-most-one, optional on the record side (Preference Invariant 3 guarantees uniqueness when present; absence is the no_record_policy path). Read-only — this composition never writes the relation. Referential integrity: every preference_id recorded in a disposition event resolves via Preference.read to a record that was in effect at decided_at (reconstructible by Preference’s Generation acceptance Check 2).

  • Invariant 1 — Disposition trichotomy. For any fanout_shaped invocation that returns a result (not rejected), the created, failed, and suppressed lists partition the subscriber set returned by Subscription.subscribers_for(event_scope) at the time of the query: every queried subscriber appears in exactly one list, no subscriber outside the query result appears in any, and |created| + |failed| + |suppressed| = |subscribers_for result|. The journal mirror of the partition holds at quiescence per the invocation→dispositions relation above, with the latest event per (fanout_id, principal_ref) as the operative disposition once redispositions have appended. Rests on: Subscription Invariant 6 (at most one Active subscription per (subscriber_ref, event_scope) — so the queried set has no duplicate refs); action wiring steps 5–7 (the composition-introduced loop); Event Log Invariants 1–2 (append-only, immutable journal entries).
  • Invariant 2 — Suppression precedence over subscription. A subscriber whose disposition is suppress(suspended | quiet-window | frequency-cap | no-record | channel-opt-out) receives no Notification.create in that invocation, even though their Subscription is Active. (The closed suppression set carries a sixth reason, unsubscribed, deliberately excluded from this invariant: it is a redispose-only outcome of the audience re-check against a principal who has left the audience — not a gate verdict against an Active subscriber — so it falls outside this invariant’s “even though Active” scope.) Suppression reasons are rendered in the fixed precedence order of action wiring step 5; the recorded reason is the first rule that fired. The verdict is evaluated against the preference record observed at disposition-evaluation time — a suspend committing after the gate read the record does not retroactively re-render the verdict (the staleness window is named in Edge cases, mirroring Notification Fanout’s subscriber-set staleness). Rests on: Preference Invariant 3 (at most one in-effect record) and Invariant 7 (current_for determinism); the composition-introduced shaping_disposition gate; Preference’s own declaration that the composing fanout pattern interprets its values.
  • Invariant 3 — Quiet-window safety (TCPA). Conditional invariant; antecedent inside the statement. Provided the deployment has declared quiet_hours_interpretation and supplies the recipient-local clock/timezone discipline that interpretation requires (a deployment-declared capability: the host’s injected now and the interpretation’s timezone resolution must be sound for the recipient’s locale), then: no Notification.create commits in any invocation for a principal whose preference record as observed by the gate at disposition evaluation carries quiet_hours containing the injected now under the declared interpretation; and, where the deployment declares a statutory_quiet_window, no committed envelope names a channel inside that window at the injected now (the statutory arm — the law’s window, evaluated for every principal, stored preference or none). The observation anchor is the same one Invariant 2 carries — a superseding set landing between the gate’s read and the create’s commit is the named staleness window, not a violation. The gate’s quiet-window rule precedes channel selection, and the statutory exclusion runs inside channel selection, so no deliver verdict can place a message inside either window. Rests on: Preference Invariant 1 (the stored quiet_hours value is immutable — what the gate read is what the principal stated); the composition-introduced gate (step 5.iii); the deployment-declared quiet_hours_interpretation (Configuration) and the recipient-local clock capability (an attestation, not a knob — declared and verified in Generation acceptance’s externally-clearable checks); and the deployment-declared invocation_duration_bound (Configuration), on which the injected now’s meaningfulness for a late-loop subscriber rests.
  • Invariant 4 — Frequency-cap safety. Conditional invariant, stated per-commit and at quiescence. Provided the deployment has declared frequency_limit_interpretation, the bound is anchored to each delivery’s own observation — at every committed delivery disposition, the in-window count the gate observed (recorded in the event’s evaluation_inputs) was strictly below every cap interpreted from the preference record observed at that disposition. The anchor matters: a principal who lowers their cap mid-window leaves a standing in-window count above the new cap with zero gate misbehavior — the bound is per-commit against the contemporaneous record, never a retrospective recount against the current one. On top of that anchor: (safety) under cap_serialization = serialized-per-principal, the observed count equals the true committed count (the gate’s read and the delivery’s commit are serialized per principal), so no two invocations can both observe headroom on the last slot and the per-window committed total respects each delivery’s contemporaneous cap; (under best-effort) two invocations may each observe count = cap − 1 and both commit (the time-of-check-to-time-of-use race), so the residual claim is: any overshoot is bounded by the number of concurrently-evaluating invocations, is diagnosable from the Event Log (each fanout.created event’s recorded observation shows the headroom the gate saw), and is detected and recorded within the declared reconciliation_window (liveness) — by the composition’s reconcile_overshoots surface (Action wiring), which the deployment invokes alongside journal-gap repair: it runs the overshoot detector (scan delivery events per principal under each journaled config_version’s interpreted windows; any window whose committed count exceeds its contemporaneous cap is an overshoot) and appends {type: "fanout.cap-overshoot-reconciled", principal_ref, config_version, window, cap, committed, action, decided_at} — the record Check 2 reads as the reconciliation evidence. The race, not the arithmetic, is the load-bearing hazard, and it is the formal layer’s verification subject (see Lineage notes §Formal model). One further clock assumption is named rather than hidden: the window arithmetic compares decided_at values stamped by different invocations’ injections of now, so the accounting assumes the host clock is non-decreasing across invocations to within the declared clock_tolerance component (b) (the accounting-clock regression bound — distinct from the cross-store skew component) — under a larger backward step the in-window count can deflate and admit deliveries past the cap with zero gate misbehavior, the same best-effort posture as Event Log’s own Invariant 7 (sequence order authoritative, wall-time best-effort). Rests on: Event Log Invariants 1–4 (the delivery events are append-only, immutable, totally ordered — the count’s derivation source) under the declared journal_query_capability; the composition-introduced gate and delivery_count_index (a derived index, outside the atomicity surface by construction); the deployment-declared cap_serialization capability and clock_tolerance.
  • Invariant 5 — No silent disposition. Every suppression and every delivery failure lands as a classified Event Log event (fanout.suppressed with reason, preference_id | none, evaluation_inputs, retry_eligible; fanout.create-failed with cause, preference_id | none) bound to its invocation by fanout_id. At quiescence (per the invocation→dispositions relation), no subscriber’s non-delivery is unexplained: the journal answers why was this person not delivered to for every queried subscriber. Rests on: Event Log’s append action and Invariants 1–2; action wiring step 6; the journal-rejected rejection in step 4 (the composition refuses to run unjournaled).
  • Invariant 6 — Constituent integrity. The composition never writes the Subscription store (subscribers_for and subscribed are its only Subscription calls), never writes the Preference store (current_for / read only), and writes the Notification store only through create (status_of is its only other Notification call). All Subscription invariants, all Preference invariants (1–10 plus Temporal property 11), all nine Notification invariants, and all seven Event Log invariants hold over their stores; every constituent is reached through its declared action and query surface, never by direct store access. Rests on: the constituents’ own invariants as written; the atom interface contract (execution-contract.md §The atom interface contract).
  • Invariant 7 — Disposition replayability. shaping_disposition is a pure function of its recorded inputs: for every journaled deliver-or-suppress disposition the gate rendered (unsubscribed events record redispose’s audience re-check, not a gate verdict — their replay is the recorded point-query outcome itself; the gate’s fail(accounting-unreadable | interpretation-undeclared) verdicts record an infrastructure-or-configuration condition rather than a reproducible deliver-or-suppress verdict, and are journaled as fanout.create-failed — all of these are outside this invariant’s quantifier), re-evaluating the gate with the event’s recorded inputs (preference record via Preference.read, injected now, the observed in-window counts from evaluation_inputs, declared interpretations) reproduces the recorded verdict, per Replay semantics. Rests on: Preference Invariant 1 (value-field immutability — the replay reads the values the gate read; the mutable status is replayed from the event’s recorded observation instead, per Replay semantics) and Invariant 9 (durability — the record is still there); Event Log Invariant 2 (the recorded inputs are immutable); the composition-introduced gate’s purity (Logic Confinement: now injected, never read inside); the deployment-declared interpretation rules’ required determinism (Configuration).
  • Invariant 8 — Payload-content consistency. Every Notification record created under a single fanout_id — by the original invocation or by any later redispose — carries the same content element in its envelope: the payload_content whose digest the invocation’s fanout.initiated entry recorded (redispose verifies the digest before re-evaluating, so the binding extends across redispositions). The envelope’s channels and format fields vary per recipient (that variation is the composition’s purpose); the content does not. This is Notification Fanout’s payload-consistency guarantee restated at the envelope’s content level. Rests on: action wiring steps 1 and 6 plus redispose step 2 (one validated, digest-bound payload_content, wrapped per recipient); Notification Invariant 1 (the created record’s payload is immutable); the deployment-declared payload_digest_function.
  • Invariant 9 — At most one live notification per subscriber per invocation. Live means a Notification record in Pending or Delivered state. The queried set contains at most one entry per subscriber (Subscription Invariant 6); the loop renders one disposition per entry; only a deliver verdict creates, and it creates exactly one record. redispose preserves the bound forward: it rejects (not-retryable) any principal whose latest disposition under the fanout_id is a fanout.created whose record is Pending or Delivered, admitting a created-again retry only when the prior record has reached transport failed or expired (each such retry is a fresh create per Notification’s own retry model, so an invocation may accumulate transport-failed records, but never two live ones); its retryability check and commit are serialized per (fanout_id, principal_ref) (the named host conformance requirement of redispose step 3). Rests on: Subscription Invariant 6; Notification Invariants 2–4 (status monotonicity — no record returns from a terminal state, so a failed/expired adjudication is stable — plus terminal exclusivity and status–timestamp match: together what makes “live” decidable from status_of); action wiring steps 5–6; redispose step 3, including its named per-(fanout_id, principal_ref) serialization requirement.
  • Invariant 10 — Invocation identity is unique and journaled. Each non-rejected invocation carries a fanout_id no other invocation shares, journaled in fanout.initiated before any disposition commits; every disposition event for the invocation carries it. The journaling is unconditional — Event Log is a constituent, not an option, and the composition rejects rather than run unjournaled (step 4). The journal’s persistence across crashes is Event Log’s own deployment-shaped obligation: the deployment must provision the journal’s store durably for the audit horizon its regime requires, a named obligation routed to the externally-clearable disclosures. Rests on: the composition-introduced id generation (step 2 — invocation-unique by construction) and journaling (step 4); Event Log Invariants 1–2 (append-only, immutable entries — what is journaled stays as journaled).

Trichotomy (Invariant 1) plus no-silent-disposition (Invariant 5) give the accountable fanout property — every queried subscriber’s outcome is recorded and classified. Quiet-window safety, cap safety, and suppression precedence (Invariants 2–4) give the shaping is unbypassable property — the regulated suppression rules cannot be skipped on any create path. Replayability (Invariant 7) gives the defensible verdict property — every recorded verdict can be independently re-derived.


Examples

The walkthrough deployment declares channels ["email", "sms", "push"], no_record_policy = deliver-unshaped with default shape {channels: [email], format: "plain"}, cap_policy = drop, quiet_window_policy = hold, cap_serialization = serialized-per-principal, frequency_limit_interpretation reading {per_day: N} as a rolling 24-hour cap, quiet_hours_interpretation resolving stored windows in the record’s own timezone, a statutory_quiet_window of 21:00–08:00 recipient-local on {sms} (its SMS sends include solicitations), a journal_query_capability covering the four required predicates, a payload_digest_function, a one-hour reconciliation_window, a five-second invocation_duration_bound, a 500ms clock_tolerance declared for both components (the deployment attests the one bound covers cross-store skew and accounting-clock regression alike), and interpretation-configuration version cfg_v3 in force throughout.

Walkthrough — one invocation, all three dispositions

Four team members subscribe to task:assigned. Their preference states at fanout time: ana — Active record pref_a {channel_preferences: {email: "preferred", sms: "opt-out"}, format: "plain"}; ben — Suspended record pref_b (vacation pause); cho — Active record pref_c with quiet_hours: {start: "22:00", end: "07:00", timezone: "Asia/Tokyo"} — and it is 23:10 in Tokyo; dia — no record.

fanout_shaped("task:assigned", {task_id: t7, assigned_by: manager_m}):

  • Step 2–4: fanout_id = fx_01; subscribers_for("task:assigned") → [ana, ben, cho, dia]; fanout.initiated journaled with queried: [ana, ben, cho, dia], config_version: cfg_v3, and fired_at.
  • ana: record Active; no quiet hours; no cap; channel interpretation → {channels: [email], format: "plain"}Notification.create(ana, {content, channels: [email], format: "plain"}) → notif_91; fanout.created journaled. → created.
  • ben: record Suspended → suppress(suspended); fanout.suppressed journaled with preference_id: pref_b. → suppressed. (Rule 1 fired; ben’s quiet hours, had he any, were never evaluated — precedence.)
  • cho: record Active; 23:10 Tokyo is inside the stored window → suppress(quiet-window); fanout.suppressed journaled with evaluation_inputs carrying the injected now and the interpreted window, retry_eligible: true (policy hold). → suppressed.
  • dia: current_for(dia) → none; policy deliver-unshaped → default shape → Notification.create(dia, {content, channels: [email], format: "plain"}) → notif_92; fanout.created journaled with preference_id: none. → created.
  • Returns {fanout_id: fx_01, created: [(ana, notif_91), (dia, notif_92)], failed: [], suppressed: [(ben, suspended, pref_b), (cho, quiet-window, pref_c)]}.

The deployment’s scheduler later queries retry-eligible suppressions for fx_01, finds cho’s, and after 07:00 Tokyo calls redispose(fx_01, cho, payload) — the audience re-check confirms cho is still subscribed (subscribed → subscribed), the gate re-evaluates with a fresh now, the quiet window no longer contains it, and the disposition commits as created(notif_93) under the same fanout_id with redisposition: true. Had cho cancelled in the interim, the retry would have committed suppressed(unsubscribed) instead — journaled, never a delivery to someone who left. The composition never schedules; the scheduler decides when, the gate decides whether.

Frequency cap firing

eli’s record carries frequency_limit: {per_day: 3}. Deliveries committed for eli on Monday at 09:00, 11:00, and 19:00 (the delivery_count_index, derived from eli’s fanout.created events). A fourth fanout fires Monday at 21:00: the gate observes three timestamps inside the rolling 24-hour window, count 3 ≥ cap 3 → suppress(frequency-cap); the journaled evaluation_inputs record {count: 3, window: rolling-24h, cap: 3} and retry_eligible: false (policy drop). No Notification record is created. A fifth fanout on Tuesday at 13:00 finds only Monday 19:00 inside the window — count 1 — and delivers.

Best-effort cap overshoot and reconciliation

A deployment that cannot serialize per principal declares cap_serialization = best-effort. Principal fred carries frequency_limit: {per_day: 2} and has one delivery in the rolling 24-hour window (count 1). Two invocations — fx_20 and fx_21 — fire for fred at nearly the same instant. Each gate independently reads the history, observes count 1 < cap 2, and commits a delivery: fx_20’s fanout.created records {count: 1, window: rolling-24h, cap: 2}; fx_21’s records the same observed {count: 1} — neither saw the other’s not-yet-committed delivery (the time-of-check-to-time-of-use race). The committed total is now 3 against a cap of 2: a one-delivery overshoot, bounded by the two concurrent invocations.

Nothing is hidden. Each fanout.created event carries the headroom its gate actually saw, so Invariant 4’s per-commit anchor still holds — each delivery was below cap at its own observation — and the overshoot is diagnosable from the journal: a post-hoc recount of fred’s window finds 3 committed deliveries under a cap of 2. Within the declared reconciliation_window, the deployment’s scheduler invokes reconcile_overshoots(fred), whose detector flags the window and appends {type: "fanout.cap-overshoot-reconciled", principal_ref: fred, config_version: cfg_v3, window: rolling-24h, cap: 2, committed: 3, action, decided_at} — the record Generation acceptance Check 2 reads as the evidence that the overshoot was detected and recorded within the window (Invariant 4’s liveness arm, discharged). A regulated deployment whose caps carry legal force would instead have declared serialized-per-principal, under which the second gate observes count 2 and suppresses frequency-cap — no overshoot is possible.

Statutory window firing — no stored quiet hours

finn holds an Active record pref_f with channel_preferences: {sms: "preferred", email: "opt-out", push: "opt-out"} and no stored quiet_hours. A fanout fires at 22:40 in finn’s locale. Rule iii is skipped (no stored window — absence is no-preference); rule v maps the record to {sms}, then the declared statutory_quiet_window (21:00–08:00 on {sms}) excludes it — the set is emptied by the statutory exclusion, so the disposition is suppress(quiet-window) with retry_eligible: true and evaluation_inputs recording the statutory window and the excluded channel. The law’s window binds finn even though finn never stated one — that is the statutory arm’s whole point. After 08:00 finn-local, the scheduler’s redispose delivers.

Marketing newsletter — channel opt-out under CAN-SPAM

A different deployment (a marketing platform) runs the same composition with no_record_policy = suppress — its regulatory posture treats absent preferences as no permission to shape a commercial send. Subscriber gus holds an Active record pref_g with channel_preferences: {email: "opt-out", sms: "opt-out", push: "opt-out"} — a full opt-out recorded after an unsubscribe click. Subscriber hana has subscribed but never opened the preferences page: no record. A campaign fanout fires: gus’s channel interpretation yields an empty deliverable set → suppress(channel-opt-out) with preference_id: pref_g — the journaled event is the CAN-SPAM honored-opt-out evidence; hana’s current_for returns nonesuppress(no-record) with preference_id: none, per this deployment’s declared policy. Neither receives a Notification record; both non-deliveries are classified, distinct, and queryable — the difference between they said no and they never said is preserved in the records.

Rejection paths

  • fanout_shaped("", {task_id: t9})rejected(invalid-request) — nothing generated, queried, or written.
  • fanout_shaped("task:assigned", null)rejected(invalid-request).
  • Subscription store down at step 3 → rejected(subscribers-unavailable) — no journal entry, no creates.
  • Event Log rejects the fanout.initiated append at step 4 (storage-failure) → rejected(journal-rejected) — the composition refuses to run an unjournaled fanout; no Notification record exists for the invocation. The same rejection covers a definitive invalid-payload (an oversized event_scope); retry distinguishes the two.
  • redispose(fx_01, ana, payload) when ana’s latest disposition under fx_01 is fanout.created and status_of(notif_91) returns Pending or Delivered → rejected(not-retryable) — one invocation never holds two live deliveries for the same principal (the transport-failure path admits her only once notif_91 has reached failed or expired).
  • redispose(fx_99, cho, payload) where no fanout.initiated entry carries fx_99rejected(not-known); redispose(fx_01, cho, altered_payload) where the digest does not match fx_01’s journaled payload_digestrejected(payload-mismatch) — the retry is bound to the original content.

Fail-closed gate — preference store outage

A fanout fires while the Preference store is down. For each subscriber, current_for fails at the read (an infrastructure failure, not a none): the gate renders no verdict, routes nothing through no_record_policy, and the subscriber’s disposition is failed with cause preference-unreadable — journaled, classified, retry-eligible via redispose once the store recovers. A suspended principal and a quiet-houred principal are not delivered to unshaped during the outage; the outage degrades to named failures, never to silent unshaped delivery. The same shape covers interpretation-undeclared (a record carries quiet_hours but the deployment never declared the interpretation) and accounting-unreadable (a cap-carrying record whose count cannot be rebuilt because the journal read fails) — each raised at the precedence rule that would consult the missing input (rule iii and rule iv respectively), so a Suspended record carrying either still suppresses as suspended first.

Partial failure

A fanout fx_07 to [ana, eli, fay]: ana’s disposition pair commits (created); eli’s create hits an infrastructure failure mid-commit — the truth-bearing pair aborts together, fanout.create-failed is journaled with cause create-failed, eli → failed; fay’s pair commits (created). Returns all three classified. The caller retries eli with redispose(fx_07, eli, payload): the gate re-evaluates (eli’s preferences may have changed, the quiet window may now apply — the retry earns delivery, it does not assume it), and the outcome lands in the journal under fx_07. A direct Notification.create(eli, envelope) is not the retry path — it would bypass the gate and produce the unjournaled Notification that breach forensics reads as bypass evidence. A fresh fanout_shaped (a new invocation against the current Active set) remains correct when re-fanning the whole scope is the intent — composing Duplicate Prevention if at-most-once across such retries is required.

Transport failure after a committed create: back on fx_01, ana’s notif_91 later fails in transport (the transport layer calls Notification.fail). The re-send also routes through redispose(fx_01, ana, payload): step 3’s transport-failure path admits her because status_of(notif_91) returns failed; the audience re-check and the gate run fresh (if it is now 23:30 in ana’s declared quiet hours, the re-send is suppressed — the retry earns delivery under current rules); a deliver verdict commits a new create, notif_95, as Notification’s retry model prescribes — a distinct record with its own outcome, journaled under the invocation, with notif_91 remaining in Failed as the audit record of the first attempt.

Regulated adversarial scenarios

  • Regulator audit — “show every message delivered inside a declared quiet window.” A TCPA auditor asks for all deliveries to principals whose in-effect preferences declared a quiet window containing the delivery moment. Procedure, records alone: enumerate fanout.created events in the audit period (Event Log read, time-ranged); for each, fetch the recorded preference_id via Preference.read (immutable, durable); where the record carries quiet_hours, evaluate the deployment’s disclosed quiet_hours_interpretation against the event’s decided_at. By Invariant 3 the result set is empty — any non-empty result is a conformance violation, and the violating event itself carries the evidence (the verdict’s inputs are journaled). The auditor separately samples fanout.suppressed(quiet-window) events and replays each verdict (Invariant 7) to confirm the gate was evaluating, not rubber-stamping.
  • Disputed delivery — “I was texted at 3am.” A principal complains of a 3am SMS (Short Message Service — text message). The investigator reconstructs from records: the principal’s in-effect preference record at the delivery moment (Preference Generation acceptance Check 2 — max set_at ≤ t within the in-effect window); the disposition — either a fanout.created event whose decided_at, channels, and preference_id show what the gate saw and decided, or no such event (the message did not come through this composition — a finding against the deployment’s delivery inventory, not this composition’s records). If the record carried no quiet_hours, the delivery was conformant with respect to stated preferences — and the declared statutory_quiet_window is then the operative question: a 3am SMS is inside any TCPA-conformant declaration, so a delivered SMS envelope at 3am indicts the deployment’s statutory-window declaration (absent, or wrongly scoped) rather than the gate; the event’s evaluation_inputs show whether a statutory exclusion was evaluated. If it carried one and the interpretation places 3am inside it, Invariant 3’s antecedent is examined: either the gate misfired (replay the verdict — Invariant 7 isolates the divergence) or the deployment’s declared clock/timezone capability was unsound (the externally-clearable disclosure names the liable layer). The composition’s records identify which.
  • Breach investigation — suppression-bypass forensics. An incident suggests deliveries bypassed suppression during a window (e.g., a deploy that skipped the gate). The investigator cross-checks, per invocation in the window: the Active subscriber set reconstructed at the fanout.initiated entry’s fired_at (Check 1’s procedure) versus the disposition events under its fanout_id (Invariant 1 — a delivery with no fanout.created event, or a reconstructed subscriber with no disposition past the reconciliation window, is structural evidence of bypass); Notification records created in the window versus fanout.created events (a Notification whose creation has no journal pair indicates writes outside the composition); and replay of suppression verdicts near the window’s edges (Invariant 7) to confirm verdicts matched the stored preferences. The journal’s append-only total order (Event Log Invariants 1–4) bounds the affected invocations.

Generation acceptance

A derived implementation is acceptable when an external auditor, given the four constituent stores, can do all of the following without recourse to source code, runbooks, or developer narration.

Record-clearable checks

  • Check 1 — Reconstruct the trichotomy for any invocation. Given a fanout_id: the fanout.initiated entry yields event_scope, the journaled queried list (the invocation’s true scope), config_version, payload_digest, and fired_at. The disposition events grouped by fanout_id, taking the latest event per principal where redispositions have appended, partition the journaled queried list — every queried subscriber in exactly one of delivered / failed / suppressed (Invariant 1), with a journaled fanout.abandoned record terminally accounting for the principals it covers. As a cross-check on the queried list itself, the auditor reconstructs the Active set at fired_at from the Subscription store’s audit surface (Subscription’s historical filter: subscribed_at ≤ fired_at and (status = active or cancelled_at > fired_at)) and compares: divergence beyond the disclosed clock_tolerance of fired_at is a finding against the implementation’s query-to-journal fidelity; divergence within it is ambiguous-pending-evidence (the Preference Check 4 discipline applied across stores — the Subscription store’s timestamps come from its own clock, the injected now from the host’s). Each fanout.created event’s notification_id resolves via Notification.status_of to a record with matching recipient_ref.
  • Check 2 — Verify frequency-cap safety per delivery. For each fanout.created event whose evaluation_inputs carry interpreted (window, cap) pairs, confirm the recorded in-window count is strictly below each recorded cap — the per-commit bound of Invariant 4, checked against what the gate observed and the record then in effect, never against the principal’s current record (a mid-window cap change is not retroactive). Then, under serialized-per-principal, confirm serialization itself from the observations: walking each principal’s delivery events in journal order, each event’s recorded in-window count equals the count derivable from the preceding delivery events in that window — serialization means every observation matches the committed history at its commit — and each was strictly below its own contemporaneous cap (Invariant 4’s per-commit anchor, already applied above). Under best-effort, any committed overshoot must be matched by a fanout.cap-overshoot-reconciled record whose decided_at falls within the disclosed reconciliation_window of the overshooting commit (Invariant 4’s liveness arm — the detector and record shape are defined there).
  • Check 3 — Confirm every suppression and failure is classified and grounded. Every fanout.suppressed event carries a reason from the closed set {suspended, no-record, quiet-window, frequency-cap, channel-opt-out, unsubscribed} (unsubscribed only on redisposition: true events), a preference_id resolving via Preference.read to a record in effect at decided_at within the disclosed clock_tolerance — or none, exactly when no in-effect record was read: the gate observed status none (the no-record reason, or a no-record deliver path emptied by the statutory exclusion), or the disposition is unsubscribed, whose evaluation_inputs record the audience re-check outcome and the injected now rather than a gate observation (the re-check never reaches the gate); the recorded preference_id is authoritative for what was read, the in-effect reconstruction is the cross-check — plus evaluation_inputs and retry_eligible (the step-6 totality rule fixes it for every reason). Every fanout.create-failed event carries a cause from the closed set {create-failed, preference-unreadable, accounting-unreadable, interpretation-undeclared} (Invariant 5 covers both halves).
  • Check 4 — Trace any fanout_id to its complete disposition set. Every disposition event’s fanout_id resolves to exactly one fanout.initiated entry; no disposition event is orphaned; no two fanout.initiated entries share an id; redisposition events carry redisposition: true and either follow an earlier event for the same (fanout_id, principal_ref) in the journal’s total order or — the crash-gap repair case — name a principal who appears in the invocation’s journaled queried list with no prior disposition event (Invariant 10; the invocation→dispositions referential integrity).
  • Check 5 — Replay any disposition verdict. For a sampled set of fanout.suppressed and fanout.created events, re-evaluate shaping_disposition from the recorded inputs per Replay semantics — both event types carry evaluation_inputs — and confirm the recorded verdict reproduces (Invariant 7). This check consumes one disclosure (the interpretation rules — see externally-clearable below); everything else is records.
  • Check 6 — Confirm payload-content consistency per invocation. For any fanout_id, every created Notification’s envelope content digests (under the disclosed payload_digest_function) to the fanout.initiated entry’s payload_digest — across the original invocation and all redispositions (Invariant 8).

Externally-clearable checks

  • Interpretation and policy disclosures. The deployment disclosed quiet_hours_interpretation, frequency_limit_interpretation, channel_interpretation (with the default shape), no_record_policy, cap_policy, quiet_window_policy, cap_serialization, payload_digest_function, reconciliation_window, invocation_duration_bound, clock_tolerance (both declared components — see the knob), and statutory_quiet_window — declared, or its omission disclosed as the deployment’s legal posture (the knob’s setting rule). The disclosure also covers the Configuration preamble’s version-retention obligation (every config_version’s rules retained for the audit horizon, the ground Checks 2 and 5 replay against) and the declared invocation_duration_bound (Configuration) — the staleness bound on which Invariant 3’s wall-time meaningfulness for late-loop subscribers depends. Without these, disposition events are not uniformly interpretable across deployments.
  • Reconciliation discipline. The deployment’s scheduler invokes the composition’s reconcile_gaps / reconcile_overshoots surfaces (Action wiring) within the declared reconciliation_window; those surfaces — not the deployment’s own code, and not fanout_shaped / redispose — own the writes of the composition-defined fanout.abandoned and fanout.cap-overshoot-reconciled events, so the write authority is capability-provenanced to the composition layer rather than ambient. The liveness arms of the invocation→dispositions relation and Invariant 4 rest on the scheduler actually invoking the surfaces: Check 1 reads the abandonment records and Check 2 reads the overshoot records as the evidence that discharges those arms. A deployment that declares a reconciliation_window but never invokes the reconciliation surfaces leaves those liveness arms undischarged — a standing nonconformance the absence of gap-and-overshoot records makes visible.
  • Journal capability and durability; constituent-store retention. The declared journal_query_capability’s predicates (by type, fanout_id, principal_ref, time range) demonstrably work against the deployed Event Log instance, and the journal’s store is provisioned durably for the deployment’s audit horizon — the obligations every rebuild, check, and Invariant 10’s journaling claim consume. The Subscription and Preference stores are likewise retained for the same horizon (each atom routes retention lifetime to Retention Window as a deployment choice; the audit cross-checks here read cancelled subscriptions and Deleted preference records across that horizon, so the deployment’s retention choice must cover it).
  • Single-pipeline routing. Every delivery subject to the deployment’s quiet-window and frequency obligations is routed through this composition’s gate (fanout_shaped / redispose) — not through the unshaped sibling, not through direct Notification.create. The composition’s invariants account only for what flows through the gate; the deployment attests that nothing regulated flows around it (see Edge cases — The gate governs only what flows through it).
  • Recipient-local clock capability. The host’s injected now and the timezone resolution behind quiet_hours_interpretation are sound for recipient locales — Invariant 3’s antecedent. The records show what the gate evaluated; whether the clock told the truth about recipient-local time is the deployment’s attestation.
  • Unified principal namespace. Subscription’s subscriber_ref and Preference’s principal_ref denote the same person under byte-exact equality — the namespace obligation from Primitive policies. The records alone cannot prove two opaque references co-refer; the deployment attests the canonicalization discipline.
  • Payload retention for recovery. The deployment retains payload_content keyed by fanout_id for its retry horizon (Edge cases — Payload data in the journal), since redispose requires the digest-matching content and the journal carries only the digest.
  • Peer-pattern wiring. Whether Consent (legal permission, sequenced before invocation), Audit Trail (attribution and sealing), and Duplicate Prevention (at-most-once fanout under retry) are wired, and with what configuration.

Edge cases and explicit non-goals

  • Legal permission is sequenced before this composition. Whether the system may communicate with the principal at all — consent under GDPR (the EU General Data Protection Regulation), prior express consent under the TCPA, opt-in regimes generally — is the Consent pattern’s question, evaluated by the deployment before fanout_shaped is invoked (or as a gate wrapping it). A deliver verdict here is not legal permission; Preference’s own spec states a preference record never overrides the legal-permission answer. A deployment that invokes this composition without sequencing its consent gate has made a sequencing error at the composing layer, not a conformance error here. Consent & Preference Management (C2) is the peer that operationalizes the consent side; its Edge cases name this boundary from the other shore.
  • Holding is a classification, not a queue. cap_policy = hold and quiet_window_policy = hold mark suppression events retry-eligible; they do not defer, schedule, or queue anything. The composition is a stateless interpreter of its invocation graph — a deferred-delivery queue would be persistent truth no constituent owns. The deployment’s scheduler reads retry-eligible suppressions from the journal and re-invokes; each re-invocation is a fresh fanout with fresh gate evaluation (the window may have moved, the record may have changed). A first-class deferred-delivery surface would be its own pattern (a scheduling atom), deliberately not absorbed here.
  • Preference staleness within an invocation. The gate evaluates the record returned by current_for at disposition-evaluation time. A suspend or superseding set committing after the gate’s read but before the create commits does not re-render the verdict — exactly as Notification Fanout commits to the subscriber set at query time, and exactly the queue-time-capture posture Preference’s own Intent states (“the atom does not push updates into already-queued work” — its Behavior carries the same commitment in its updates-are-not-retroactive bullet). The staleness window is one subscriber’s step-5-to-step-6 span. Deployments for which a mid-fanout suspension must win re-check current_for at the transport layer before dispatch — a transport-layer policy, outside this composition. The acceptance of staleness is deliberately asymmetric across the two actions: the original loop’s window is bounded by one invocation’s duration, so query-time audience and read-time preferences are accepted as-is; redispose’s horizon is unbounded (a hold retry may fire days later), so it re-verifies both the audience (Subscription.subscribed — a cancel between invocation and retry yields the journaled unsubscribed suppression, never a delivery to someone who left) and the shape (a fresh current_for read), per its step 4.
  • Crash and journal-gap reconciliation. A crash mid-loop leaves a fanout.initiated entry with fewer disposition events than the reconstructed subscriber set — detectable by Check 1, surfaced as the invocation→dispositions relation’s liveness case. The composition’s reconcile_gaps(fanout_id, payload_content) surface (Action wiring) — which the deployment’s scheduler invokes within the declared reconciliation_window — resolves each gap subscriber: a Notification record without a journal pair cannot exist (the truth-bearing pair commits atomically per subscriber), so a gap subscriber received nothing — reconcile_gaps calls redispose(fanout_id, principal_ref, payload_content) for each (the named crash-gap exception in redispose step 3: a subscriber in the reconstructed queried set with no disposition event is retryable), and for any residue it cannot resolve — a principal whose membership stays ambiguous within the clock_tolerance, or an invocation the deployment chooses not to repair — appends {type: "fanout.abandoned", fanout_id, principals | all, reason, decided_at} (scoped to named principals or the whole invocation), the terminal record that discharges the invocation→dispositions liveness arm for the covered principals — redispose step 3 rejects any principal an abandonment record covers, so the terminal is enforced, not advisory: the at-quiescence claim is then satisfied by the abandonment record, visibly and accountably, rather than by a disposition per subscriber. Check 1’s partition is evaluated modulo this terminal — an abandoned invocation’s gap subscribers are accounted for by the abandonment record, and membership ambiguity within the tolerance is ambiguous-pending-evidence, not a clean failure. Re-invoking fanout_shaped wholesale after a crash without Duplicate Prevention can double-deliver to subscribers whose pairs committed; redispose, which rejects already-delivered principals, is the per-principal path that cannot.
  • The gate governs only what flows through it — the single-pipeline obligation. The quiet-window and frequency-cap invariants account for deliveries this composition commits; they say nothing about a message the deployment sends through the unshaped sibling, a direct Notification.create, or any channel outside the gate. A deployment under TCPA-class obligations must route every delivery those obligations cover through fanout_shaped / redispose — the same single-gate discipline C2 imposes on the consent side, where processing systems must consume its processing_permitted gate rather than reading Consent directly. The obligation cannot be proven from this composition’s records (they show what came through, not what went around); it is attested in the externally-clearable disclosures, and the breach-forensics scenario’s Notification-without-journal-pair cross-check is the records-side detector for violations of it.
  • Frequency-cap window boundary semantics. Whether a window is rolling or calendar-aligned, which timezone anchors alignment, and whether the cap counts created records (this composition’s accounting unit — the create is the send decision) or downstream delivered outcomes is fixed by frequency_limit_interpretation. This composition counts committed delivery dispositions (fanout.created events); a deployment whose regulatory cap counts transport outcomes reconciles at the transport layer.
  • Per-channel frequency caps. The interpreted cap is per principal. A deployment whose frequency_limit vocabulary encodes per-channel caps expresses them inside frequency_limit_interpretation (the interpreted (window, cap) pairs may be channel-scoped); the invariant’s accounting then partitions by channel using the channels element each delivery_count_index entry and fanout.created event already carries. The spec’s stated invariant is the principal-level bound; channel-scoped refinements are interpretation, disclosed like the rest.
  • Transport — including the re-send path after transport failure. This composition creates shaped Notification records; it does not dispatch them. The transport layer reads Notification.pending_for, honors the envelope’s channels and format, and calls deliver / fail / expire. The composition can only advise the transport — the envelope is data, not enforcement — so the deployment carries the obligation that makes it binding on its transport layer, disclosed with its peer-pattern wiring. When transport ends in failed or expired, Notification’s own retry model is a new create per attempt — and under the single-pipeline obligation that new create routes through redispose (its step-3 transport-failure path admits a principal whose prior record reached transport failure), so the re-send re-passes the gate: a quiet window that has opened since the original send blocks it, and the cap counts the original create. A transport layer that retries by calling Notification.create directly has produced the bypass artifact breach forensics flags.
  • Scope hierarchy, wildcards, delivery ordering, payload size. Inherited unchanged from Notification Fanout’s non-goals: exact-match scopes; unordered creates; payload bounds belong to the composing layer.
  • Fan-out at scale. Inherited from Notification Fanout and sharpened by this composition’s own choices: the per-subscriber work (one current_for read, one gate evaluation, one truth-bearing pair) parallelizes freely — Invariants 1 and 5 constrain coverage, not execution strategy — but three surfaces scale with the subscriber count and are the deployment’s to size: the fanout.initiated entry journals the full queried list in one payload-capped append (size the Event Log instance’s cap to the maximum fanout scope, per Primitive policies); the single injected now’s staleness grows with loop length (the disclosed invocation-duration bound); and per-principal cap evaluation under serialized-per-principal serializes only per principal, never across the loop.
  • Payload data in the journal — and the retention obligation the digest creates. fanout.initiated journals a digest of the content, not the content — the Event Log’s payload cap (default 64 KB) and the privacy posture both argue against duplicating message content into the journal. The Notification records carry the content; the digest binds journal to records. The digest-only journal creates a named deployment obligation: every recovery path runs through redispose, which requires the original payload_content and verifies it against the digest — but an invocation whose dispositions were all suppressed or all failed (or that crashed before any create) leaves no in-system copy of the content. The deployment must therefore retain payload_content, keyed by fanout_id, for at least its retry horizon (no shorter than the reconciliation_window, and as long as any hold-marked suppression remains schedulable) — disclosed with the peer-pattern wiring. Deployments whose audit regime requires content in the sealed journal compose Audit Trail with an explicit decision about content duplication.
  • Authorization to fanout and to redispose. Not enforced here; any caller may invoke fanout_shaped, and any caller may invoke redispose for any journaled invocation. redispose does require the digest-matching payload_content — a capability-shaped bar (only a holder of the original content can re-dispose) — but that is integrity binding, not authorization. Permissions gates both actions at the composing layer; Actor Identity or Audit Trail attributes the initiator where required — and redispose, a delivery-causing action with an unbounded horizon, is the surface attribution-required deployments should gate first.
  • Cross-store consistency under partial failure. Four stores are touched per invocation. The per-subscriber truth-bearing pair (create + journal) is atomic per the Contract’s multi-write obligation; everything across subscribers is independent by design (no rollback across parallel branches); the indexes are outside every atomicity surface. Two host serialization obligations are conformance requirements, not knobs, and they serialize on two distinct axes that both bind a redispose deliver at once. Per-(fanout_id, principal_ref) (redispose step 3 — the obligation Invariant 9’s forward bound rests on): this is also what closes the read-then-create window between Notification.status_of returning failed/expired and the new create committing, so two concurrent redisposes cannot both adjudicate the same record retryable and both deliver. Per-principal (the cap path, posture set by cap_serialization): this governs every cap evaluation for a principal, fanout_shaped and redispose alike. The two axes are independent — the live-record bound and the cap bound — and a redispose deliver serializes on both. The residual partial states — journaled invocation with incomplete dispositions — are enumerated under crash reconciliation above.
  • Clock semantics. The invocation’s now is host-injected at the seam (Logic Confinement), once per invocation: fired_at and every disposition event’s decided_at carry the same injected instant, so all of an invocation’s gate evaluations share one clock reading and replay needs no per-subscriber time reconstruction. The cost is bounded staleness within one invocation — a fanout whose loop runs long evaluates its last subscriber’s quiet window against a now read at invocation start; the deployment bounds invocation duration via the declared invocation_duration_bound knob (Configuration) accordingly (a transport-layer re-check before dispatch is the mitigation for deployments where the bound is material, as with the suspension-staleness case above). Recipient-local resolution is quiet_hours_interpretation’s job under the declared clock capability; skew between the injected now and recipient wall clocks is the capability’s risk surface, named in Invariant 3’s antecedent and the externally-clearable disclosure.

Standards references

  • TCPA — Telephone Consumer Protection Act (47 U.S.C. §227) and 47 CFR §64.1200(c)(1) — restricts calls and texts outside permitted hours (the Federal Communications Commission’s implementing rule presumes no solicitations before 8am or after 9pm recipient-local time) and underwrites per-message statutory damages. Invariant 3 (quiet-window safety) is the structural mechanism: the gate evaluates the recipient’s stored quiet window in recipient-local time before any create, and the suppression record is the affirmative evidence of compliance. The statutory 8am–9pm presumption applies whether or not the principal stored a window, which is why the gate carries the statutory_quiet_window arm (Configuration) alongside the stored-preference arm — Invariant 3 covers both. Three obligations this composition names but does not itself discharge: the recipient-local clock capability, the prior-express-consent requirement (the Consent peer’s), and the single-pipeline routing obligation — the invariants cover only deliveries routed through the gate, so the deployment must route every TCPA-covered delivery through it and must declare the statutory window for the covered channels (Edge cases; externally-clearable disclosures).
  • CAN-SPAM Act (15 U.S.C. §7701 et seq., esp. §7704) — commercial email senders must honor opt-outs. The channel-opt-out suppression (an email: "opt-out" channel preference yielding an empty deliverable set or excluding the channel) is the enforcement point; Preference’s immutable records plus the suppression journal produce the audit trail CAN-SPAM enforcement requires. The 10-business-day honoring window is a deployment obligation on preference-change-to-enforcement latency, met structurally here because the gate reads the in-effect record at every disposition.
  • GDPR Article 7(3) — withdrawal of consent must be as easy as giving it. The Article 7(3) obligation proper belongs to the Consent atom and the C2 composition (Preference’s own Standards references draw this line). This composition’s contribution is the enforcement-latency half of the ease story: a withdrawal or preference change recorded upstream is honored at the next disposition evaluation — there is no cached permission to invalidate, because the gate reads current_for fresh per invocation.
  • GDPR Article 21(2) — the right to object to direct-marketing processing. Per Preference’s Standards references, the objection’s delivery-shaping signal lives in the preference record and the legal-permission revocation in Consent; this composition is the named enforcement point for the first half — the recorded opt-out becomes a suppression at the next evaluation.
  • CASL (Canadian Anti-Spam Legislation) and the ePrivacy Directive (2002/58/EC) — inherited through Preference’s standards surface; the same gate is the enforcement point.

It inherits from: Subscription (Observer, pub-sub, WebSub), Notification (SMTP — Simple Mail Transfer Protocol — disposition mapping, webhooks, the Apple and Google push services), Preference (CAN-SPAM, TCPA, GDPR 7(3)/21(2), CASL, ePrivacy), Event Log (append-only journaling), and the Outbox pattern via Notification Fanout’s framing — the shaped Notification records are the outbox the transport consumes.


Status

grounded on Final Critique 12 — 2026-06-12 — authored 2026-06-11 against the adjudicated kickoff plan; foundation round plus nine fresh-reader Final Critique rounds (FC4: 22 findings, 6 foundational; FC5: 14, 2; FC6: 13, 3; FC7: 14, 3; FC8: 10, 2; FC9: 13, 0 foundational — the first grounding gate). A post-grounding fresh-reader rescan reopened the gate: FC10 (6 findings, 1 foundational — a gate fail-closed/precedence ordering defect, OG-1); FC11 (10 findings, a foundational reconciliation-surface provenance cluster — resolved by introducing the composition-introduced reconcile_gaps / reconcile_overshoots surface, taking the action surface to three); FC12 (5 findings, 0 foundational — the re-grounding gate). All findings closed in-pattern every round, Pass 2 clean from FC8 on; formal-layer vote YES and discharged (TLA+ cap-TOCTOU model + overshoot buggy twin verified in tools/harness/, bound saturation recorded; the FC10–FC12 changes do not touch the modeled race — see the §Formal model coverage note); regulated overlay carried in full. The eighteenth and final C-numbered composition.


Lineage notes

Drafted as the composition that retires the final unstarted C-numbered roadmap row (C11), unblocked 2026-05-29 when Preference grounded. The constituent-set decision — compose Subscription + Notification + Preference + Event Log directly; do not compose Notification Fanout — was adjudicated in the 2026-06-11 planning session on the C9 precedent (Reservation Lifecycle re-wires Idempotent Reservation’s atoms rather than wrapping the grounded composition, because the smaller composition’s action surface exposes no insertion point). Notification Fanout’s fanout has no per-subscriber hook between query and create; the gate must sit exactly there. Audit Trail and Consent were adjudicated peers, not constituents: attribution is optional to the mechanism, and legal permission is sequenced upstream by Preference’s own boundary declaration.

Structural milestone. Retires the forthcoming-link debts standing against C11: the C11 marker in Preference’s Composition notes and the marker on C2’s Preference-Aware Notification Fanout boundary note in Consent & Preference Management — both now resolved to links to this composition. Corrects the roadmap’s C11 row, which listed Notification Fanout as a constituent.

Conventions inherited from prior work. Regulated adversarial scenarios and Generation acceptance (split into record-clearable / externally-clearable) inherited from the methodology directly (pressure-testing.md §Regulated-pattern conventions); the structural-relation templates from spec-format.md §Cross-cutting authoring conventions; the composition-state classification discipline from execution-contract.md §Composition state. Authored under the 2026-06-11 vocabulary direction (lint rules J and K) from the first line.

Pass 2 adjudication — candidate concept “Delivery Frequency Accounting” (flagged by the kickoff plan; adjudicated at drafting). The frequency-cap accounting — count a principal’s deliveries per window, render a verdict — is concept-shaped: it recurs across domains (API throttling, login-attempt caps, spend limits), which the four extraction questions take seriously. Run honestly: (1) recurs across domains? Yes. (2) own state machine? No — the verdict is a pure function over the delivery history (Event Log events) and the injected clock; the “counter” has no lifecycle, no transitions, no states of its own; the history it reads is Event Log’s store, governed by Event Log’s invariants. (3) specifiable without it, composed in? The evaluation is one rule inside shaping_disposition; there is no freestanding surface to compose. (4) would another pattern reinvent it? Another pattern would reinvent the counting query, which is one Event Log read — not a concept’s worth of reinvention. Gate 3 of the composition-layer extraction gate settles it: the recurrence introduces no state the constituents do not already carry — the deliveries live in Event Log, the limit lives in Preference, the verdict is derived — so it names wiring, not a freestanding concept. The Execution Contract’s materialized-projection tiebreaker confirms the boundary and names the re-opening condition: the moment a deployment requires the cap bound to be unconditional without host serialization — i.e., the counter must be transactionally consistent with the deliveries it counts — the counter is the materialized-projection atom (a true Rate Limiter, with its own store, identity, and invariants), not an index. C11 deliberately does not require that: Invariant 4 is conditional on the declared cap_serialization capability, so the count stays a derived index and the extraction is not warranted now. Recorded as considered-and-declined with the falsifiable re-opening condition.

Formal-layer vote — 2026-06-11: YES. Invariant 4’s frequency-cap bound under concurrent invocations is a genuine action-vs-action race: two fanout_shaped invocations evaluating the same principal can both observe count = cap − 1 and both commit — the same time-of-check-to-time-of-use class as capacity-constraint-enforcement.tla’s overshoot twin. A derived TLA+ model verifies the serialized form holds and the split observe/commit form overshoots. The suspend-vs-fanout race (a suspend landing between the gate’s current_for read and the create’s commit) is deliberately not modeled, with the named reason: Invariant 2 is stated at disposition-evaluation time (the queue-time-capture posture Preference’s own Behavior blesses), so the interleaving violates no stated invariant — there is no property for a model to check, only a staleness window the spec names in Edge cases.

Round 1 — foundation (Pass 1 → 2 → 3, author-led, 2026-06-11). Pass 1 walked the nine GRID nodes, the reference graph (every Rests on: clause resolved against the constituent specs’ invariant lists as written — Subscription’s nine, Notification’s nine, Preference’s ten plus Temporal property 11, Event Log’s seven), and the accessibility checks. Pass 2 ran the extraction questions — the flagged Delivery Frequency Accounting adjudication is recorded above; the hold-marking (scheduling) and interpretation-rule surfaces were checked for over-absorption and confirmed in-pattern (holding is a classification on the record, never a queue; interpretation is deployment configuration the constituent itself delegates). Pass 3 ran the adversarial question set and the three postures. Seven findings, all closed in-pattern:

  • F1 — subsection order deviation — refining → the load-bearing wiring decision subsection preceded Action wiring, against spec-format.md’s required order; reordered to Composition state → Configuration → Primitive policies → Action wiring → Replay semantics → load-bearing decision.
  • F2 — SMS unglossed — refining → first prose use of SMS in the disputed-delivery scenario now spells out Short Message Service.
  • F3 — FCC and push-service acronyms unglossed — refining → “the FCC rule” rewritten as the Federal Communications Commission’s implementing rule; the inherited push-service initialisms replaced with plain words.
  • F4 — decided_at provenance unstated — foundational → the spec nowhere said whose clock stamps the disposition events; the Clock semantics edge case now pins one host-injected now per invocation (fired_at and every decided_at carry the same instant), names the bounded within-invocation staleness this buys, and names the transport-layer re-check as the mitigation where the bound is material.
  • F5 — empty default shape produces a lying suppression reason — refining → an empty declared default channel set would route every no-record deliver verdict into suppress(channel-opt-out) with no opt-out on record; Configuration now requires the default shape’s channel set be non-empty and points the suppress-everything posture at no_record_policy = suppress.
  • F6 — gate input mis-typed as a single count — refiningshaping_disposition was signed with recent_delivery_count while frequency_limit_interpretation may yield several (window, cap) pairs; the input is the timestamp set (the index entry), per-pair counts computed inside the pure function; signature, step 5, and evaluation_inputs aligned.
  • F7 — channel-opt-out and suppress-mode no-record had no worked example — refining → marketing-newsletter example added exercising both under CAN-SPAM, including the they-said-no versus they-never-said distinction the records preserve.

Pass 1 / Pass 2 rerun after the fixes: clean — the reorder and glosses introduce no new references; the new example uses only declared configuration values; no new extraction candidate opened.

Final Critique 4 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-11 (AI-conducted round; claude-fable-5, fresh context: pass question sets, the spec body with Status and Lineage withheld, the constituent specs). The reviewer first re-verified every cross-reference head-on against the constituents (invariant counts, names, signatures, rejection vocabularies — all confirmed), then surfaced 22 findings: 6 foundational, 13 refining, 3 rhetorical. The gate did not close clean; all 22 were closed in-pattern in the same session, and the foundational six reshaped the action surface — the round earned its keep. Findings and fixes:

  • FC-7 — journal query capability undeclared — foundational → every rebuild, check, and adversarial query needed payload-predicate queries Event Log’s spec leaves to implementation policy (the C6/FC5-1 capability-provenance class). Fixed: journal_query_capability declared as a named Configuration capability; Composition state, Invariant 4’s Rests on, and a new externally-clearable check all route through it.
  • FC-11 — Preference-store failure unhandled on the load-bearing read — foundational → a failed current_for read was indistinguishable from none, so an outage delivered unshaped to suspended principals. Fixed: step 5 fails closed — failed(preference-unreadable) (and accounting-unreadable for a failed cold-start rebuild); an unreadable shaping input never produces a deliver verdict.
  • FC-12 — no journaled per-principal retry; the recommended recovery was indistinguishable from bypass — foundational → direct Notification.create retries skipped the gate and produced the exact unjournaled artifact breach forensics flags. Fixed: redispose(fanout_id, principal_ref, payload_content) added — gate-re-evaluated, digest-bound, journaled under the original fanout_id, rejecting already-delivered principals; every recovery path (failed retry, hold scheduler, crash gaps) now routes through it.
  • FC-13 — gate verdict undefined under an undeclared interpretation — foundational → a record carrying quiet_hours in a deployment that never declared the interpretation had no specified outcome. Fixed: fail-closed rule in the Configuration preamble and step 5 — failed(interpretation-undeclared), never a silent deliver, never a fabricated suppression reason.
  • FC-14 — single-pipeline routing obligation unnamed — foundational → deliveries around the gate (unshaped sibling, direct create) defeated the cap and quiet-window claims with no named obligation. Fixed: the single-pipeline obligation named as an Edge case (on the C2 processing-systems-must-use-the-gate precedent), an externally-clearable disclosure, and a third named-not-discharged obligation in the TCPA standards entry.
  • FC-15 — delivered events lacked the replay inputs Replay semantics claimed — foundational → deliver verdicts were unreplayable and best-effort overshoots undiagnosable. Fixed: fanout.delivered events now carry evaluation_inputs (observed in-window counts per interpreted pair); Replay semantics, Invariant 7, and Checks 2/5 updated to consume them.
  • FC-1 — consumed knobs missing from Configuration — refiningpayload_digest_function and reconciliation_window added as knobs. FC-2 — fanout_dispositions removal clause missing — refining → eviction-at-deployment-discretion clause added (every fact regenerates). FC-3 — unresolvable body references to “the formal model” — refining → rephrased to name the formal layer with a Lineage §Formal model pointer. FC-4 — phantom subscriber-accounting field in the breach scenario — refining → scenario now reconstructs the Active set at fired_at per Check 1. FC-5 — Invariant 8 had no acceptance check — refining → Check 6 added (digest-anchored content consistency). FC-8 — Event Log Invariant 6 mis-cited for fanout_id uniqueness — refining → Invariant 10’s Rests on corrected to the composition-introduced generation plus Event Log Invariants 1–2. FC-9 — journal durability overstated — refining → Invariant 10 renamed to “unique and journaled”; store durability named as the deployment’s provisioning obligation, routed externally. FC-16 — cap bound not anchored to the contemporaneous record — refining → Invariant 4 restated per-commit against the record observed at each disposition; Check 2 rewritten accordingly (a mid-window cap-lowering set is not a retroactive violation). FC-17 — single injected now vs. later constituent reads unreconciled — refining → cross-store clock tolerance added to the disclosures; Checks 1 and 3 evaluate within it, with the recorded preference_id authoritative and boundary cases ambiguous-pending-evidence. FC-18 — retry_eligible undefined for three reasons — refining → totality rule in step 6: suspended / no-record / channel-opt-out are always false (they relent by principal action, not time). FC-19 — Invariant 3 lacked Invariant 2’s observation anchor — refining → “as observed by the gate at disposition evaluation” propagated. FC-20 — cap example arithmetic wrong under its natural reading — refining → rewritten with explicit Monday/Tuesday timestamps. FC-21 — format default for a channels-without-format record unpinned — refiningchannel_interpretation now supplies the absent-dimension default.
  • FC-6 — CFR / U.S.C. unglossed — rhetorical → both spelled out at first use. FC-10 — quote attributed to Preference’s Behavior instead of Intent — rhetorical → attribution corrected. FC-22 — log-unavailable mislabeled the definitive invalid-payload case — rhetorical → renamed journal-rejected, with the retryable-vs-definitive distinction stated where the rejection is defined.

Pass 1 / Pass 2 rerun after the fixes: clean — the new action, causes, knobs, and check are referenced in both directions (signature ↔ steps ↔ invariants ↔ checks ↔ disclosures); redispose is an emergent action in the expected sense (no constituent owns a gate-re-evaluated retry), not an over-absorption; no new extraction candidate opened (the retryability rule reads the journal; it carries no state).

Final Critique 5 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-11 (AI-conducted round; claude-fable-5, fresh context: pass question sets, the spec body with Status and Lineage withheld, the constituent specs). The reviewer re-verified the full cross-reference surface against the constituents (all counts, names, signatures, rejection vocabularies confirmed accurate) and surfaced 14 findings: 2 foundational, 9 refining, 3 rhetorical — the foundational pair both attacking the surfaces Final Critique 4’s fixes had introduced, which is the refinement loop working as designed. All closed in-pattern (one rhetorical accepted with rationale):

  • FC5-6 — replay of status-dependent verdicts unsupported — foundational → the gate’s rule 1 turns on status, the one field Preference Invariant 1 leaves mutable, so Preference.read at replay time cannot recover what the gate saw. Fixed: evaluation_inputs on both event types now record the observed record status; Replay semantics rewritten around the immutable-from-the-record / mutable-from-the-event split; Invariant 7’s Rests on corrected to value-field immutability.
  • FC5-7 — redispose check-then-act race unserialized — foundational → two concurrent redisposes could both observe a retryable latest event and double-deliver under an invariant stated unconditionally. Fixed: step 3 now names the per-(fanout_id, principal_ref) serialization as a host conformance requirement (the Preference per-principal precedent); Invariant 9 cites it; the cross-store edge case enumerates it.
  • FC5-1 — redispose journal-rejected unwired — refining → steps 2 and 4 now produce it (failed journal read; rejected disposition append, nothing committed). FC5-2 — clock capabilities cited to the wrong home — refiningclock_tolerance promoted to a Configuration knob; Invariant 3’s citation corrected (the recipient-local capability is an attestation, declared in the externally-clearable checks). FC5-5 — query capability’s structural home unnamed — refining → the knob now names Event Log’s Reverse Index pattern (forthcoming) as the eventual constituent home. FC5-8 — replay-input type mismatch — refining → the gate’s compute-counts ∘ verdict factoring stated; replay re-enters at the verdict factor with recorded counts. FC5-9 — fail-closed missed the undeclared default shape — refining → the preamble rule now covers the no-record deliver path. FC5-10 — reconciliation_window optionality contradicted its consumers — refining → required for every deployment. FC5-11 — crash-gap admission unspecified — refining → step 3 now reconstructs per Check 1 and fails closed on tolerance-ambiguous membership. FC5-12 — cross-invocation accounting clock unstated — refining → Invariant 4 names the non-decreasing-to-within-clock_tolerance assumption and its deflation failure mode. FC5-13 — example gaps on the novel mechanisms — refining → fail-closed example added; not-known / payload-mismatch rejections exemplified; walkthrough configuration completed.
  • FC5-4 — non-verbatim C2 quotation — rhetorical → paraphrased to C2’s actual gate discipline. FC5-14 — Summary “nothing is ever silently dropped” overclaim — rhetorical → restated with the crash-detection qualifier in plain language. FC5-3 — “Record-clearable checks” vs. the overlay’s “Audit-Trail-traversal-clearable checks” heading — rhetorical, accepted with rationale → this composition carries no Audit Trail substrate, so the overlay’s traversal name would claim a surface that is not wired; the sibling Notification Fanout’s “Record-clearable checks” is the established heading for the substrate-less case, and the load-bearing structure (the two-way split itself) is intact.

Pass 1 / Pass 2 rerun after the fixes: clean — the observed-status field, the serialization requirement, and the two new knobs are referenced from their consumers; no new extraction candidate (the serialization requirement is a host obligation, not state).

Final Critique 6 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-11 (AI-conducted round; claude-fable-5, fresh context as before). The reviewer confirmed the full cross-reference surface accurate and surfaced 13 findings: 3 foundational, 9 refining, 1 rhetorical. All closed in-pattern (one refining accepted with rationale):

  • FC6-3 — gate not total for a record without channel_preferences — foundational → a frequency_limit-only record reached rule v with no defined verdict. Fixed: rule v and the channel_interpretation knob now apply the declared default shape wherever the deliver path has no stated channels (no-record deliver path and channel-less records).
  • FC6-4 — payload-retention obligation unnamed — foundational → every recovery path needs the digest-matching payload_content, but an all-suppressed or zero-commit invocation leaves no in-system copy. Fixed: the retention obligation (keyed by fanout_id, held for the retry horizon) named in the payload edge case and the externally-clearable disclosures.
  • FC6-5 — statutory quiet windows owned by no one — foundational → the gate fired only on stored quiet_hours, while the TCPA’s 8am–9pm presumption is not preference-conditional. Fixed: statutory_quiet_window Configuration knob (window + channel set, evaluated for every disposition); rule v excludes statutory-blocked channels and suppresses as quiet-window when the exclusion empties the set; Invariant 3 gains the statutory arm; the TCPA standards entry and the disputed-delivery scenario rewritten accordingly.
  • FC6-2 — Subscription equality declaration overstated — refining → the byte-exact rule restated as this composition’s own declared policy, conservative against both constituents. FC6-6 — channel-scoped caps unimplementable from the declared inputs — refiningdelivery_count_index entries and fanout.delivered events now carry (decided_at, channels); the gate’s history input and rule iv updated. FC6-7 — redispose step 4 conflicted with “exactly as there” — refining → the journal-rejection deviation is now named as step 4’s own rule. FC6-8 — fail-closed “field” unscoped — refining → scoped to the four interpreted preference fields; metadata excluded explicitly. FC6-9 — false analogy defending journal-rejected — refining → defense rewritten on its real ground (one invocation-level taxonomy; the distinction available by retry or Event Log telemetry). FC6-10 — “exactly once per invocation” contradicted by redispose — refining → Intent and Mechanism now say once per disposition. FC6-11 — redispose lacked a fanout_id primitive policy — refining → step 1 validates it. FC6-12 — ambiguous crash-gap subscriber had no terminal — refiningfanout.abandoned added as the journaled terminal discharging the liveness arm; Check 1 evaluated modulo it. FC6-1 — “Record-clearable checks” vs. the overlay’s traversal-named container — refining, accepted with rationale → this composition carries no Audit Trail substrate, so the traversal name would claim an unwired surface; the substrate-less sibling (Notification Fanout, grounded) is the heading precedent; flagged below as a library-wide container-naming question for spec-format rather than a per-pattern fix.
  • FC6-13 — body cited its withheld Lineage as authority — rhetorical → Composition state now stands on the rebuild procedures alone.

Pass 1 / Pass 2 rerun after the fixes: clean. Library-wide item surfaced: spec-format’s Generation-acceptance split names its first container “Audit-Trail-traversal-clearable checks,” which mis-fits compositions with no Audit Trail substrate (Notification Fanout and now C11 both use “Record-clearable checks”); the container-naming rule deserves a substrate-less variant in spec-format’s next round.

Final Critique 7 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-11 (AI-conducted round; claude-fable-5, fresh context as before; the FC6-1 accepted-deviation noted to the reviewer as settled). 14 findings: 3 foundational, 9 refining, 2 rhetorical. All closed in-pattern. The headline fix — journaling the queried list — closed one foundational and two refining findings at once:

  • FC7-3 — crash-gap path read Subscription’s audit surface at runtime — foundationalredispose’s reconstruction violated the atom interface contract (Subscription’s historical filter lives on its audit surface, not its runtime Q surface) and contradicted Composes and Invariant 6. Fixed: fanout.initiated now journals the exact queried list; runtime retryability reads the journal only; the Subscription-store historical reconstruction moves to Check 1 as an auditor-side cross-check, where the audit surface is the legitimate instrument.
  • FC7-5 — replay had no configuration-contemporaneity rule — foundational → a legitimate interpretation change between disposition and replay produced divergence the trichotomy misattributed. Fixed: the interpretation configuration is versioned-never-mutated (Configuration preamble, on Preference’s instance-configuration-record precedent); config_version journaled per invocation; Replay semantics, Check 2, and the walkthrough evaluate under the journaled version.
  • FC7-6 — overshoot “reconciliation evidence” demanded but undefined — foundational → the cap liveness arm had no detector, actor, or record. Fixed: Invariant 4 now defines the detector (scan delivery events per principal under each journaled version’s windows) and the record (fanout.cap-overshoot-reconciled), run by the same reconciliation as journal gaps; Check 2 consumes the record.
  • FC7-1 — SMS glossed late — refining → glossed at its new first use (the statutory knob). FC7-2 — gate definition referenced one-directionally — refining → step 5 now names §The load-bearing wiring decision (Mechanism). FC7-4 — constituent-store retention unpropagated — refining → the durability disclosure now covers Subscription and Preference store retention across the audit horizon. FC7-7 — fired_at-vs-query gap uncovered by clock_tolerance — refining → resolved structurally by the journaled queried list (no runtime reconstruction); the cross-check tolerance language survives only on the auditor side. FC7-8 — walkthrough omitted the statutory window its channels require — refining → walkthrough declares it (and the knob’s requirement collapsed to one rule: required where deliveries on a statutorily restricted channel fall within the restriction’s scope). FC7-9 — abandonment terminality unenforced — refiningfanout.abandoned gained a principal scope; redispose rejects covered principals. FC7-10 — reconciliation_window had no setting rule — refining → rule added (no longer than the audit regime’s reporting period; a never-binding window is a vacuous declaration). FC7-11 — serialized arm vs. the index’s no-consistency posture — refiningserialized-per-principal now explicitly obligates rebuild-before-evaluation. FC7-12 — failure half of Invariant 5 unchecked — refining → Check 3 extended to the closed cause set.
  • FC7-13 — retry-eligibility rationale invited the wrong inference — rhetorical → terminal-for-this-invocation stated outright. FC7-14 — fanout.delivered named a create-commit with a transport word — rhetorical → events renamed fanout.created / fanout.create-failed, ending the collision with Notification’s transport-level Delivered state.

Pass 1 / Pass 2 rerun after the fixes: clean — the queried and config_version fields, the overshoot record, and the renames are referenced from their consumers; the versioned-configuration discipline is deployment-owned vocabulary (Preference’s precedent), not a new concept to extract.

Final Critique 8 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-12 (AI-conducted round; claude-fable-5, fresh context as before; the FC6-1 accepted-deviation noted as settled). Pass 2 returned fully clean for the first time. 10 findings: 2 foundational, 7 refining, 1 rhetorical — both foundational attacking the retry surface FC5–FC7 had built. All closed in-pattern:

  • FC8-4 — transport-failure retry structurally foreclosed — foundational → Notification’s retry model (a new create per attempt) had no conformant per-principal path: direct creates are branded bypass, and redispose rejected any already-created principal. Fixed: redispose step 3 gained the transport-failure path — a fanout.created principal is retryable when Notification.status_of shows the record reached failed or expired — so the re-send re-passes the gate under current rules; Invariant 9 restated as at-most-one-live (Pending or Delivered) record with the rule’s Rests on citing Notification Invariants 2–4; the Transport edge case names the path and the bypass consequence; examples exercise it.
  • FC8-5 — redispose never re-checked Subscription across its unbounded horizon — foundational → a principal who cancelled between the invocation and a days-later hold retry was still delivered to. Fixed: redispose step 4 re-checks Subscription.subscribed (the declared point query) before the gate, committing a journaled suppress(unsubscribed) on a cancelled audience member; the staleness edge case now defends the asymmetry (bounded intra-invocation window accepted; unbounded redispose horizon re-verified); the closed reason set, retry_eligible totality, Check 3, Composes, and Invariant 6 all extended.
  • FC8-1 — Primitive policies missed redispose’s inputs — refiningfanout_id bullet added; preamble covers both actions. FC8-2 — disclosure list incomplete against Configuration — refiningstatutory_quiet_window (or its omission-as-posture) and the version-retention obligation added. FC8-3 — statutory-emptied no-record suppression contradicted Check 3’s preference_id rule — refining → the rule restated principle-first: none exactly when the gate observed no in-effect record (or never reached the gate, on unsubscribed). FC8-6 — Check 2’s serialized clause ambiguous under a mid-window cap change — refining → rewritten as an observation-vs-history walk (each event’s recorded count equals the recount from preceding events; each below its own contemporaneous cap — never a recount against the current record). FC8-7 — Check 4 contradicted the crash-gap repair — refining → the no-prior-event repair case named in the check. FC8-8 — clock_tolerance conflated two clock disciplines — refining → split into components (a) cross-store skew and (b) accounting-clock regression, with consumers re-pointed. FC8-9 — step 6 omitted the statutory-exclusion record step 5.v promised — refining → added to the deliver-path evaluation_inputs enumeration.
  • FC8-10 — “deliberately overrides” the derived-index posture — rhetorical → restated as conformance: the serialized path performs the rebuild inside the serialization boundary, which is exactly the guarantee a rebuild can honor.

Pass 1 / Pass 2 rerun after the fixes: clean — subscribed and status_of are declared constituent queries (capability provenance intact); the unsubscribed reason and the transport-retry path are referenced from signature to checks; no new extraction candidate (the retry adjudication reads constituent stores through declared surfaces and carries no state).

Final Critique 9 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-12 (AI-conducted round; claude-fable-5, fresh context as before; the FC6-1 accepted-deviation noted as settled). GATE CLEAN FOR GROUNDING — zero foundational findings. Pass 1 surfaced one rhetorical item; Pass 2 returned clean for the second consecutive round, with the reviewer explicitly confirming all ten invariants’ capability provenance, the extraction-gate verdicts, and that the serialized-per-principal rebuild defense holds against the derived-index rule’s three obligations. Pass 3 at X2 surfaced 13 findings: 0 foundational, 8 refining, 5 rhetorical — under the 92%-good threshold, the grounding-determinative result. All 13 closed in-pattern in the same session:

  • FC9-2 — replay quantifier covered the unrendered unsubscribed verdict — refining → Invariant 7, Replay semantics, and Check 5 now scope to gate-rendered dispositions; an unsubscribed event’s replay is its recorded point-query outcome. FC9-3 — Check 3 demanded a gate observation unsubscribed events never make — refining → step 4 now journals the audience re-check outcome in evaluation_inputs; Check 3’s none-rule restated to match. FC9-4 — abandonment terminality scoped only to the crash-gap path — refiningredispose step 3 now opens with the abandonment terminal, applied on every path. FC9-5 — authorization to redispose unstated — refining → the authorization edge case covers both actions, names the digest bar as integrity-not-authorization, and flags redispose as the surface to gate first. FC9-6 — invocation-duration bound not disclosed — refining → added to the externally-clearable disclosures. FC9-7 — fan-out-at-scale edge case silently dropped from the sibling’s set — refining → restored, sharpened to this composition’s own scale surfaces (journaled queried list vs. the payload cap; now-staleness vs. loop length; per-principal-only serialization). FC9-8 — statutory arm never exercised by an example — refining → the finn example added (no stored window; the statutory exclusion empties the set; delivery after the window via redispose). FC9-9 — journal_query_capability’s consumer list omitted the action path — refiningredispose’s runtime journal reads enumerated.
  • FC9-1 — Notification Invariant 2 cited but unglossed — rhetorical → gloss completed (status monotonicity is what makes the transport adjudication stable). FC9-10 — walkthrough’s single-number clock_tolerance unqualified — rhetorical → the both-components attestation stated. FC9-11 — “in-memory” over-pinned the index’s locale — rhetorical → dropped. FC9-12 — “advisory … must treat it as binding” self-contradiction — rhetorical → unpacked (the composition advises; the deployment’s obligation makes it binding). FC9-13 — Check 2’s duplicated anchor clause — rhetorical → deduplicated.

Pass 1 / Pass 2 rerun after the fixes: clean. The pattern grounds on Final Critique 9 with the formal layer already discharged (see §Formal model above) — refining and rhetorical findings closed, foundational count zero across all three passes.

Formal model — 2026-06-12: TLA+ authored and verified. Derived model preference-aware-notification-fanout.tla + config preference-aware-notification-fanout.cfg, checked by tla-checker via tools/harness/check.mjs. What it checks: the load-bearing Invariant 4 (frequency-cap safety, safety arm) under cap_serialization = serialized-per-principal — one principal, one interpreted (window, cap) pair, Workers concurrent fanout invocations each rendering at most one disposition; the correct gate is a single atomic observe-and-commit (GateDeliver) alongside the cap-suppression branch (GateSuppress, which makes the cap guard non-vacuous: once the cap is met, suppression is the only enabled disposition). The committed delivery count never exceeds the cap (Inv4_CapSafety) and never goes negative. The delivery_count_index is deliberately not modeled as separate state — per the Execution Contract’s derived-index rule, the model covers the truth-bearing store (the journaled count) and omits the index. Exhaustive: 10 states at Cap = 2, Workers = 3, holds; bound bump to Workers = 4 → 29 states, exhaustive, still holds (saturation recorded in the config). Buggy twin preference-aware-notification-fanout-buggy.tla splits the gate into GateObserve / GateCommit with no re-check at commit — the deployment that declared serialization but implemented read-then-commit; two workers both observe headroom on the last slot and both commit, overshooting the cap; rejected by the checker (Inv4_CapSafety violated, 30 states), with Inv_NonNegative still holding so the violation is isolated to the cap bound — the twin shape mirrors capacity-constraint-enforcement-buggy-toctou.tla exactly, as the kickoff adjudication predicted. Scope exclusions, named: quiet windows / channel selection / suspended paths (per-record predicates, no cross-invocation race); the suspend-vs-fanout interleaving (no stated invariant to violate — see the formal-layer vote above); redispose’s per-(fanout_id, principal_ref) serialization (the identical guard shape at a different key). Conflict-protocol outcome: none — the model corroborates the English; canonical English unchanged.

Final Critique 10 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-12 (AI-conducted post-grounding rescan; fresh context: pass question sets, the spec body with Status and Lineage withheld, the constituent specs). GATE REOPENED — one foundational surfaced after FC9 grounded. The reader re-verified the cross-reference surface (all counts, names, signatures, rejection vocabularies confirmed) and surfaced 6 findings: 1 foundational, 2 refining, 3 rhetorical, all closed in-pattern:

  • OG-1 — fail-closed preamble overrode the gate’s own precedence short-circuit — foundational → step 5’s input-specific fail-closed checks (accounting-unreadable, interpretation-undeclared) were evaluated before shaping_disposition’s precedence, so a Suspended (or quiet-window-eligible) principal carrying frequency_limit was mis-dispositioned failed(accounting-unreadable) during an Event Log read outage — contradicting the declared precedence and Invariant 2’s “first rule that fired” (no safety invariant breached; the defect is mis-classification: a retry-eligible failure where a terminal suppression was owed). Fixed: preference-unreadable remains the one pre-gate guard (the record gates every rule); accounting-unreadable / interpretation-undeclared deferred into the precedence walk and raised at the consuming rule (iii / iv / v); shaping_disposition gains the recent_delivery_history | unavailable input and the fail(accounting-unreadable | interpretation-undeclared) codomain; rules ii–v, Mechanism, the Configuration preamble, step 6, and the outage example aligned. Disposition correctness is now independent of host input-assembly order (the gate short-circuits at rule i regardless of whether the host eagerly attempted the rebuild).
  • OG-2 — per-principal cap serialization’s applicability to redispose uncross-referenced — refining → the cap_serialization knob and redispose step 4 now state the cap path serializes wherever it runs (both actions), and redispose’s per-(fanout_id, principal_ref) serialization does not subsume it. OG-3 — reconciliation-run obligation and abandonment-write authority unenumerated — refining → an externally-clearable “Reconciliation discipline” disclosure was added (superseded by FC11’s surface). OG-4 — “fall through to deliver” read as bypassing the statutory exclusion — rhetorical → rule 5.ii now falls through to channel selection (rule v). OG-5 — misplaced parenthetical in redispose step 3 — rhetorical → reworded. OG-6 — “delivered event” vs the type name fanout.created — rhetorical → named fanout.created (deliver-verdict) in step 6 and Invariant 4.

Refinement re-pass (Pass 1 → 2 → 3 after the fixes) surfaced one follow-on, closed: OG-1f — Invariant 7’s “the gate rendered” quantifier swept in the new fail(...) codomain — refining → Invariant 7 now scopes to gate-rendered deliver-or-suppress dispositions and excludes the fail verdicts (journaled as fanout.create-failed), parallel to the unsubscribed carve-out (FC9-2 precedent). Pass 2 clean — the unavailable marker and the fail codomain are pure-function inputs/outputs, not new state.

Final Critique 11 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-12 (AI-conducted, fresh context as before). GATE NOT CLEAN — reconciliation-surface provenance cluster. A second fresh reader surfaced 10 findings: 4 foundational (claimed), 4 refining, 2 rhetorical. Adjudication trimmed the foundational cluster: the central valid defect was that the two reconciliation event types (fanout.abandoned, fanout.cap-overshoot-reconciled) were written by anonymous “deployment reconciliation” with no capability-provenance home (R-2 / R-9), while R-3 (“fanout.abandoned is unclassified composition state”) was an over-claim — the record lives in Event Log, the sanctioned history store, so it is not composition-carried state, and R-8 was refining-foundational-adjacent. Resolution (adjudicated, owner’s choice: a composition-introduced surface over a lighter declared-capability framing): introduce the composition-introduced reconciliation surface reconcile_gaps / reconcile_overshoots that owns both writes, reads the journal under journal_query_capability, computes pure detectors, appends via EventLog.append, and carries no composition state (records live in Event Log) — re-pointing Invariant 4’s liveness arm, the invocation→dispositions relation, the crash-reconciliation edge case, and the externally-clearable disclosure to it. The action surface is now three surfaces, not two. Other fixes: R-4 — preference-unreadable gate-verdict ambiguity — refining → step 5 restructured into one pre-gate guard plus two in-gate verdicts. R-5 — redispose journal-rejected “nothing committed” vs the irreversible Notification.create — refining → the deliver-pair rollback now explicitly leans on the cross-store multi-write atomicity boundary. R-6 — no-record deliver-unshaped vs an undeclared default shape — refining → rule ii and the no_record_policy knob now pin that deliver-unshaped requires a declared default shape, else fail-closed, never a silent deliver. R-8 — the invocation-duration bound is load-bearing for Invariant 3 but was not a knob — refining → promoted to a declared invocation_duration_bound Configuration knob with a setting rule; Invariant 3’s Rests on, Clock semantics, the walkthrough config, and the disclosures re-pointed.

Final Critique 12 — fresh-reader Phase 3 + clearance gate, X2 on Pass 3 — 2026-06-12 (AI-conducted, fresh context as before). GATE CLEAN FOR GROUNDING — zero foundational findings. A third fresh reader confirmed all nine GRID nodes, every constituent invariant citation by name and number, capability provenance for every Rests on: line, both derived-index classifications, and — the FC11 subject — that the reconciliation surface carries no composition state and its writes are consistently owned and referenced. 5 findings: 0 foundational, 3 refining, 2 rhetorical, all closed in-pattern (one rhetorical accepted with rationale):

  • S-1 — Invariant 2’s enumeration omitted unsubscribed — refining → the deliberate exclusion noted in-line (unsubscribed is a redispose-only audience-recheck outcome, not a gate verdict against an Active subscriber). S-2 / S-4 — redispose’s status_of read-then-create window, and the two-axis serialization, stated thinly across three sites — refining / rhetorical → consolidated into the Cross-store edge case: per-(fanout_id, principal_ref) closes the status_ofcreate window (Invariant 9), per-principal closes the cap window (cap_serialization), and a redispose deliver serializes on both. S-3 — the best-effort cap-overshoot hazard (the spec’s self-named load-bearing race) had no worked example — refining → the fred example added: two concurrent invocations both observe count = cap − 1, both deliver, the per-commit anchor still holds, and reconcile_overshoots writes the fanout.cap-overshoot-reconciled record Check 2 reads.
  • S-5 — blockquote “frequency caps suppress” overstates against the per-commit best-effort conditional — rhetorical, accepted with rationale → the Tier-1 blockquote legitimately simplifies; the precise conditional (hard under serialized-per-principal; bounded detectable overshoot otherwise) is carried in Invariant 4, the cap_serialization knob, and now the fred example.

Pass 1 / Pass 2 rerun after the fixes: clean. The pattern re-grounds on Final Critique 12 — the FC10 gate-ordering defect and the FC11 reconciliation-provenance cluster closed, foundational count zero across all three passes.

Formal model — coverage note, 2026-06-12 (post-FC12). The FC10–FC12 changes do not touch the cap-TOCTOU race the TLA+ model verifies (Invariant 4’s atomic observe-and-commit under serialized-per-principal, and the split-observe/commit buggy twin). The gate fail-closed reordering (OG-1) concerns per-record unreadable/undeclared inputs, not the cross-invocation cap race; the reconciliation surface (reconcile_overshoots) formalizes the liveness-arm writer, which the model already lists as out-of-scope (a best-effort liveness property, not the modeled safety race); and OG-2 / S-2 / S-4 reinforce the very serialized atomic-observe-and-commit assumption the correct model encodes. The model continues to corroborate the English; no re-derivation required, coverage matrix unchanged.


Grace Commons — open foundation for business logic patterns.

This site uses Just the Docs, a documentation theme for Jekyll.