Preference
Table of contents
- Preference
- Intent
- Summary
- Structure
- Examples
- Consumer SaaS — onboarding preferences
- Marketing platform — vacation suspend
- Account closure — explicit deletion
- Rejection path — set with no preference fields
- Rejection path — set with undeclared channel
- Rejection path — suspend a Deleted record
- Rejection path — delete an already-Deleted record
- Regulated adversarial scenarios
- Edge cases and explicit non-goals
- Generation acceptance
- Composition notes
- Standards references
- Status
- Lineage notes
A messaging primitive: a per-principal record of how that principal wants delivery shaped — which channels are preferred, what frequency limits apply, what quiet hours are in force, what format is preferred. Each record has an opaque immutable id; the principal reference and the preference values are immutable properties set at create time. The atom records how delivery should be shaped when a notification is otherwise permitted. Whether the principal has subscribed to the relevant topic, and whether the system may communicate with the principal at all, are answered by separate concepts and are not this atom’s concerns.
Intent
Every system that pushes information to people accumulates two kinds of question over time. The first is should this person receive this class of information at all? — a topic-subscription question, and separately, a legal-permission question. The second is the question this atom answers: given that the first kind has resolved in favor of delivery, how should the delivery be shaped? A principal may want email but not SMS. They may want at most five notifications a day. They may want silence between 10pm and 7am. They may want plain text rather than rich HTML. None of these is a question about whether to deliver; all are questions about how.
Preference records the answers. The atom owns the per-principal record of delivery-shaping values — channels, frequency limits, quiet hours, format — and the lifecycle of that record from creation through suspension or deletion. The composing fanout pattern reads the record at the moment a notification is queued and shapes the delivery accordingly. The atom does not deliver, it does not consult subscriptions, it does not evaluate legal permission, and it does not interpret the meaning of any preference value beyond enforcing structural constraints (the at-most-one-currently-in-effect-per-principal rule, the channel-must-be-declared rule, immutability of recorded values).
The atom’s three semantic states are distinct because each answers a different operational question. Active: the principal has stated delivery preferences and delivery should proceed under them. Suspended: the principal has paused delivery without modifying their preferences; subsequent fanout calls observe the suspension and suppress delivery. Deleted: the principal’s preference record is no longer in effect — either because it was superseded by a new set call (the principal updated their preferences) or because the principal explicitly deleted it. Suspended is a first-class state distinct from “Active with empty channel preferences” — suspending a record does not modify any preference value, so a later resumption (via a fresh set call carrying the prior values) does not require the principal to re-enter their choices. Deleted is the terminal state; once a record is Deleted, no transition restores it; the principal who wants their preferences back creates a new record.
Updates are not retroactive in the operational sense the atom commits to: a new set call produces a new record, the prior record transitions to Deleted, and the prior record’s preference values are unchanged. A composing fanout pattern that captured a prior record’s values at the moment a notification was queued continues to deliver that notification under the captured values — the atom does not push updates into already-queued work and does not modify any caller’s captured copy of a prior record’s values. The atom records sufficient timestamps (set_at on creation, deleted_at on supersession) for any composing pattern to determine which record was currently in effect at any past moment.
This is a freestanding concept in the EOS (Essence of Software — Daniel Jackson’s framework for specifying software concepts as freestanding, composable units) sense. It has its own state (the preference record set), its own actions (set, suspend, delete, current_for, read), and its own operational principles (one currently-in-effect record per principal; immutability of preference values; supersession on update; suspension preserves values; terminal Deleted; no record is ever removed from the store). It does not implement notification routing, subscription evaluation, legal-permission evaluation, delivery transport, or interpretation of preference values’ semantics. Each is a separate composable pattern; see Composition notes.
Summary
Preference is the atom that records, per principal, how delivery should be shaped — the channels the principal prefers, the frequency limits they have set, the quiet hours during which they want silence, the format they want their notifications in. It does not record whether the principal wants the information (that is a different concept), and it does not record whether the system is legally permitted to communicate with the principal (that is another different concept). It records, given that those other questions have resolved in favor of delivery, the envelope the principal wants the delivery to come in.
The atom’s job is narrow: maintain a durable (persisted to storage, survives system restarts) record of each principal’s preferences, support updates via supersession (a new preference record replaces the prior one; the prior record is retained in history as audit evidence), support suspension (the principal pauses delivery without removing the preference values, so a later resumption is one action away), and support deletion (the principal’s preferences are explicitly removed from currently-in-effect status). The store is append-only — no preference record is ever removed; the full history of every principal’s preferences is queryable for the lifetime of the system.
Two queries are the primary runtime operations. current_for(principal_ref) returns the principal’s currently-in-effect preference record (the one in Active or Suspended state) or none if no such record exists — this is the query a composing fanout pattern uses at the moment a notification is queued, to determine the delivery shape. read(preference_id) returns the full record for any preference id, in any state, including Deleted — this is the query an audit, a data-subject access request, or a compliance review uses to trace the history.
The atom enforces three structural commitments load-bearingly. At most one currently-in-effect record per principal prevents conflicting preference signals — a principal who has set preferences twice has the second set governing, and the first set is retained as history rather than as an active alternative. Channel preferences must reference declared channels prevents records from referencing channels the deployment does not actually support — the declared channel set is named at instance creation, and set calls referencing channels outside the declared set are rejected. Suspension preserves preference values keeps the suspend-resume cycle cheap for users — suspending a record does not modify its channel preferences, frequency limit, quiet hours, or format; a later set call that mirrors the suspended record’s values returns the principal to Active without any vocabulary loss.
The atom does not interpret preference values beyond enforcing the structural rules above. What “preferred” or “opt-out” means at the channel level, how a frequency limit is shaped (per-hour? per-day? per-topic?), how quiet hours encode timezone — all are deployment vocabulary. The atom stores the values as opaque payloads against a declared channel set; the composing fanout pattern interprets and applies them.
The most common uses are: implementing user-facing notification preference pages (web app, mobile app, account settings); honoring opt-out and frequency obligations under marketing regulations (CAN-SPAM, TCPA); satisfying the GDPR Article 7(3) easy-withdrawal-of-consent expectation by making preference changes one action away (a new set call is the same surface as the original); building the audit trail of how delivery shaping evolved for each principal over time. The atom is the third entry in the messaging/ category.
Structure
Store instance model
The Preference atom operates against a named store instance. A store_name identifies the instance; multiple instances coexist in real systems — one per product line, jurisdiction, or principal namespace. preference_id values are unique within an instance; uniqueness across instances is a composing concern. principal_ref uniqueness is enforced within the instance for the at-most-one-currently-in-effect rule; a principal known to two different instances is two distinct principals at the atom level. Calls implicitly target a single routed instance; instance selection is a deployment-routing concern.
Each instance carries a declared channel set — the named delivery surfaces that records in this instance may reference. The set is declared at instance creation (e.g., ["email", "sms", "push", "in-app"]) and is persisted as an instance configuration record in the store at creation time, alongside and distinct from preference records. The instance configuration record carries: store_name, the declared channel set as an ordered list of channel names, and a declared_at timestamp. This record is part of the preference store’s queryable surface — an auditor given the store can read it without recourse to external configuration files or deployment tooling. The atom does not define what channel names are valid in the abstract; it enforces only that channel names appearing in a preference record are members of the declared set at the moment the preference record was created. Adding a channel to or removing a channel from the declared set is a deployment-level operation that produces a new instance configuration record (with an updated declared_at) rather than modifying the original; the full history of channel-set declarations is therefore queryable. Impact on existing preference records (e.g., a removed channel still appearing in a Deleted historical preference record) is the composing layer’s concern.
Instance configuration records are owned by the deployment, not by the atom’s runtime action surface: there is no declare_channels action in the atom. The deployment writes configuration records via its provisioning tooling (a one-time write at instance creation; an append on each channel-set update). The atom’s audit story depends on the deployment honoring three obligations on those writes, named here as a single contract. Append-only: an existing configuration record’s fields are never mutated; a channel-set change produces a new record with a new declared_at. Durable: no configuration record is removed from the store. Bootstrap-ordered: at least one configuration record exists with declared_at at or before the set_at of any preference record in the store — equivalently, the deployment writes the initial configuration record before exposing the atom’s set action to callers. Invariant 10 binds the append-only and durable obligations to the audit surface; the bootstrap-ordering obligation is enforced indirectly through Invariant 5, because a set call carrying channel_preferences cannot pass the membership check if no configuration record exists at the matching declared_at. Deployments that need the contract enforced positively rather than indirectly — for example, regulator-facing audit that must show who updated the declared channel set and when — wrap configuration writes with an Audit Trail or Actor Identity composition; the bare atom does not.
Identity model
Every preference record known to the system has a preference_id — an opaque, immutable, system-generated identifier produced by set. The id is the record’s identity; the principal reference and the preference values are immutable properties of the record, not its identity.
The opaque-id model follows the same discipline used across the library. Identifying a record by principal_ref alone would collapse the principal’s update history into a single mutable record, defeating the audit story — a principal who updates their preferences three times has three records, each with its own id, each independently queryable. Identifying by (principal_ref, set_at) would entangle identity with timestamps, which the at-most-one-currently-in-effect rule (Invariant 3) already polices on a different axis. Opaque ids preserve one-record-one-id discipline.
The atom uses principal rather than recipient or subscriber as the entity term because preferences are recorded against an identity — independent of whether the principal has yet been the target of any notification or has subscribed to any topic. A principal may hold a preference record without ever being subscribed to anything; a principal subscribed to many topics holds at most one currently-in-effect preference record across all of them.
Ids are not reused after a record reaches Deleted.
Inputs
- A principal reference identifying who the preferences belong to. Opaque — the principal registry is a separate concern. The atom requires only that principal references support equality testing (so
current_forcan locate the currently-in-effect record and the at-most-one-currently-in-effect rule can be enforced); it does not parse, normalize, or otherwise interpret their contents. Equality is exact and the atom performs no normalization (no case-folding, whitespace trimming, or Unicode normalization). Because Invariant 3 (at most one currently-in-effect record per principal) depends on consistent equality, the deploying system is responsible for canonicalizingprincipal_refvalues before passing them — two references intended to denote the same principal must compare equal, or the atom will treat them as two distinct principals. - A
channel_preferencesvalue (optional): a map (or equivalent structure) from declared channel name to opaque per-channel preference value. The atom enforces that every key is a member of the instance’s declared channel set; the per-channel preference value is opaque and is stored unchanged. Whether"preferred","backup","opt-out", a numeric priority, or a structured record is the right shape for the preference value is the deployment’s vocabulary call. - A
frequency_limitvalue (optional): an opaque, deployment-shaped value capturing the principal’s frequency cap (e.g.,{per_day: 5},{per_hour: 1, per_day: 10}). The atom does not interpret the shape; the composing fanout pattern does. - A
quiet_hoursvalue (optional): an opaque, deployment-shaped value capturing the windows during which delivery should be suppressed (e.g.,{start: "22:00", end: "07:00", timezone: "America/Los_Angeles"}). The atom does not interpret the shape. - A
formatvalue (optional): an opaque, deployment-shaped value capturing format preferences (e.g.,"html"vs"plain", density, locale). The atom does not interpret the shape. - A
metadatavalue (optional): an opaque payload the atom stores unchanged. Carries deployment-specific context (the form version through which preferences were collected, the user-agent string, the consent-flow identifier). The atom does not parse or validate it. Unlike the four preference fields,metadatadoes not by itself satisfy the at-least-one-preference-field requirement onset: asetcarrying onlymetadataand nochannel_preferences,frequency_limit,quiet_hours, orformatis rejected asinvalid-request. - Opaque-input bounds. All opaque inputs —
principal_ref, per-channel preference values,frequency_limit,quiet_hours,format, andmetadata— are stored as-supplied; the atom enforces no length cap on any of them. The deploying system is responsible for bounding their size to match whatever the underlying store, transport, and equality-check implementations can handle efficiently. The cap (or the choice to leave size unbounded and accept the operational consequences) is deployment policy, disclosed alongside the fanout-on-no-record and clock-tolerance disclosures. - Actions:
set(principal_ref, channel_preferences?, frequency_limit?, quiet_hours?, format?, metadata?) → preference_id | rejected(reason)suspend(preference_id) → ok | rejected(reason)delete(preference_id) → ok | rejected(reason)current_for(principal_ref) → preference_record | noneread(preference_id) → preference_record | not-known
- A clock providing wall-time timestamps, injected at the atom’s single I/O seam. Per the Logic Confinement Principle (
EXECUTION_CONTRACT.md), the host reads wall-time at the seam and the pure transition receives the timestamp as an input — the clock is neither read inside the pure core nor supplied by the business caller. Timestamps on each action (set_at,suspended_at,deleted_at) are stamped from this host-injected clock at the moment of the write, never passed in by the caller. This is both logic-confinement-conformant (no internal clock read in the core) and audit-sound: it forecloses caller-supplied timestamp lying and binds the audit story (Temporal property 11, clock-tolerance disclosure) to a single clock the deployment can characterize.
Outputs
- The current set of preference records (Active, Suspended, and Deleted).
- For each record:
preference_id,principal_ref,channel_preferences(if supplied),frequency_limit(if supplied),quiet_hours(if supplied),format(if supplied),metadata(if supplied),set_at,status, and the applicable lifecycle timestamps (suspended_atif the record has ever been Suspended;deleted_atif Deleted). setreturns the newpreference_idon success, or a rejection naming the failed precondition.suspendanddeletereturnokon success, or a rejection naming the failed precondition.current_forreturns one of two first-class outcomes: the full preference record (all stored fields for the principal’s currently-in-effect record), ornoneif no Active or Suspended record exists for the principal. Both are answers to the query, not success-failure pairs.readreturns one of two first-class outcomes: the full preference record for the queried id, ornot-knownif no record with that id exists. A Deleted record is returned in full byread; deletion is a state, not a removal.
State
A preference record occupies one of three named states:
- Active — the principal’s preferences are in force; the record’s values shape any delivery the composing fanout pattern attempts. There is at most one Active or Suspended record per principal at any time (Invariant 3).
- Suspended — the principal has paused delivery; the record’s preference values are retained unchanged, but a composing fanout pattern observing the Suspended state suppresses delivery. There is at most one Active or Suspended record per principal at any time.
- Deleted — the record is no longer in effect. Terminal. A record reaches Deleted by being superseded (a new
setcall for the same principal) or by explicit deletion. Deleted records are retained in the store as audit evidence; they are returned byread(preference_id)but excluded fromcurrent_for(principal_ref).
Each record carries:
preference_id— opaque, immutable, system-generated. Set onset. Never changes.principal_ref— opaque reference to the principal whose preferences are recorded. Set onset. Never changes.channel_preferences— map from declared channel name to opaque per-channel preference value. Set onsetif supplied. Never changes; absence is also immutable.frequency_limit— opaque value if supplied. Set onset. Never changes; absence is also immutable.quiet_hours— opaque value if supplied. Set onset. Never changes; absence is also immutable.format— opaque value if supplied. Set onset. Never changes; absence is also immutable.metadata— opaque value if supplied. Set onset. Never changes; absence is also immutable.set_at— wall-time when the record was created. Set onset. Never changes.status—active,suspended, ordeleted. Set toactiveonset; transitions per the rules below.suspended_at— wall-time when the record was suspended. Absent on records that have never been Suspended; set on the transition to Suspended; never changes after set. If a record went Active → Suspended → Deleted,suspended_atis present anddeleted_atis also present.deleted_at— wall-time when the record reached Deleted. Absent unless status isdeleted; set on the transition to Deleted; never changes after set.
Transitions:
set(principal_ref, ...)→ a new record is recorded in Active with a freshpreference_id, the suppliedprincipal_ref, the supplied preference fields, andset_at = now. If the principal has a currently-in-effect record (Active or Suspended) at the moment of the call, that prior record’sstatustransitions todeletedand itsdeleted_atis set tonowas part of the same operation (Invariant 4 — supersession). Returns the newpreference_id. Rejected per the Decision points forset.suspend(preference_id)→ the record atpreference_idmoves Active → Suspended;suspended_at = now. Returnsok. Ifpreference_idis not known, returnsrejected(not-known). If the record is not in Active, returnsrejected(not-active). State is unchanged on rejection.delete(preference_id)→ the record atpreference_idmoves Active → Deleted or Suspended → Deleted;deleted_at = now. Returnsok. Ifpreference_idis not known, returnsrejected(not-known). If the record is already Deleted, returnsrejected(already-deleted). State is unchanged on rejection.current_for(principal_ref)→ read-only query; no state change. Returns the unique record for whichrecord.principal_ref = principal_refandrecord.status ∈ {active, suspended}, ornoneif no such record exists. Invariant 3 guarantees uniqueness.read(preference_id)→ read-only query; no state change. Returns the full record for the given id, ornot-knownif no record exists for that id.
Flow
- A principal expresses preferences; the composing layer creates a record. A preferences UI (web settings page, mobile app, API) collects the principal’s choices and calls
set(principal_ref, channel_preferences, ...). The atom records the preference set in Active and returns the id. If a prior record was currently-in-effect, it transitions to Deleted atomically with the new record’s creation. - The composing fanout pattern reads the record at delivery time. When a notification is queued for
principal_ref, the fanout pattern callscurrent_for(principal_ref). The atom returns the currently-in-effect record (Active or Suspended) ornone. The fanout pattern uses the returned record (or its absence) to shape — or suppress — the delivery. - The principal updates, suspends, or deletes their preferences. Exactly one of three transitions applies at the principal’s next action:
- 3a. The principal updates:
set(principal_ref, new_values)→ a new record is created in Active; the prior record transitions to Deleted (Invariant 4). - 3b. The principal pauses:
suspend(preference_id)→ the record moves Active → Suspended; subsequentcurrent_forqueries return the Suspended record, and the composing fanout pattern suppresses delivery. - 3c. The principal explicitly removes:
delete(preference_id)→ the record moves to Deleted; subsequentcurrent_for(principal_ref)returnsnone(until a newsetis called).
- 3a. The principal updates:
- Audit and recovery queries. An auditor, DSAR processor, or compliance review queries
read(preference_id)for any record (Active, Suspended, or Deleted) orcurrent_for(principal_ref)for the principal’s currently-in-effect record. Deleted records are returned byreadfor the lifetime of the store.
Decision points
-
At
set(principal_ref, channel_preferences?, frequency_limit?, quiet_hours?, format?, metadata?)—principal_refmust be non-empty — specifically, not null, undefined, or the empty string; otherwiseinvalid-request. The atom does not parse or interpret the opaque value beyond this presence check. At least one ofchannel_preferences,frequency_limit,quiet_hours, orformatmust be supplied — asetcall carrying no preference field has nothing to record and is rejected asinvalid-request. (The atom records the absence of preferences as the absence of a record, not as an empty record.) An emptychannel_preferencesmap ({}) does not satisfy this requirement: it carries no channel preference and is treated as not-supplied, so asetwhose only preference field is an emptychannel_preferencesmap is rejected asinvalid-request. Ifchannel_preferencesis supplied, every channel name appearing as a key must be a member of the instance’s declared channel set; a reference to an undeclared channel isinvalid-request. The per-channel preference value, thefrequency_limitvalue, thequiet_hoursvalue, theformatvalue, andmetadataare opaque — the atom does not parse, validate, or interpret their contents. There is no uniqueness constraint on the preference values themselves: two principals may hold identical preferences. All three rejection cases (emptyprincipal_ref, no preference field supplied, undeclared channel key) produceinvalid-request; the implementation is free to choose which to report first in error messages or telemetry, but the rejection code is invariant across them — the caller branches on the code, not the priority. -
At
suspend(preference_id)—preference_idmust reference a known record; otherwisenot-known. The record must be in Active; suspending a Suspended or Deleted record is rejected asnot-active. The singlenot-activecode covers both the Suspended and Deleted cases (a Suspended record never returns to Active, per Invariant 2); a caller that needs to distinguish the two callsread(preference_id)to inspect the record’s state. -
At
delete(preference_id)—preference_idmust reference a known record; otherwisenot-known. The record must not already be in Deleted; deleting an already-Deleted record is rejected asalready-deleted. A Suspended record may be deleted; the transition is Suspended → Deleted. -
At
current_for(principal_ref)— no precondition. An emptyprincipal_ref(null, undefined, or empty string) returnsnone— no record has an empty principal_ref, so the result is structurallynone. Any non-empty value that matches no record also returnsnone; sinceprincipal_refis opaque, there is no format-validation step. Bothnoneand a returned record are first-class outcomes; neither is a rejection. -
At
read(preference_id)— no precondition. An emptypreference_id(null, undefined, or empty string) returnsnot-known— no record has an empty id, so the result is structurallynot-known. Any non-empty value that matches no record also returnsnot-known; sincepreference_idis opaque, there is no format-validation step. Bothnot-knownand the full record are first-class outcomes; neither is a rejection.
Behavior
Observed behavior, derived from how user-preference systems are actually deployed:
-
Suspended is a first-class state, distinct from “channel preferences set to empty.” The likely objection: “if the principal wants no delivery, they can set every channel to
opt-out— why a separate state?” The mechanism: a Suspended record retains the prior preference values unchanged; the principal can return to Active delivery by callingsetwith the same (or any new) values, with no need to remember and re-enter their prior choices. A record with channel preferences set to opt-out, by contrast, loses the prior preferred-channel information — the principal who wants to resume delivery must re-state every channel preference. The audit story is also distinct: a Suspended record represents the principal’s “pause” intent; a record with all-opt-out channels represents the principal’s “modify” intent. Collapsing the two loses recoverable signal about what the principal meant. The result: suspending is cheap to reverse and audit-distinct from preference-modification; the principal’s intent (pause vs. modify) is recoverable from the records. -
Updates are not retroactive. The likely objection: “shouldn’t the most-recent preferences govern all in-flight notifications, including ones already queued under prior preferences?” The mechanism: a new
setcall does not modify any prior record; it creates a new record in Active and transitions the prior record to Deleted with adeleted_attimestamp. The composing fanout pattern is expected to capture the preference record’s values at the moment a notification is queued (its operational read time), not at the moment delivery is attempted. The atom records sufficient timestamps (set_at,deleted_at) for any composing pattern to determine which record was currently in effect at any past moment, supporting both queue-time-capture and delivery-time-re-evaluation policies. The result: a fanout pattern with queue-time-capture semantics delivers already-queued notifications under prior preferences; the nextsetdoes not retroactively re-shape them; the audit trail shows which preference record governed each notification. -
Frequency limits and quiet hours are preference fields, not separate atoms. The likely objection: “rate-limiting and time-windowing recur across many domains — surely they deserve their own atoms?” The mechanism: rate-limiting recurs in the abstract as a class of concern, but the values stored here as
frequency_limitandquiet_hoursare stored preference values — opaque payloads with no state machine of their own, no lifecycle independent of the preference record that carries them, and no meaning until interpreted by the composing fanout pattern at delivery time. A separate Rate-Limit atom would have its own identity, state, and actions; what this atom carries is a parameter, not a concept. The result: frequency limits and quiet hours stay in the preference record as opaque deployment-vocabulary fields; the composing layer interprets them at delivery time. -
Channels are deployment-declared, not atom-defined. The atom does not enumerate valid channel names. The declared channel set is named at instance creation and is part of the deployment’s configuration. The atom’s commitment is structural: a record’s
channel_preferenceskeys must be from the declared set at the moment of record creation. What channels mean operationally (email transport, SMS gateway, push service) is the composing system’s responsibility. -
At most one currently-in-effect record per principal. A
setcall for a principal who has an Active or Suspended record creates a new record and atomically transitions the prior record to Deleted. The two transitions (new record’s creation, prior record’s transition) are part of the same operation; an external observer never sees a moment in which the principal has two Active-or-Suspended records. -
Authorization is capability-based across writes and reads alike. The atom does not enforce who may call any of its actions. Any caller with a
principal_refmaysetpreferences for that principal or callcurrent_for(principal_ref); any caller with apreference_idmaysuspend,delete, orreadthe record. The read posture is the same as the write posture: capability gating on identifiers, no role check, no per-action authorization. Composing systems that need richer authorization — the principal must consent to a third party setting their preferences, the deletion must be co-signed by a privacy admin, only the principal or a privacy admin may read preference history — wrap the bare actions with Permissions or Actor Identity. The bare atom enforces something specific and useful (capability gating on ids) and the layering story for richer models is clean. -
The atom does not consult, re-check, or override the legality of communicating with the principal. Whether legal permission exists to deliver any notification to this principal at all is a separate concept, evaluated outside this atom. A composing fanout pattern that finds a currently-in-effect Active preference record does not, on the strength of that record alone, possess permission to deliver; the composing layer must sequence permission-evaluation before reading preferences. The atom’s commitment is conditional: given that delivery is permitted, here is the principal’s stated shape for it. A revocation of legal permission, by the separate concept that owns it, makes the preference record operationally irrelevant — the composing layer is responsible for sequencing permission-evaluation before reading preferences. The atom does not detect or react to permission revocation; the records remain unchanged. A composing system that reads preferences without first re-checking permission has made a sequencing error, not an atom-conformance error.
-
The atom does not consult or evaluate topic subscriptions. Whether this principal is subscribed to the topic of any particular notification is a separate concept. A composing fanout pattern that finds a currently-in-effect record does not, on the strength of that record alone, know that the notification falls within a topic the principal follows. The composing layer must sequence subscription-evaluation alongside preference-reading.
-
Absent preference fields signal no-preference, not opt-in or opt-out. A record with
channel_preferencessupplied butfrequency_limitabsent signals “the principal has stated channel preferences but no frequency cap”; the composing fanout pattern applies its deployment default for the absent dimension. The same holds for each preference field. This is structurally distinct from achannel_preferencesthat includes a channel with an explicit"opt-out"value (or equivalent) — which signals an active opt-out, not an absence. -
current_for(principal_ref)is a deterministic query. Invariant 3 (at-most-one-currently-in-effect) guarantees the query has a unique answer. The query is answered entirely from the current preference record set; no out-of-band data is consulted. -
read(preference_id)returns the full record in any state. A Deleted record is queryable viareadfor the lifetime of the store. The audit trail of a principal’s preferences over time is recoverable by enumerating records withrecord.principal_ref = Xand reading each. -
Concurrent calls resolve serially under host serialization guarantees. Two concurrent
setcalls for the same principal_ref: the first creates record R1 and supersedes any prior; the second creates record R2 and supersedes R1 (which was Active for the brief window between the two calls). Two concurrentsuspendcalls on the same id: the first succeeds; the second receivesnot-active(the record is now Suspended, not Active). Two concurrentdeletecalls on the same id: the first succeeds; the second receivesalready-deleted. A concurrentsuspendanddeleteon the same Active id: whichever is serialized first wins. Ifdeleteis first (record → Deleted), the latesuspendreceivesnot-active. Ifsuspendis first (record → Suspended), the latedeletesucceeds withok— a Suspended record is a valid target fordelete(the Suspended → Deleted transition is permitted per Decision points);already-deletedis not returned because the record is Suspended, not Deleted. Acurrent_forread concurrent with asetfor the same principal observes the serialization order — it returns either the prior record or the new one, never a torn state; under the queue-time-capture fanout policy a read that returns a just-superseded record is acceptable, because the fanout captured the record’s values at queue time. The host serializes the transitions; the recorded timestamps witness that order subject to Temporal property 11’s clock caveat. How the composing system handles competing callers is the composing system’s responsibility. -
No preference record is removed from the store. All records — Active, Suspended, Deleted — remain queryable via
readfor the lifetime of the system. Thedeleteaction moves a record into the Deleted state; it does not remove the record from storage.current_forexcludes Deleted records by design. -
Re-creating preferences after explicit deletion produces a new record. A principal who calls
deleteand thensetlater has two records: the prior Deleted record (carrying the original preference values and adeleted_attimestamp) and the new Active record (carrying freshpreference_id,set_at, and whatever preference values the principal supplied). The two records have independent ids. The retired id is not reused.
Feedback
Each successful action produces an observable, measurable change:
- After
set— a new record appears in Active with a freshpreference_id, the suppliedprincipal_ref, the supplied preference fields, andset_at. Total preference record count increases by one. Active-or-Suspended count for the principal becomes 1 (any prior was transitioned to Deleted as part of the operation). The id is returned. Falsifiable: afterset(p, prefs) → n,read(n)must return a record withstatus = activeandprincipal_ref = p;current_for(p)must return that record. - After
suspend— the record moves to Suspended withsuspended_at. Active count for the principal decreases by 1; Suspended count increases by 1; total count unchanged. Falsifiable:read(n)must returnstatus = suspendedandsuspended_atset;current_for(principal_ref)must still return the record. - After
delete— the record moves to Deleted withdeleted_at. Active-or-Suspended count for the principal decreases by 1; Deleted count increases by 1; total count unchanged. Falsifiable:read(n)must returnstatus = deletedanddeleted_atset;current_for(principal_ref)must not return the record. - After
current_for— no state change. Returns the full preference record ornone. - After
read— no state change. Returns the full preference record ornot-known.
set rejections: invalid-request. suspend rejections: not-known, not-active. delete rejections: not-known, already-deleted.
The full preference set — Active, Suspended, Deleted — is queryable via read and (for currently-in-effect records only) current_for.
Invariants
The following hold across all valid sequences of actions and constitute the verification surface of the pattern:
-
Invariant 1 — Preference record immutability. Once recorded, a preference record’s
preference_id,principal_ref,set_at, and each supplied preference field (channel_preferences,frequency_limit,quiet_hours,format,metadata) never change. Fields not supplied atsetremain absent for the record’s lifetime. Once set,suspended_atanddeleted_atnever change. Thestatusfield is the only mutable field; it transitions per Invariant 2. -
Invariant 2 — Status monotonicity. A record’s
statustransitions only in one direction: Active → Suspended (viasuspend), Active → Deleted (viadeleteor supersession), or Suspended → Deleted (viadeleteor supersession). No record returns from Suspended to Active or from Deleted to any other state. To return a principal to Active delivery from a Suspended or Deleted state, a freshsetcall creates a new record. -
Invariant 3 — At most one currently-in-effect record per principal. For any
principal_ref, at most one preference record is instatus ∈ {active, suspended}at any time. Asetcall for a principal who has a currently-in-effect record transitions the prior record to Deleted as part of the same operation (Invariant 4). -
Invariant 4 — Supersession atomicity. When
setis called for a principal who has a currently-in-effect record, the prior record’s transition to Deleted and the new record’s creation are part of the same operation. No external observer sees a moment in which the principal has two records in{active, suspended}. Invariant 4 asserts only the atomic co-occurrence of the two state changes, which holds unconditionally; the timestamp relationship between the prior record’sdeleted_atand the new record’sset_atis a best-effort directional claim deferred to Temporal property 11, because it depends on clock monotonicity that Invariants 1–10 do not. -
Invariant 5 — Channel preferences reference declared channels. Every channel name appearing as a key in any preference record’s
channel_preferencesis a member of the instance’s declared channel set at the moment the preference record was created. The authoritative source for that declared set is the instance configuration record (see Store instance model) with the maximumdeclared_at≤ the preference record’sset_at. A subsequent update to the declared channel set produces a new instance configuration record; it does not invalidate historical preference records whose keys were valid at their own creation time. -
Invariant 6 — Suspension is value-preserving. The
suspendtransition changes onlystatustosuspendedand setssuspended_at; the preference values (channel_preferences,frequency_limit,quiet_hours,format,metadataif supplied) and the principal’s reference remain as they were atsettime. This is the structural mechanism behind the cheap-resumption property: a composing system that wants to resume delivery can read the Suspended record’s values and replay them in a newsetcall without any vocabulary loss. -
Invariant 7 —
current_fordeterminism.current_for(principal_ref)returns the unique record for the principal withstatus ∈ {active, suspended}, ornoneif no such record exists. The query is determined entirely by the preference record set at query time. No out-of-band data is consulted. Invariant 3 guarantees the result is unique when non-none. -
Invariant 8 — No id reuse. No two preference records share a
preference_idacross the lifetime of the system. A deleted record’s id is not reused even after the record is Deleted. -
Invariant 9 — Preference store durability. No preference record is removed from the store. The total record count is monotonically non-decreasing. Deleted records are retained as audit evidence and remain queryable via
readfor the lifetime of the system. -
Invariant 10 — Configuration record integrity. Each instance configuration record in the store is immutable once written (its
store_name, declared channel set, anddeclared_atnever change) and is never removed. A channel-set change produces a new configuration record with a newdeclared_at; the prior record remains unchanged and queryable. The total configuration record count for an instance is monotonically non-decreasing. Unlike Invariants 1–9, which the atom’s own runtime actions enforce, Invariant 10 is a deployment-contract property: the atom asserts it as a required property of any conformant store, but configuration records are written by deployment provisioning tooling outside the atom’s action surface (see Store instance model), so the atom cannot enforce it at runtime and cannot detect an in-place mutation or removal from its records alone — positive enforcement requires composing configuration writes with Audit Trail or Actor Identity. This invariant is the audit-side counterpart to Invariants 1 and 9 for preference records, and is what makes Generation acceptance check 5 clearable from the store alone: the declared channel set at any pastset_atis the configuration record with the maximumdeclared_at≤set_at, and that record’s contents are guaranteed unchanged. -
Temporal property 11 — Timestamp ordering (best-effort; not a hard invariant — see note below). For any record with
suspended_atset,set_at ≤ suspended_at. For any record withdeleted_atset,set_at ≤ deleted_at. For any record with bothsuspended_atanddeleted_atset,suspended_at ≤ deleted_at. Across a supersession pair(R_prior, R_next)for the same principal,R_prior.deleted_at ≈ R_next.set_at— equal within the deployment’s declared clock tolerance (see Generation acceptance Check 4); this is the cross-record counterpart that, with the within-record inequalities above, constitutes the complete temporal model (the atomic co-occurrence itself is Invariant 4). These inequalities are best-effort under non-monotonic clocks; if the underlying clock moves backward between transitions, an inequality may be violated. Unlike Invariants 1–10 — which hold unconditionally across every reachable state — this property holds only when the implementation provides monotonic-clock discipline. The implementor is responsible for that discipline; see Edge cases. The inequalities are labeled separately from the hard invariants because audit reconstructions (Generation acceptance check 2) depend on their directional guarantee; violations are observable and diagnosable rather than silently corrupting.
Preference record immutability and store durability together give the auditability property — the full history of every principal’s preferences is recoverable from the preference store alone, with no gaps. At-most-one-currently-in-effect and supersession atomicity together give the unambiguous-currency property — at any moment, every principal has at most one preference record governing delivery, and the moment of transition between records is recorded. Suspension-is-value-preserving gives the cheap-resumption property — a principal who suspends and later wants to resume does not lose their prior preference values; the composing system can re-set them from the suspended record’s stored fields.
Examples
The three lifecycle scenarios below trace one principal (user_u) through onboarding, vacation suspend, and account closure — the record chain pref_001 → pref_088 → pref_141 — to show the same atom serving each operational context in turn. The rejection-path and regulated scenarios that follow use their own principals and ids. The deployment in each example has declared channels ["email", "sms", "push", "in-app"] at instance creation unless otherwise noted.
Consumer SaaS — onboarding preferences
A new user onboarding to a productivity app picks their notification preferences: email for daily digests, push for real-time mentions, no SMS, no quiet hours, plain-text format. The settings page calls set(principal_ref: user_u, channel_preferences: {email: "digest", push: "real-time", sms: "opt-out"}, format: "plain") → pref_001. The record enters Active.
When the composition fires an event for which user_u is subscribed, the fanout pattern calls current_for(user_u) → returns pref_001. The fanout pattern reads the channel preferences and creates one Notification per non-opted-out channel.
Three weeks later, the user adds SMS for urgent items and a frequency cap: set(principal_ref: user_u, channel_preferences: {email: "digest", push: "real-time", sms: "urgent-only"}, frequency_limit: {per_day: 10}, format: "plain") → pref_088. The prior record pref_001 transitions to Deleted with deleted_at; the new record pref_088 is in Active. current_for(user_u) now returns pref_088. Subsequent notifications are shaped under the new preferences.
Marketing platform — vacation suspend
A subscriber to a marketing newsletter is going on a two-week vacation and wants to pause all notifications without losing their preferences. The settings page calls suspend(pref_088) → ok. The record moves Active → Suspended with suspended_at. current_for(user_u) returns the record (in Suspended state). The fanout pattern observes the Suspended state and suppresses delivery.
When the subscriber returns, the settings page reads the suspended record’s values (via current_for(user_u)) and offers them as defaults; the subscriber confirms and the settings page calls set(principal_ref: user_u, channel_preferences: {email: "digest", push: "real-time", sms: "urgent-only"}, frequency_limit: {per_day: 10}, format: "plain") → pref_141. The prior record pref_088 (Suspended) transitions to Deleted with deleted_at (preserving its suspended_at and the original set_at). pref_141 is the new Active record carrying the previously-stored values. The subscriber is back to full delivery.
Account closure — explicit deletion
A user closes their account. As part of the closure flow, the account-deletion service calls delete(pref_141) → ok. The record moves to Deleted with deleted_at. current_for(user_u) returns none. The fanout pattern, finding no currently-in-effect record, treats delivery per the deployment’s fanout-on-no-record policy (some deployments default to system-default delivery, others suppress entirely; the policy is composing-system configuration).
The prior records (pref_001, pref_088, pref_141) all remain in the store as Deleted records. A subsequent DSAR (data subject access request) for user_u’s preference history enumerates the principal’s records from the audit surface (filtering the store on principal_ref = user_u, per Generation acceptance Check 1) and returns the full chronological history; each record’s content is what read(preference_id) would return for it.
Rejection path — set with no preference fields
A composing system attempts to record a “preference set” carrying nothing: set(principal_ref: user_u) → rejected(invalid-request). No preference_id is issued; no record enters the store. The Decision-point rule (at least one of channel_preferences, frequency_limit, quiet_hours, or format must be supplied) is the constraint.
Rejection path — set with undeclared channel
The instance was created with declared channels ["email", "sms", "push", "in-app"]. A composing system attempts to record a preference referencing a channel not in the declared set: set(principal_ref: user_v, channel_preferences: {email: "preferred", carrier-pigeon: "backup"}) → rejected(invalid-request). No record enters the store. The composing system must use only declared channels.
Rejection path — suspend a Deleted record
A retry after a network timeout: suspend(pref_001) → rejected(not-active). The record pref_001 is Deleted. The caller detects the rejection and suppresses the retry.
Rejection path — delete an already-Deleted record
A duplicate teardown call: delete(pref_141) → rejected(already-deleted). The record pref_141 already reached Deleted in the account-closure flow; the second delete changes nothing and is rejected. (A delete on a Suspended record, by contrast, succeeds with ok — the Suspended → Deleted transition is valid per the Decision points.)
Regulated adversarial scenarios
Three scenarios the preference store must survive in regulated contexts:
-
Regulator audit — demonstrate honoring opt-out under CAN-SPAM. A regulator investigates whether a marketing platform honored a principal’s opt-out for the
emailchannel after2026-03-14. The investigator queries the audit surface forprincipal_ref = user_v, enumerating the principal’s records. The records show:pref_201withemail: "preferred"currently-in-effect from2025-08-01to2026-03-14(set_at = 2025-08-01,deleted_at = 2026-03-14);pref_244withemail: "opt-out"currently-in-effect from2026-03-14onward (set_at = 2026-03-14,status = active). For any email delivery alleged to have occurred touser_vafter2026-03-14, the fanout pattern would have calledcurrent_for(user_v)and readpref_244. If a delivery occurred against this record, that is either a fanout-pattern conformance failure (the pattern read the record but delivered anyway) or a separate composing-layer failure — either way, the preference record is the structural evidence of the principal’s stated intent at the time of delivery. Invariants 1 (immutability) and Temporal property 11 (timestamp ordering) are the rebuttal: the record was created at that time with those values; it does not change. -
Disputed delivery — principal claims their quiet hours were ignored under TCPA. A principal complains that they received SMS at 11:30pm despite quiet hours of 10pm-7am. The investigator first reconstructs which record was currently-in-effect at the delivery timestamp — Generation acceptance Check 2: enumerate the principal’s records and take the one with the maximum
set_at≤ the delivery time whose currently-in-effect window covers it — obtaining itspreference_id, then queriesread(preference_id). The record showsquiet_hours: {start: "22:00", end: "07:00", timezone: "America/Los_Angeles"}. The atom’s records confirm the principal’s stated quiet hours at the moment of delivery; whether the fanout pattern observed them is the composing-layer question. The atom’s commitment: the principal’s preferences were recorded; the record is the structural evidence; no developer narration is required to confirm what the principal stated. -
Breach investigation — identify principals whose preferences may have been corrupted during a security incident. A security incident on
2026-04-01T05:00Zexposed the preference store to potential unauthorized modification. The investigator queries the audit surface for any record withset_at,suspended_at, ordeleted_atfalling within the breach window. Invariant 1 (immutability) and Invariant 9 (durability) are the atom-level rebuttal — but only as a contract, not as cryptographic enforcement: any record created before the breach window should not have been altered, and the atom’s records expose alteration only insofar as the underlying store does. Cryptographic protection against post-hoc tampering belongs to a composing Tamper Evidence pattern; without that composition, the bare atom’s records support forensic reconstruction (which records existed when, which transitioned within the window, which principals are affected) but do not, on their own, prove that no out-of-band write occurred. Records with a transition timestamp within the breach window are candidates for forensic review against the composing system’s authentication logs; records outside the window are presumed unaltered subject to whatever integrity discipline the underlying store provides.
Edge cases and explicit non-goals
What this atom does not cover:
-
Notification routing and fanout. This atom records preferences; it does not consult subscriptions, fire events, or create notifications. Those belong to a composing fanout pattern that wires the topic-subscription concept, the legal-permission concept, this atom, and an event source.
-
Whether delivery is legally permitted. Whether the system has permission to communicate with the principal at all — under GDPR’s consent requirement, HIPAA’s authorization rule, ePrivacy’s opt-in for non-essential communications — is a separate concept evaluated by the composing layer. The atom’s commitment is conditional: given that delivery is permitted, here is the principal’s stated shape for it. The composing layer must sequence permission-evaluation before reading preferences. See the corresponding Behavior bullet.
-
Whether the principal is subscribed to the topic. Whether this principal follows the topic of any particular notification is a separate concept. The composing layer must sequence subscription-evaluation alongside preference-reading. A principal who has set preferences but is not subscribed to a topic does not receive notifications for that topic, regardless of preference values; conversely, a principal who is subscribed but has no preference record receives notifications under the composing system’s default shaping.
-
Transport mechanism. Whether
emailis delivered via SMTP, a transactional email API, or an internal mail relay is a deployment concern. The atom records the principal’s channel-level preference; the composing layer interprets it. -
Preference-value semantics. What
"preferred","opt-out","backup", or a numeric priority means at the channel level is the deployment’s vocabulary call. The atom stores and returns these values opaque. Similarly for the structure offrequency_limit,quiet_hours, andformat— all opaque. -
Channel set evolution. Adding a channel to or removing a channel from the declared set is a deployment-level operation outside this atom’s surface. Existing records continue to carry their original channel keys (Invariant 5 holds at record-creation time, not perpetually). The composing layer is responsible for handling records that reference channels no longer in the declared set — typically by treating the absent channel as default-suppressed.
-
Configuration record management is a deployment obligation. The atom does not expose an action that writes instance configuration records; the deployment writes them through its provisioning tooling (see Store instance model). The three obligations on those writes — append-only, durable, bootstrap-ordered — are the deployment’s contract, bound to the audit surface by Invariant 10 (for the first two) and enforced indirectly through Invariant 5’s
set-time membership check (for the third). A deployment that mutates a configuration record in place, removes one, or exposessetbefore writing the first configuration record has broken the contract; the atom cannot detect the first two cases from its records alone and surfaces the third as aninvalid-requestrejection on everysetcall. Deployments needing positive enforcement (attestation of who declared which channel set, when) compose configuration writes with Audit Trail or Actor Identity. -
Default preferences for principals without a record. A principal who has never called
sethas no record;current_forreturnsnone. What the composing fanout pattern does in that case — apply system defaults, suppress entirely, prompt the principal — is composing-layer policy and must be disclosed for cross-deployment audit (see Generation acceptance). -
Resume from Suspended without re-statement. The atom does not provide a
resume(preference_id)action that returns a Suspended record to Active. To resume, a composing system reads the Suspended record’s values viacurrent_fororreadand callssetwith those values; the new record is in Active and the Suspended record transitions to Deleted. The likely objection: “this is a common user flow — why not a first-class action?” The mechanism: aresumeaction that returned a Suspended record to Active would introduce a reverse status transition (Suspended → Active), breaking Invariant 2’s monotonicity guarantee (Active → Suspended → Deleted is the only permitted direction). Beyond monotonicity, a principal who suspend-resume-suspends-resumes repeatedly would accumulate all of that history on a single record — each cycle adding aresumed_atand a newsuspended_at— rather than producing distinct records per lifecycle event. The supersession path keeps the audit trail clean: each lifecycle action produces a new record or a monotonic state transition, and the composing-layer ergonomics of reading the Suspended record’s values and offering them assetdefaults are cheap. The result: lifecycle monotonicity and audit-trail clarity win over action-surface convenience. -
Deletion-reason distinction (supersession vs. explicit). The atom does not record whether a Deleted record was superseded by a new
setor explicitly deleted by the principal. Both produce a Deleted record withdeleted_at. The distinction is approximately recoverable from the records via a timestamp-gap heuristic: if a successor record exists for the same principal withset_atwithin the deployment’s declared clock tolerance of the Deleted record’sdeleted_at, the deletion was likely atomic supersession (Invariant 4); if the gap is material, the deletion was likely explicit followed by later re-creation. This is a heuristic, not a definitive recovery — explicit-delete-then-recreate and supersession produce the same record structure, distinguishable only by the size of the timestamp gap. Deployments that need first-class deletion-reason recording (e.g., for regulator-facing audit dashboards that must distinguish “principal updated preferences” from “principal closed account”) can compose with Actor Identity or Audit Trail to attribute each lifecycle action; the bare atom does not carry a reason field. -
Conflicting preferences. Two
setcalls with different preference values for the same principal in rapid succession produce two records; the second supersedes the first. The atom does not detect or warn about “conflicting” preferences; it records the second as authoritative per Invariant 4. Composing systems that need conflict-detection (e.g., a UI that warns “you just changed this — are you sure?”) implement it at the composing layer. -
Bulk operations. There is no bulk-set, bulk-suspend, or bulk-delete surface. Operations on multiple principals require iteration at the composing layer.
-
Per-topic preference overrides. A principal who wants different preferences per topic (e.g., real-time push for security alerts but daily digest for newsletters) needs a richer model than this atom. The atom’s record applies to all notifications for the principal uniformly. Per-topic preferences are a composing-layer extension; one approach is to compose a separate preference instance per topic-class, with the composing fanout pattern selecting the right instance per notification.
-
Preference attribution. The atom does not record who called
set,suspend, ordelete. Attribution — the principal set their own preferences via the web UI, an admin set preferences on behalf of the principal, an automated process suspended preferences in response to bounce-back — belongs to a composing Actor Identity pattern. Thepreference_idis the hook: a composing Actor Identity pattern recordsattest(preference_id, acted_by_ref, credential)at action time. No field is added to the preference record itself; the attribution lives in the Actor Identity store. -
Authorization to set, suspend, or delete. The atom does not enforce who may call these actions. Authorization — only the principal or an authorized admin may modify preferences — belongs to the composing system.
-
Atomicity and crash semantics. The
setaction with supersession changes two records simultaneously: the prior record’sstatusanddeleted_at, and the new record’s full creation. A crash mid-setthat creates the new record without transitioning the prior, or vice versa, violates Invariant 4 (supersession atomicity). Thesuspendanddeletetransitions change two fields on one record (statusand a timestamp); a partial write violates Invariant 1 or Temporal property 11. The implementor is responsible for the transactional boundary that makes each operation atomic. The atom’s invariants additionally presume a host that serializes writes targeting the sameprincipal_ref— at minimum, linearizable writes perprincipal_ref(or a serializable transaction wrapping each operation). The serialization domain is the principal, not the individual record id: althoughsuspendanddeletetake apreference_id, every record belongs to exactly oneprincipal_ref, so all operations touching any record of a given principal —set(keyed by principal) andsuspend/delete(keyed by id) alike — must serialize against one another, or a concurrentsetandsuspend/deleteon the same principal can interleave inconsistently. Under weaker isolation (snapshot, read-committed), two concurrentsetcalls for the same principal can both observe no-currently-in-effect and commit, violating Invariant 3; the deployment is responsible for choosing a host isolation level that forecloses that case. Atomicity under normal operation is a conformance requirement: an implementation whosesetcan persist a new record without transitioning the prior — leaving the principal with two currently-in-effect records after recovery — is non-conformant, because Invariants 3 and 4 must hold across every reachable state. The spec does not define post-crash reconciliation (how an implementation detects and repairs a partial write is the implementor’s concern), but the post-recovery store must not exhibit a standing invariant violation. -
Preference data retention. The preference store retains all records for the lifetime of the system (Invariant 9). If preference values contain sensitive data — a
quiet_hoursvalue naming a timezone tied to physical location, ametadatapayload carrying tracking identifiers — the composing system is responsible for the retention and erasure policy. Retention Window is the composing pattern that bounds how long records must be kept and when they may be purged. The bare atom does not implement preference expiry or redaction. -
Cryptographic integrity of records. The atom’s immutability is spec-level — the spec says fields never change; it does not seal the records against malicious modification. Court-admissible and regulator-admissible preference records require composition with Tamper Evidence.
-
Clock semantics.
set_at,suspended_at, anddeleted_atare wall-time from the implicit clock. Clock skew, NTP adjustments, and timezone handling are deployment concerns. Temporal property 11 is best-effort under non-monotonic clocks.
Generation acceptance
The audit surface is the preference store inspected on its stored fields — distinct from and complementary to the action surface (set, suspend, delete, current_for, read). The action surface answers what does the atom do at runtime?; the audit surface answers what does the atom commit to recording, queryable on stored fields?. A derived implementation must produce a store that supports the audit-surface queries below, independent of whether the runtime action surface exposes them.
A derived implementation of Preference is acceptable — in the regulator-acceptance sense — when an external auditor, given the preference store, can do all of the following without recourse to source code, runbooks, or developer narration:
-
Check 1 — Enumerate every preference record with its full lifecycle.
preference_id,principal_ref,set_at,status, and the applicable lifecycle timestamps (suspended_atif the record was ever Suspended;deleted_atif Deleted) are present and queryable for every record ever created. The preference values (channel_preferences,frequency_limit,quiet_hours,format,metadata) are present for every record on which they were supplied. No record is missing from the store (Invariant 9). -
Check 2 — Reconstruct the currently-in-effect record for any principal at any past point in time. Given a
principal_refand a timestampt, the auditor can determine which record was currently-in-effect attby filtering onset_at ≤ tand (status ∈ {active, suspended}ordeleted_at > t) — taking the record with the maximumset_at≤t(Invariant 3 guarantees this record is unique). The interval convention is half-open[set_at, deleted_at): att == set_atthe record is currently-in-effect; att == deleted_atit is not (a successor created in the same atomic supersession is the one currently-in-effect att == deleted_at == successor.set_at, per Invariant 4). The reconstruction is exact with respect to stored timestamps (Invariants 1 and 4); the wall-clock truth of those timestamps is subject to Temporal property 11’s best-effort clock caveat. -
Check 3 — Confirm at-most-one-currently-in-effect. For any
principal_ref, at most one record is instatus ∈ {active, suspended}at any point in time. The auditor verifies by enumerating the principal’s records and confirming no two records have overlapping currently-in-effect windows (Invariants 3 and 4). -
Check 4 — Confirm supersession atomicity at the timestamp level. For any pair of records
(R_prior, R_next)for the same principal whereR_next.set_at > R_prior.set_atandR_prior.status = deleted, the auditor inspects the gapR_next.set_at − R_prior.deleted_at. A gap within the deployment’s declared clock tolerance is consistent with atomic supersession (Invariant 4). A gap materially exceeding the declared tolerance requires investigation: the gap is consistent with either an explicitdeleteissued between the two records (no atomicity violation; the principal was without a currently-in-effect record for the duration of the gap) or an atomicity violation (supersession was non-atomic; an external observer could have seen the principal with zero or two currently-in-effect records during the gap). The atom’s records cannot distinguish the two cases on their own — the same record structure is produced by both. Deterministic discrimination requires composition with Audit Trail (which records each action as a discrete event with its own attribution) or Actor Identity (which attests each action’s initiator); without such composition, the auditor must flag the gap as ambiguous-pending-external-evidence rather than as a clean pass or a clean failure. The deployment must disclose its clock tolerance (the maximum gap expected between two writes within the same atomic operation, e.g., “writes within the same database transaction share a statement timestamp; tolerance is 0ms” or “wall-time at write time; tolerance is 500ms under NTP”). The clock-tolerance disclosure belongs alongside the fanout-on-no-record policy disclosure required by check 6. -
Check 5 — Confirm channel-set membership at record creation. For every preference record’s
channel_preferences(if present), every key is a member of the instance’s declared channel set as it stood at the moment the preference record was created. The auditor verifies by reading the instance configuration records (see Store instance model) — specifically the configuration record whosedeclared_atis ≤ the preference record’sset_atwith the maximumdeclared_atin that range. Because the instance configuration records are persisted in the store at creation and on each channel-set update, this check is clearable from the store alone without recourse to external configuration artifacts. Confirming membership for every preference record also confirms the deployment’s bootstrap-ordering obligation: a record whose keys pass the membership check necessarily has a configuration record withdeclared_at≤ itsset_at, so no preference record predates all configuration records. -
Check 6 — Identify composing patterns active in this deployment. Whether preference attribution (Actor Identity), event firing history against preferences (Event Log), retention (Retention Window), tamper-evidence on the preference store (Tamper Evidence), and the legal-permission and topic-subscription concepts the deployment uses to gate delivery are wired in, and with what configuration. The deployment’s fanout-on-no-record policy must also be disclosed — when
current_for(principal_ref)returnsnone, does the fanout pattern apply system defaults, suppress delivery entirely, or prompt for preferences? Without this disclosure, the same operational situation (no preference record) produces different delivery outcomes across deployments and cross-deployment audit cannot interpret records uniformly.
This is the generator’s contract: any code generated from this atom must produce a preference store and a query surface that pass the six checks above.
Composition notes
Preference is freestanding and is designed to compose with:
- Subscription — the topic-interest concept that the composing fanout pattern consults to determine whether a given notification falls within a class the principal follows. Preference shapes delivery once Subscription has confirmed the principal follows the topic; the two atoms are sequenced peers in the fanout pipeline.
- Consent — the legal-permission concept that the composing fanout pattern consults to determine whether the system may communicate with the principal at all. Preference shapes delivery once Consent has confirmed legal permission; the two atoms are sequenced peers (Consent first, then Preference), not alternatives.
- Notification — the delivery-record concept. After a fanout pattern reads Preference and decides to deliver on a channel, it creates a Notification record on that channel. Preference is consulted; Notification is the result.
- Preference-Aware Notification Fanout (forthcoming — C11) — the composition that wires Subscription + Preference + Notification + an event source into an end-to-end fanout pipeline that honors per-principal delivery shaping. The composition observes Suspended preferences as delivery-suppress, frequency limits as queue-or-drop, quiet hours as defer-until-window, and channel preferences as route-or-suppress.
- Notification Fanout — the existing two-atom fanout composition. Preference-Aware Notification Fanout extends rather than replaces it; deployments that have not yet adopted preference shaping continue to use the base composition.
- Event Log — records each preference action (
set,suspend,delete) as an auditable event for replay and investigation when in-record timestamps are insufficient (e.g., when a deployment needs to record the full sequence of suspend-cycles a principal performed, beyond the singlesuspended_atthe record retains). - Actor Identity — records who initiated each preference action when attribution is required.
preference_idis the hook: a composing Actor Identity pattern recordsattest(preference_id, acted_by_ref, credential)at action time. - Retention Window — the preference store must be retained for whatever regulatory or operational lifetime the deployment requires.
- Tamper Evidence — in regulated contexts, the preference store is a target for after-the-fact manipulation (a record alleging the principal opted out could be rewritten to allege they opted in). Cryptographic commitment makes any rewrite detectable.
- Duplicate Prevention — for at-most-once semantics on
setunder retry conditions, where a network-timeout retry should not produce a second supersession.
Standards references
- CAN-SPAM Act (15 U.S.C. §7701 et seq.) — requires commercial email senders to honor unsubscribe requests within 10 business days and provide a working opt-out mechanism in every commercial message. The Preference atom’s
channel_preferenceswith channel-level opt-out values, combined with the composing fanout pattern’s enforcement, is the structural mechanism. The atom’s immutability and durability guarantees produce the audit record CAN-SPAM enforcement requires. - Telephone Consumer Protection Act (TCPA, 47 U.S.C. §227) — restricts automated and unsolicited calls and SMS, including frequency caps and time-of-day restrictions. The
frequency_limitandquiet_hoursfields, combined with the composing fanout pattern’s enforcement, are the mechanism for honoring these requirements per principal. - GDPR Article 7(3) — withdrawal of consent must be as easy as giving it. Article 7(3) is a consent obligation and belongs to the Consent atom, which governs the legal-permission axis. This atom satisfies an analogous preference-update ease principle: a principal who wants to change channel preferences, tighten frequency limits, or suspend delivery calls
setorsuspend— the same surface that established preferences originally. The ease-of-update claim here is scoped to preference shaping, not to consent withdrawal; the Consent atom carries the Article 7(3) obligation proper. - GDPR Article 21(2) — right to object to processing for direct marketing purposes; objection must terminate marketing processing without delay. Full Article 21(2) compliance is a two-atom obligation: this atom records the delivery-preference signal of the objection — encoded as the deployment’s per-channel “opt-out” value within
channel_preferences(the atom stores the value opaquely; the deployment’s vocabulary fixes the encoding) — while the Consent atom must revoke the corresponding legal permission to process. A deployment that records an opt-out in Preference without also updating Consent has not fully discharged the Article 21 obligation. The Preference atom’s contribution is the durable, immutable record of the principal’s stated opt-out signal at the moment of objection; combined with the composing fanout pattern’s enforcement (which interprets the deployment’s opt-out encoding) and Consent’s legal-permission revocation, the Article 21 obligation is satisfied. - ePrivacy Directive (2002/58/EC, as amended) — consent and preference requirements for electronic communications in the EU. Cookie consent and marketing communication preferences fall under this directive’s surface; the Preference atom’s record is the artifact preferences-page UIs produce.
- CASL (Canadian Anti-Spam Legislation) — analogous to CAN-SPAM with stricter consent requirements; honoring per-principal channel preferences is the mechanism.
- Daniel Jackson, The Essence of Software — freestanding-atom posture;
channel_preferences,frequency_limit,quiet_hours, andformatas opaque deployment-vocabulary fields. - Eiffel’s design-by-contract — preconditions on
set,suspend,delete; named rejection reasons.
Status
grounded on Final Critique 5 — 2026-05-29. Foundation passes and one author-conducted refinement round (initial drafting session); fresh-reader AI Phase 3 round (2026-05-25); first Opus Phase 4 Happy Torvalds X2 gate (2026-05-25) surfaced 13 findings (3 foundational, 9 refining, 1 rhetorical), all closed in-pattern. The 2026-05-29 round ran a fresh-reader Phase 3 (claude-sonnet, all three passes) in parallel with a fresh-reader Opus Happy Torvalds X2 clearance gate (claude-opus), each given the spec body only (Status and Lineage withheld). The Opus gate returned at-or-above the 95%-good threshold with zero foundational findings — the grounding-determinative result; the round’s refining and rhetorical findings (17 refining, 1 rhetorical, consolidated across both reviewers) were closed in-pattern. Retro-mapping to the CLAUDE.md round-structure convention: the first Opus gate (2026-05-25, 3 foundational, did not close clean) is Final Critique 4; the 2026-05-29 clean gate is Final Critique 5 — the canonical grounding marker. Third entry in atoms/messaging/.
Lineage notes
This atom is the third entry in the messaging/ category, drafted after Subscription, Notification, and Notification Fanout had grounded. The conceptual minefield — distinguishing Preference from Subscription and from Consent — was the load-bearing authoring challenge; the resolution lives in the Intent paragraphs and the Behavior bullets that name what this atom does not consult or evaluate. Lineage notes name Subscription and Consent by reference where defending the boundary; the specification body uses neutral language (the topic-subscription concept, the legal-permission concept) per the freestanding discipline.
Regulated-pattern conventions — Regulated adversarial scenarios and Generation acceptance — inherited from the methodology directly (PRESSURE_TESTING.md), baked in from the first draft because the atom’s standards-anchored examples invoke regulated marketing-communication domains (CAN-SPAM, TCPA, GDPR Article 21).
Pass 1 — Structural completeness (GRID). Three findings, all closed in-pattern.
-
State node initially had only Active and Deleted, missing Suspended as a first-class state. The early draft treated Suspended as a transient “Active-with-suppressed-delivery” — losing the load-bearing distinction from “Active-with-empty-channels”. Fixed: Suspended elevated to first-class state with its own
suspended_attimestamp, its own transitions in and out, and its own Decision-point treatment. -
Decision points did not name what triggers
invalid-requestonsetspecifically. The early draft said “invalid-request for malformed inputs” without enumerating the cases — empty principal_ref, empty preference field set, undeclared channel reference, undeclared per-channel-preference-value shape. Fixed: Decision point atsetnow enumerates the four cases that produceinvalid-request. -
Feedback section did not state falsifiable post-conditions for
setandsuspend. The early draft listed observable changes but did not give the “after action, query X must return Y” form. Fixed: Feedback now names falsifiable post-conditions for each successful action.
Pass 2 — Conceptual independence (EOS). Five extraction candidates evaluated; four kept in-pattern, one resolved by changing the action surface.
-
Suspended state — extraction candidate. Could “suspended” be its own atom (a delivery-suppression concept) composed with this atom? Evaluated: a delivery-suppression atom would need its own identity, its own lifecycle, its own actions — and would couple back to this atom via the suspension-target relationship. The suspension is of this record; it does not exist independently. Kept in-pattern as a first-class state of the preference record.
-
frequency_limitandquiet_hours— extraction candidates. Rate-limiting and time-windowing recur across many domains (auth retry caps, API rate limits, scheduling windows). Could they be separate atoms? Evaluated: rate-limiting recurs as a class of concern, but the values stored here are deployment-vocabulary parameters with no state machine of their own and no lifecycle independent of the preference record. They are values the record carries, not concepts composed in. Kept in-pattern. Defended in-line in Behavior with the four-step rubric. -
Channel set declaration — extraction candidate. Could the declared channel set be a separate atom (a “Channel Registry”)? Evaluated: the channel set is deployment configuration, not a state-bearing concept — channels are added or removed by deployment-level operations outside any atom’s surface. Kept as a Store-instance-model concern, parallel to Consent’s
store_namediscipline. A Channel Registry atom is not justified by present library evidence; if the need surfaces (channel governance becomes a domain in its own right), the extraction is straightforward. -
Resume action — extraction candidate resolved as removal. The early draft had a
resume(preference_id)action that returned a Suspended record to Active. Evaluated: a resume action introduces mutability ofstatusafter the first transition, weakening Invariant 1’s audit story (either a third lifecycle timestampresumed_atwould be needed, orsuspended_atwould be overwritten — both complications). The same operational outcome is achieved bysetwith the prior record’s values (composing system reads suspended record, replays values), at the cost of one extra record in history. Resolved:resumeremoved from the action surface; the supersession-via-set path is the canonical resume mechanism. Defended in-line in Edge cases. -
Per-topic preference overrides — extraction candidate. A principal who wants different preferences per topic class (e.g., real-time push for security alerts, daily digest for newsletters) seems like a richer model. Evaluated: per-topic overrides could be implemented as separate preference instances per topic-class with the composing fanout pattern selecting the right instance — that is, the per-topic-ness is composition surface, not atom surface. Kept the atom as one-record-per-principal; named the per-topic concern in Edge cases as a composing-layer extension.
Pass 3 — Adversarial scrutiny (Linus mode). Seven foundation findings, all closed in-pattern.
-
Boundary with Subscription not defended in Behavior. The Intent named the distinction but Behavior was silent on what Subscription does that this atom does not. A reader could plausibly read the atom as a superset that “knows about” subscriptions. Fixed: Behavior bullet added stating the atom does not consult or evaluate topic subscriptions; the composing fanout layer sequences subscription-evaluation alongside preference-reading.
-
Boundary with Consent not defended in Behavior. Same defect class: Intent named the distinction but Behavior was silent. Fixed: Behavior bullet added stating the atom does not consult or evaluate legal permission; the composing layer sequences permission-evaluation before reading preferences. The atom’s commitment is conditional.
-
Suspended-vs-empty-channels distinction not defended in Behavior. The Intent named the distinction but a reader who skimmed to Behavior to find the operational rule could miss it. Fixed: Behavior bullet added carrying the four-step rubric defense — Suspended preserves preference values; empty channels does not; suspending is cheap to reverse and audit-distinct from preference-modification.
-
Updates-not-retroactive claim was implicit, not defended. The Intent stated it but no Behavior bullet defended the model — a reader could plausibly believe the atom does push updates into in-flight work. Fixed: Behavior bullet added with the four-step rubric defense — the atom does not modify prior records on
set; queue-time-capture is the expected fanout policy; the atom records sufficient timestamps for either queue-time or delivery-time policies. -
Examples were happy-path-only initially. Onboarding, vacation-suspend, account-closure were all valid happy-path-or-natural-flow examples; there was no explicit rejection-path example. Fixed: three rejection-path examples added (set with no preference fields, set with undeclared channel, suspend a Deleted record).
-
Atomicity and crash semantics absent. Each transition changes two fields simultaneously;
setwith supersession changes two records. A partial write violates Invariant 1, 4, or 10. Personal Todo, Subscription, and Notification all name this explicitly. Fixed: Edge case added covering each action’s atomicity boundary. -
Regulated adversarial scenarios and generation acceptance missing from initial draft. The standards-anchored examples invoke regulated marketing-communication domains (CAN-SPAM, TCPA); library rules require both sections for any pattern whose examples invoke regulated contexts. Fixed: both sections added, with three adversarial scenarios covering regulator audit (CAN-SPAM opt-out), disputed delivery (TCPA quiet hours), and breach investigation (store corruption).
Refinement round 1 — eight additional findings, all closed in-pattern.
-
Invariant 1 listed
metadataand other optional fields without distinguishing “supplied” from “absent”. A reader could infer that an absentmetadatafield is a defect rather than a normal state. Fixed: Invariant 1 now distinguishes immutable fields from each-supplied-field; fields not supplied atsetremain absent for the record’s lifetime. -
Invariant 6 (suspension preserves values) read as redundant with Invariant 1 (immutability). Both said the preference fields don’t change. Distinguished: Invariant 6 reframed as a transition-specific commitment — the
suspendtransition changes onlystatusandsuspended_at, no other field — which is structurally distinct from Invariant 1’s general immutability claim. Invariant 6 carries the structural mechanism behind the cheap-resumption property. -
setrejection priority unspecified for multi-condition failures. When asetcall violates multiple preconditions (empty principal_ref AND undeclared channel), the spec did not say which rejection wins. Fixed: Decision point atsetnotes that all four cases produceinvalid-request; the implementation chooses which to report first in telemetry, but the rejection code is invariant — callers branch on the code, not the priority. -
Consent boundary in Behavior did not say “does not override”. The original wording said the atom does not consult or check legal permission; it did not explicitly say it does not override. A reader could plausibly believe the atom’s currently-in-effect record overrides legal permission. Fixed: Behavior bullet expanded to state the atom does not consult, re-check, or override legal permission; revocation of legal permission makes the preference record operationally irrelevant; the composing system is responsible for sequencing; a composing system that reads preferences without first re-checking permission has made a sequencing error.
-
GDPR Article 21 missing from Standards references. Article 21(2) gives the data subject the right to object to direct marketing — the atom’s channel-level opt-out is the structural mechanism. Initial draft cited only Article 7(3) for the “easy update” angle, missing the substantive Article 21 right. Fixed: Article 21(2) added to Standards references with a one-line description of how the atom’s records and the composing fanout pattern’s enforcement together satisfy the right.
-
Deletion-reason distinction (supersession vs. explicit) not named as out-of-scope. The audit story relies on the distinction being recoverable from successor records, but this was not made explicit. A reader could believe the atom records a deletion reason directly. Fixed: Edge case added naming the distinction as recoverable-via-successor; deployments needing first-class deletion-reason recording compose with Actor Identity or Audit Trail.
-
Absent preference fields’ meaning unclear in Behavior. A record with
channel_preferencessupplied butfrequency_limitabsent: does that signal no-cap, or default-cap, or what? The early draft was silent. Fixed: Behavior bullet added stating absent fields signal no-preference (the composing fanout pattern applies its deployment default for the absent dimension); this is structurally distinct from an explicit opt-out value. -
Initial Invariant 8 (Id stability) redundant with Invariant 1. Invariant 1 already covered immutability of
preference_id; the separate Invariant 8 added no distinct claim. Same defect class as Subscription’s Pass 3 finding 1 (“Invariant 1 and 4 redundant”). Fixed: Invariant 8 removed; subsequent invariants renumbered. No-id-reuse (general claim, system-wide uniqueness) remains as the renumbered Invariant 8.
Pass 1 / Pass 2 reruns after Refinement round 1: clean. No new structural or extraction findings surfaced; the Pass 3 refinements were prose-and-precision tightenings that did not introduce new GRID nodes, new actions, or new concerns.
Library-wide concerns surfaced but not resolved in this round — recorded here for the next sweep:
-
The “currently-in-effect” concept vs. “Active” alone. This atom introduces currently-in-effect as a derived state (
status ∈ {active, suspended}) for the at-most-one rule. Subscription has a single Active state for the same purpose. The library may benefit from canonicalizing the term and its predicate form for use across atoms with multi-state currency rules; the canonical statement belongs in a shared document. -
Channel-set-declared-at-instantiation as a recurring pattern. Several atoms now have deployment-declared vocabulary at instance-creation time (Consent’s
store_name, Permissions’ scope vocabulary, this atom’s declared channels). A canonical convention for declaring, evolving, and auditing these vocabularies would reduce per-atom drift. Belongs inPRESSURE_TESTING.mdorCONTRIBUTING.md. -
Conditional-atom-commitment framing. Both Preference’s Consent-and-Subscription boundary and Notification’s “doesn’t enforce who may call create” framing express the same structural posture: the atom’s contribution is conditional on the composing layer having done X before calling. A canonical “conditional-commitment” framing in the methodology would let future atoms declare their conditional posture without re-deriving the defense in-pattern.
Fresh-reader AI-conducted Phase 3 round — 2026-05-25 (claude-sonnet-4-6). All three passes run with fresh-reader discipline (no prior-round findings provided before Pass 3). Pass 1 clean; Pass 2 clean. Pass 3 surfaced 10 findings: 2 foundational and 8 refining. All 10 closed in-pattern in the same session. Conventions inherited from the methodology directly.
-
F-P3-A — GDPR Article 7(3) citation scope — refining. Standards reference attributed Article 7(3) (consent withdrawal) to this atom’s
setaction. Article 7(3) is Consent’s obligation; Preference’s obligation is preference-update ease. Fixed: citation narrowed to the analogous preference-update ease principle; note added that Article 7(3) proper belongs to the Consent atom. -
F-P3-B — GDPR Article 21(2) overstated — refining. Standards reference described the atom’s channel opt-out as “the mechanism for honoring an Article 21 objection,” which implies sole sufficiency. Full Article 21 compliance also requires Consent to revoke legal permission. Fixed: reference rewritten to describe this atom’s contribution as the durable opt-out-signal record, and to name Consent’s legal-permission revocation as the parallel obligation.
-
F-P3-C — Behavioral inconsistency: late
deleteafter successfulsuspend— foundational. Behavior section’s concurrent-calls paragraph stated that a latedeletefollowing a successfulsuspendreturnsalready-deleted. Decision points specify that Suspended → Deleted is a valid transition returningok. These directly contradicted each other. Fixed: concurrent-calls paragraph corrected to state that ifsuspendis serialized first (record → Suspended), the latedeletereturnsok. -
F-P3-D — Supersession atomicity tolerance undefined — refining. Generation acceptance check 4 used “within clock-precision” and “materially distinct” without definition. Fixed: check 4 now requires the deployment to declare its clock tolerance (e.g., transaction timestamp vs. wall-time-at-write, and the expected maximum gap); the declared tolerance is the reference bound for auditors.
-
F-P3-E — False disjunction in no-
resumedefense — refining. Edge cases defense stated a resume action would “require a third lifecycle timestampresumed_atand complicate the audit story or would overwritesuspended_atand violate immutability.” The second horn was false — aresumeaction would addresumed_at, not overwritesuspended_at. Fixed: false horn removed; defense rewritten around the actual reasons: reverse status transition breaks Invariant 2’s monotonicity, and repeated suspend/resume cycles on a single record complicate the audit trail. -
F-P3-F — Channel-set history not guaranteed in store — foundational. Generation acceptance check 5 required the auditor to verify channel-set membership “from the records alone,” but the declared channel set was described as deployment configuration — not necessarily a store record. Fixed: Store instance model updated to commit to persisting the declared channel set as an instance configuration record in the store at creation time and on each update. Invariant 5 and Generation acceptance check 5 updated to reference the instance configuration record as the authoritative, store-resident source.
-
F-P3-G —
principal_reflength cap unstated — refining. Inputs section did not state a length cap forprincipal_ref. Fixed: explicit disclaimer added that no length cap is enforced by the atom and the deploying system is responsible for boundingprincipal_refsize. -
F-P3-H — “Malformed” undefined for opaque identifiers — refining. Decision points for
readandcurrent_forused “malformed” as a category for identifiers without defining what “malformed” means for opaque values. Fixed: “malformed” removed; language revised to describe the result as returningnot-known/nonefor any value (empty or otherwise) that matches no record, with an explicit note that no format-validation step exists for opaque identifiers. -
F-P3-I — Invariant 10 misclassified — refining. Invariant 10’s timestamp-ordering inequalities were labeled as invariants but acknowledged as best-effort under non-monotonic clocks — a property that can be violated under achievable deployment conditions is not an invariant under the methodology’s definition. Fixed: relabeled “Temporal property 10” with a parenthetical distinguishing it from Invariants 1–9, which are unconditional.
-
F-P3-J — Deletion-reason recovery overstated — refining. Edge cases claimed the supersession-vs-explicit-deletion distinction is “recoverable from the records” via successor-existence check. The check is only approximately discriminating — explicit-delete-then-recreate produces the same structure as supersession, distinguishable only by timestamp gap. Fixed: rewritten to describe the recovery as a timestamp-gap heuristic approximation, name the discriminating signal (gap relative to the deployment’s declared clock tolerance), and distinguish from definitive recovery.
Pass 1 / Pass 2 reruns after Phase 3 round: clean. The F-P3-F fix (adding the instance configuration record to the Store instance model) introduces a new store artifact. Pass 1 re-check confirms: the new artifact is referenced in Invariant 5, Generation acceptance check 5, and the Store instance model; the reference graph is intact. No new concerns were introduced that Pass 2 would extract. No new GRID nodes were opened that Pass 3 would find.
Opus clearance gate — Happy Torvalds X2 — 2026-05-25 (claude-opus-4-7, fresh-reader). All three passes run with fresh-reader discipline (no prior-round findings provided before Pass 3). Pass 1 clean; Pass 2 clean. Pass 3 at X2 surfaced 13 findings: 3 foundational, 9 refining, 1 rhetorical. All 13 closed in-pattern in the same session. Conventions inherited from the methodology directly. The two foundational clusters were both side effects of the F-P3-F instance-configuration-record fix from the prior round — the new store artifact was added to the audit surface without a complete contract — plus a check-4 circularity inherited from F-P3-D. Status remains partially resolved pending the next Phase 4 round confirming the fixes hold.
-
F-P3-1 — Instance configuration record integrity invariants missing — foundational. Invariant 5 and Generation acceptance check 5 treated the configuration record as the audit-surface source-of-truth but no invariant guaranteed its immutability or durability; a silent post-hoc rewrite would let an undeclared channel retroactively pass check 5. Fixed: new Invariant 10 added covering configuration record immutability and durability, paralleling Invariants 1 and 9 for preference records. Existing Temporal property 10 renumbered to Temporal property 11; the two prior cross-references (Atomicity edge case, CAN-SPAM scenario, plus Clock semantics edge case and Generation acceptance check 2) updated.
-
F-P3-2 — Configuration record write surface unspecified — foundational. The atom’s action surface did not include configuration-record creation; the audit story depended on configuration records existing in well-formed, ordered form with no specified write surface, atomicity, or rejection rules. Fixed: Store instance model expanded with the deployment-owned write contract (append-only, durable, bootstrap-ordered), Invariant 10 binds the first two obligations, Invariant 5 enforces the third indirectly via
set-time membership; new Edge case bullet “Configuration record management is a deployment obligation” names the contract and the path to positive enforcement via Audit Trail / Actor Identity composition. -
F-P3-3 — Generation acceptance check 4 qualifier was circular — foundational. Check 4’s “unaccounted for by explicit
deleteorsuspend” qualifier depended on the same timestamp-gap heuristic the Edge case on deletion-reason recovery explicitly disclaimed as non-deterministic; a real supersession-atomicity violation (large gap from a non-atomic write) would be misattributed to explicit delete and excluded from the check. Fixed: check 4 rewritten to drop the qualifier and frame any out-of-tolerance gap as ambiguous-pending-external-evidence; deterministic discrimination between explicit-delete and atomicity-violation requires composition with Audit Trail or Actor Identity, named explicitly in the check. -
F-P3-4 —
setDecision point count mismatch — refining. “All four rejection cases” claimed a count that did not match the three enumerated cases (emptyprincipal_ref, no preference field supplied, undeclared channel key); the stale fourth case (“undeclared per-channel-preference-value shape”) was eliminated by the opaque-value posture earlier in the section. Fixed: count corrected to three with the cases enumerated inline. -
F-P3-5 — Required host serialization model unnamed — refining. Invariants 3 and 4 depended on “host serialization guarantees” without naming the isolation level required; snapshot isolation could allow two concurrent
setcalls for the same principal to both observe no-currently-in-effect and commit, violating Invariant 3. Fixed: Atomicity edge case now names the minimum required model (linearizable writes perprincipal_ref, or a serializable transaction wrapping each operation) and the failure mode under weaker isolation. -
F-P3-6 — Read-authorization posture silent — refining. The capability-based authorization Behavior bullet covered writes (
set,suspend,delete) but was silent on reads (current_for,read); composing systems handling privacy-sensitive preference data could not infer the read posture. Fixed: bullet renamed “Authorization is capability-based across writes and reads alike” and extended to state the read posture explicitly (same capability gating onprincipal_ref/preference_id, no role check, no per-action authorization). -
F-P3-7 — Clock ownership ambiguous — refining. Inputs section said “an implicit clock providing wall-time timestamps” without saying whether the clock was atom-owned or caller-provided; caller-provided timestamps would allow lying and break the clock-tolerance disclosure mechanism’s auditability. Fixed: Inputs section now states timestamps are atom-owned (
set_at,suspended_at,deleted_atread from the atom’s clock at write time, never caller-supplied), binding the audit story to a single clock the deployment can characterize. -
F-P3-8 — Length-cap disclaimer asymmetric across opaque inputs — refining. Inputs section disclaimed length-cap responsibility for
principal_refonly; per-channel preference values,frequency_limit,quiet_hours,format, andmetadatacarry the same unbounded-size risk with the same delegation-to-deployer answer. Fixed: disclaimer moved into a single “Opaque-input bounds” paragraph covering all opaque inputs and stating the cap (or the choice to leave it unbounded) is deployment policy disclosed alongside fanout-on-no-record and clock-tolerance. -
F-P3-9 — Terminology drift in regulator-audit scenario — refining. The CAN-SPAM scenario said “
pref_201Active withemail: "preferred"from2025-08-01to2026-03-14,” butactiveis a status state that does not span time intervals; “currently-in-effect” is the temporal predicate the spec uses elsewhere. Fixed: scenario rewritten using “currently-in-effect from X to Y” with the correspondingset_atanddeleted_atvalues made explicit. The library-wide currently-in-effect-vs-Active concern flagged earlier in Lineage applies here directly; the per-atom fix lands the discipline locally pending the canonical convention. -
F-P3-10 —
deleteaction vs. “deleted from the store” terminology overload — rhetorical. The Behavior durability bullet used “deleted” in the sense of “removed,” distinct from thedeleteaction / Deleted state used everywhere else. Fixed: bullet rewritten as “No preference record is removed from the store” with a clarifying sentence that thedeleteaction moves a record into the Deleted state without removing it from storage. -
F-P3-11 — Reconstruction boundary convention unstated — refining. Generation acceptance check 2’s formula used
deleted_at > t(half-open interval excluding the deletion instant) without naming the convention; an auditor evaluatingt == deleted_atexactly could compute a different answer than expected. Fixed: half-open interval[set_at, deleted_at)convention noted adjacent to the formula, with the supersession-boundary case explained (t == deleted_at == successor.set_atmakes the successor currently-in-effect). -
F-P3-12 — Breach-scenario “cannot have been altered” overclaim before the qualifier — refining. The breach scenario asserted “any record created before the breach window cannot have been altered (immutability holds spec-level)” several sentences before the Tamper Evidence qualifier landed; the initial sentence risked reading as a stronger claim than the spec actually supports. Fixed: claim softened to “should not have been altered” and the Tamper Evidence qualifier brought adjacent, with the records-alone forensic reconstruction (which records existed when, which transitioned within the window) named separately from the cryptographic-integrity claim.
-
F-P3-13 — Article 21 standards reference assumed a fixed opt-out vocabulary — refining. The reference described “a per-channel opt-out value in
channel_preferences” but per-channel preference values are opaque deployment vocabulary throughout the spec; the atom does not define an “opt-out” sentinel. Fixed: reference rewritten to acknowledge the encoding dependency (the deployment’s vocabulary fixes the encoding; the atom stores the value opaquely) and to credit the composing fanout pattern with interpreting the encoding at delivery time.
Pass 1 / Pass 2 reruns after Phase 4 round. Pass 1: the new Invariant 10 is referenced from the Store instance model, the new “Configuration record management is a deployment obligation” Edge case, the rewritten Generation acceptance check 4, and the new Phase 4 Lineage entry; Temporal property 11 (formerly Invariant 10) cross-references updated in the Atomicity edge case, the Clock semantics edge case, the CAN-SPAM scenario, and Generation acceptance check 2. Reference graph intact; no orphaned references. Pass 2: the configuration-record contract clarifications keep the artifact in-pattern (no new extraction candidate surfaces) and the host-serialization disclosure stays in-pattern (the atom is the immediate consumer of the guarantee); the library-wide concerns already flagged (currently-in-effect canonicalization, channel-set-declared-at-instantiation, conditional-atom-commitment) are reinforced rather than newly opened. Both reruns clean.
Library-wide concerns reinforced by this round — recorded here for the next sweep:
-
Configuration-record-shaped store artifacts as a recurring pattern. This round’s fix (Invariant 10 + the deployment-write contract) is the second instance of the same shape — the first was Consent’s
store_namediscipline, and Permissions’ scope vocabulary is the same shape under a different name. A canonical “deployment-owned store-resident vocabulary record” convention would foreclose per-atom drift in how integrity invariants are stated and how the deployment’s write contract is named. Belongs inPRESSURE_TESTING.md’s methodology section orSPEC_FORMAT.md’s atom-shape extras. -
Host serialization disclosure as a recurring obligation. This round added explicit serialization-model naming to the Atomicity edge case. Several atoms (Subscription, Notification, Personal Todo) defer concurrency to “host serialization guarantees” without naming the required model. A canonical disclosure convention — name the minimum isolation level required; name the failure mode under weaker isolation — would let composing patterns reason about cross-atom composition under shared host guarantees. Belongs in
PRESSURE_TESTING.md.
Deferred work — formal models. Phase 4 X2 review flagged this atom as a strong candidate for formal-model siblings (per PRESSURE_TESTING.md §Formal models). The highest-value artifact is a TLA+ model of supersession atomicity (Invariant 4) under interleaved concurrent operations, which would verify the new linearizable-per-principal_ref requirement and either confirm or refute the check-4 indistinguishability claim (atomic supersession vs. explicit-delete-then-reset). The natural second artifact is an Alloy model of the records relation (preference records + configuration records, wired through Invariants 5 and 10 plus bootstrap-ordering), following the Attributed Permissions Admin pattern (static structural + dynamic Alloy 6 LTL). Deferred past the grant deadline; pick up after the deadline window closes.
Final Critique 5 — fresh-reader Phase 3 + Opus Happy Torvalds X2 clearance gate — 2026-05-29. Two fresh-reader reviews run in parallel with genuinely different priors, each given the spec body only (Intent through Standards references; Status and Lineage withheld to preserve fresh-reader discipline) plus the full pass question sets: a Phase 3 round (claude-sonnet, all three passes) and the Opus Happy Torvalds X2 clearance gate (claude-opus). Pass 1 clean and Pass 2 clean on both reviewers. The Opus gate returned GATE CLEAN FOR GROUNDING — zero foundational findings (the grounding-determinative result under the 95%-good threshold); the sonnet Phase 3 surfaced 12 findings (1 GRID, 1 EOS, 8 Linus, 2 regulated-check), which on adjudication against the contradiction-not-preference test and the foundational/refining/rhetorical taxonomy were all refining or rhetorical and heavily overlapped the Opus set. Consolidated across both reviewers: 17 refining, 1 rhetorical, 0 foundational — all closed in-pattern in the same session. The gate explicitly confirmed the spec anticipated its hardest adversarial reads (whitespace principal_ref disclaimed-by-design; enumeration-by-principal resolved by the audit-surface/action-surface split; bootstrap-ordering enforced indirectly via Invariant 5; the breach scenario’s cryptographic limit honestly routed to Tamper Evidence). Conventions inherited from the methodology directly. This is the clean Final Critique rerun the prior partially resolved Status was pending; the atom grounds on Final Critique 5.
-
F-FC5-1 — Generation acceptance checks unnumbered — refining. Cross-references (“check 2”, “check 4”, “check 5”, “check 6”) relied on the reader counting bullets. Fixed: the six checks are now explicitly numbered Check 1–Check 6.
-
F-FC5-2 — Invariant 4 asserted a timestamp equality unconditionally — refining. Invariant 4 (a hard, unconditional invariant) carried “the prior record’s
deleted_atequals (or is within clock-precision of) the new record’sset_at,” a timestamp claim that can be violated under non-monotonic clocks. Fixed: the timestamp clause relocated to Temporal property 11 (best-effort); Invariant 4 now asserts only the atomic co-occurrence of the two state changes. -
F-FC5-3 — Cross-record temporal ordering not surfaced in Temporal property 11 — refining. The supersession-pair ordering (
R_prior.deleted_at ≈ R_next.set_at) lived only inside Invariant 4, splitting the temporal model across two sections. Fixed: added to Temporal property 11 as the cross-record counterpart, unifying the model in one place (cross-referencing Check 4’s clock tolerance). -
F-FC5-4 —
principal_refequality/normalization unstated — refining. The spec said the atom does not normalizeprincipal_refbut never stated equality semantics, on which Invariant 3 depends. Fixed: Inputs now states equality is exact with no normalization, and the deployment must canonicalize values before passing them or two references for the same principal will be treated as distinct. -
F-FC5-5 — Empty
channel_preferencesmap unaddressed by the at-least-one rule — refining.setrequires at least one preference field, but whether an emptychannel_preferencesmap ({}) counts as “supplied” was undefined. Fixed:setDecision point now treats{}as not-supplied; asetwhose only field is an empty map isinvalid-request. -
F-FC5-6 —
metadatanot flagged as excluded from the at-least-one rule in the Inputs list — refining. A generator reading the Inputs list in isolation could treat ametadata-onlysetas valid. Fixed: themetadataInput bullet now states it does not by itself satisfy the at-least-one-preference-field requirement. -
F-FC5-7 — Clock framing not Logic-Confinement-explicit — refining. The “atom-owned clock, read at write time” framing (from prior finding F-P3-7) read as an internal clock read, in tension with the Logic Confinement Principle’s inject-don’t-read rule. Fixed: Inputs clock bullet now frames the clock as host-injected at the atom’s single I/O seam — received by the pure core, not read internally, not caller-supplied — reconciling the audit-soundness goal with logic confinement.
-
F-FC5-8 —
current_forlinearizability vs concurrentsetunstated — refining. The concurrency treatment covered concurrent writes but not acurrent_forread concurrent with aset. Fixed: concurrent-calls Behavior bullet now statescurrent_forobserves the serialization order (prior or new record, never torn) and that a just-superseded read is acceptable under the queue-time-capture fanout policy. -
F-FC5-9 — Serialization domain ambiguous (id-keyed vs principal-keyed) — refining.
suspend/deletetake apreference_idwhilesettakes aprincipal_ref; the required serialization domain was stated only “perprincipal_ref,” leaving the cross-action case implicit. Fixed: Atomicity edge case now states the serialization domain is the principal — all operations touching any record of a given principal serialize against one another. -
F-FC5-10 — Crash-recovery conformance unstated — refining. The spec said recovery semantics are undefined without saying whether a post-crash standing invariant violation is conformant. Fixed: Atomicity edge case now states atomicity under normal operation is a conformance requirement and the post-recovery store must not exhibit a standing Invariant 3/4 violation, while post-crash reconciliation mechanics remain the implementor’s concern.
-
F-FC5-11 — Invariant 10 atom-enforced vs deployment-contract not distinguished — refining. Invariant 10 asserted configuration-record integrity but the atom’s action surface does not write configuration records. Fixed: Invariant 10 now states it is a deployment-contract property — asserted as required of any conformant store, not runtime-enforced by the atom (unlike Invariants 1–9) — with positive enforcement via Audit Trail / Actor Identity composition.
-
F-FC5-12 —
suspendnot-activeconflates Suspended and Deleted — refining. A caller receivingnot-activecould not tell whether the record was Suspended or Deleted. Fixed: Decision point now notes the single code covers both states (a Suspended record never returns to Active) and that callers disambiguate viaread. -
F-FC5-13 — Bootstrap-ordering had no acceptance check — refining. The deployment’s bootstrap-ordering obligation is auditable from the store but no Generation acceptance check named it. Fixed: Check 5 now notes that confirming channel-set membership for every record also confirms bootstrap-ordering (no preference record predates all configuration records).
-
F-FC5-14 — Disputed-delivery scenario presumed the in-effect
preference_id— refining. The TCPA scenario jumped toread(pref_id_at_time_of_delivery)as if the id were known, skipping the reconstruction step. Fixed: the scenario now reconstructs the in-effect record via Check 2 (enumerate by principal, maxset_at ≤ t) before callingread. -
F-FC5-15 — Account-closure DSAR implied a chain of
readcalls with ids from nowhere — refining. The example described querying eachpreference_idviareadwithout saying where the id list came from. Fixed: reframed as an audit-surface enumeration filtering the store onprincipal_ref(Check 1), withread’s per-id content noted as equivalent. -
F-FC5-16 — Examples framed as “three domains” but trace one principal — refining. The intro said “the same atom, three domains” while the three lifecycle scenarios actually chain one principal’s records (
pref_001 → pref_088 → pref_141). Fixed: intro reframed as one principal’s lifecycle across operational contexts; rejection-path and regulated scenarios noted as using their own ids. -
F-FC5-17 — Missing
delete(already-deleted)rejection example — refining. Thealready-deletedrejection had no worked example though the other rejections did. Fixed: added adelete(pref_141) → rejected(already-deleted)example, contrasted with the valid Suspended → Deleteddelete. -
F-FC5-18 — “The atom records the transition order” overclaim — rhetorical. Order is witnessed by timestamps, which Temporal property 11 declares best-effort; “records the transition order” overstated the artifact. Fixed: softened to “the host serializes the transitions; the recorded timestamps witness that order subject to Temporal property 11’s clock caveat.”
Adjudicated, not actioned this round. The sonnet review’s RC-2 (Generation acceptance Check 4 depends on an externally-disclosed clock tolerance not resident in the store) was considered and classified refining-deferred rather than closed: the clock tolerance is already named as a required deployment disclosure grouped with the fanout-on-no-record disclosure, so Check 4 is records-plus-one-disclosure rather than fully records-internal. Promoting the clock-tolerance value into the instance configuration record (making Check 4 self-contained) is a worthwhile enhancement but touches the configuration-record schema, Invariant 10, and the Store instance model; it is deferred to a future round rather than folded into the grounding round, since the Opus gate found Check 4 adequate and non-circular as written.
Pass 1 / Pass 2 reruns after Final Critique 5. Pass 1: the new Check numbering, the Temporal-property-11 cross-record clause, the relocated Invariant 4 clause, the deployment-contract clause on Invariant 10, and the new delete(already-deleted) example are all referenced and resolved against the reference graph; no orphaned references introduced. Pass 2: every fix sharpened existing content or tightened a disclosure boundary; no new concern was opened that would extract to a separate atom, and the configuration-record and host-serialization clarifications keep their concerns in-pattern (the atom remains the immediate consumer of both). Both reruns clean. Library-wide concerns previously flagged (currently-in-effect canonicalization; deployment-owned store-resident vocabulary records; host-serialization-disclosure convention) are reinforced, not newly opened.