Supporting reference · Working design and evidence, not a promise of complete support.

On this page

References and relationship edges

Status: Evidence-backed direction. Target-independent Association semantics and bounded Reference and Association lowering are implemented. Broader Association shapes and every Tree lowering remain open. The Rails Associations page states the admitted shapes and their code and checks.

Current answer

A Reference is one semantic relationship edge owned by the Entity that stores the logical foreign-key slot. It is not an ordinary Field and it is not a Rails association declaration. An Association is every named traversal over a Reference or over other Associations, including the forward belongs_to-style traversal.

This split keeps edge facts in one place even when generated code exposes several accessors, scoped accessors, or no accessor on one side. The first opinionated Foundation always needs one conventional same-key, unqualified referencing-side Association for every Reference, so the current format does not ask the author to repeat it. The live Project graph materializes that traversal and keeps it synchronized with its Reference. Other direct and indirect Associations remain authored because their names, predicates, cardinality, or traversal paths are product meaning.

Start with two edges to the same target

Dunbar150's Follow Entity contains two References to Profile:

[
  {
    "subject_uuid": "019f9425-5412-77b3-87b8-ae7a27d42ec5",
    "key": "follower",
    "name": "Follower",
    "required": true,
    "on_referenced_deleted": "delete_referencing_record",
    "targets": ["profile"]
  },
  {
    "subject_uuid": "019f9425-5412-7112-8e5c-12cd42e38be6",
    "key": "leader",
    "name": "Leader",
    "required": true,
    "on_referenced_deleted": "delete_referencing_record",
    "targets": ["profile"]
  }
]

Every Reference must include required with an explicit boolean value. notes is Field-only; Reference objects are closed, so adding notes to a Reference is a schema error. Use the required name for its human-facing label.

The target does not identify the edge. follow.follower and follow.leader are different stored facts. A Rails lowering can derive follower_id and leader_id from their local slots.

The follower edge then supports several named traversals:

Association Starting Entity Meaning
follow.follower Follow The Profile stored in the follower slot
profile.outgoing_follows Profile Every Follow whose follower is this Profile
profile.leader_commitments Profile A scoped subset of the same Follows
profile.accepted_outgoing_follows Profile Another scoped subset of the same Follows

The derived forward Association and Reference share follow.follower because paths are resolved in typed namespaces. The Association does not duplicate the stored edge; it is the live graph's typed traversal of that edge.

What owns what

Two kinds of ownership need separate names:

  1. Definition and storage ownership. The referencing Entity contains the Reference definition. Its generated table receives the foreign-key representation.
  2. Traversal ownership. The Entity containing an Association is where that named traversal begins.

The 2021 prototype instead anchored each relationship declaration to a physical foreign-key Column. The relationship recovery audit (repository-only) traces the later move away from that shape. One foreign-key fact can support several direct and indirect accessors without duplicating the edge. The current working direction preserves that lesson:

The authoring service may eventually use relational records that connect References and Fields internally. That persistence choice does not need to leak into the reviewed Foundation Plan.

Readable typed paths and subject identity

The current examples use application-local Entity keys such as profile. Entity-owned definitions have owner-local keys and derive readable paths in the form owner.local_slot, such as follow.follower.

Nesting establishes ownership, but readable paths remain useful because definitions are referenced from other Entities and diagnostics should not depend on array positions. The final path component is the local key for a Reference or Association. It must not be inferred from a relationship target:

The required name is human-facing. It does not determine a generated reader. The first target derives Reference storage and Association readers from each readable local key. Foundation Plan import parses each qualified path once into owner and local-key values. A separate accessor property would duplicate the local key unless the product intentionally allowed the two names to diverge, which the initial format does not.

Paths are also typed. A Reference and its derived forward Association share one readable path: bookmark.movie can identify both the stored edge and Bookmark's movie traversal. A reference locator resolves the first namespace; an association locator resolves the second. Typed properties keep that overlap unambiguous.

The JSON Schema checks local key, UUID, and readable-path syntax. For References and Associations, the implemented document census, candidate slice, live catalog, and Analyzer now confirm that:

Other typed namespaces remain incomplete. The Rails Analyzer must also prove that each generated local reader is distinct in the target model's method namespace, even when the typed semantic paths themselves do not collide.

The current sketch/0.23 format keeps these typed readable paths for links and adds a separate Project-scoped subject_uuid for continuity across accepted complete-document submissions. A rename retains that UUID and changes the key plus every affected readable path in one atomic submission. Active Record stores the resolved relationship through same-Project surrogate foreign keys rather than storing the path as database identity. The identity decision (repository-only) records why UUIDs do not replace readable links.

The current pure Association slice builds all authored and derived candidates before resolving typed Reference, Predicate, through, and source links. It rejects missing links and a readable path collision between an authored Association and a derived forward row. The live-graph Analyzer now checks owner compatibility, indirect cycles, Predicate ownership, and semantic cardinality. Authored aliases are valid target-independent traversals; the Rails Analyzer still needs to prove that each alias can be lowered by the selected profile.

One edge, multiple traversals

A direct Association names one Reference and one starting side:

The containing Entity must match the selected endpoint. For a closed multi-target Reference, a referenced-side Association may begin at any member of the target set. A scoped direct Association remains another traversal over the same edge.

The derived same-key forward Association is the ordinary generated relationship reader. The format also permits authored side: "referencing" aliases when another name or Predicate carries product meaning. The mechanical same-key, unqualified traversal remains reserved for the derived row and omitted from JSON. The current Analyzer proves that an authored alias starts at the Reference owner. The Rails Analyzer still needs to prove profile lowerability. A target-side reader is optional: omitting a referenced-side Association suppresses that accessor without erasing edge requiredness, multiplicity, deletion meaning, or integrity.

The live graph stores authored and derived rows in one Association table because indirect authored paths may name either origin. An authored row claims Association subject identity. A derived row instead correlates to its Reference, carries no Association subject_uuid, and is omitted from serialized Foundation Plan JSON. Supported Reference creation, changes to owner/key/name/position, and deletion create, synchronize, or remove that row. The document-oriented Explorer synthesizes it from the Reference for ERD and Rails-output views. A later AnalysisRun may retain effective bounds or result Entity sets, but not another copy of the forward row.

An indirect Association composes two named Associations:

{
  "subject_uuid": "019f9425-5412-7b4b-9ddd-37e504a13120",
  "key": "bookmarked_movies",
  "kind": "indirect",
  "name": "Bookmarked movies",
  "through": "user.bookmarks",
  "source": "bookmark.movie"
}

Both properties resolve in the Association namespace, so wrapper objects such as { "association": "user.bookmarks" } would be redundant. Shinar composes the same two kinds of traversal:

{
  "subject_uuid": "019f9425-5412-7b6e-a14c-654814dc405f",
  "key": "members",
  "kind": "indirect",
  "name": "Members",
  "through": "message.chat",
  "source": "chat.members"
}

The current Analyzer proves that the first Association begins at the containing Entity, its result owns the source Association, the path does not cycle, and authored bounds agree with composed multiplicity. A rejected Association has no semantic fact, and dependents are blocked without discarding independent facts.

Every Association returns distinct record identities. If several through records reach the same final record, the result contains that record once. Cardinality therefore counts distinct identities, and target lowerings must add deduplication where their native relationship query would otherwise repeat rows.

The semantic format does not inherit every restriction of one target. The Active Record Associations guide allows any non-polymorphic Association as the through step of has_many :through, while has_one :through requires a supported singular, non-polymorphic step. The first Rails profile omits and lists paths that violate those target constraints. That is the current implementation choice, not a rule against emitting a conventional partial Association in pre-alpha while the missing traversal semantics stay in the GapSet. Keeping the path semantic allows another target or a later Rails strategy to support one without changing what the application means.

kind remains explicit even though the presence of reference or through and source could imply the variant. It gives the JSON Schema, authoring API, future persistence, and diagnostics one discriminator for two closed shapes. A direct Association requires reference and side and rejects the indirect keys. An indirect Association requires through plus source and rejects reference and side. Omitting kind is invalid rather than asking the Compiler to infer intent from an incomplete object.

The recovered relationship work considered path as the indirect kind's name. indirect was retained because it is established Rails terminology and describes the traversal relative to the stored Reference. derived was declined because it would collide conceptually with derived Fields.

An Association may name one optional predicate. It qualifies the records returned by the completed direct or indirect traversal, and the Predicate must belong to that result Entity. One pointer is sufficient because a Predicate already owns a recursive Expression: and, or, and not compose tagged nodes, while matches_predicate reuses another Predicate owned by the same Entity.

The bounded importer retains that link for a direct referenced-side Association when both its Reference and Predicate survive service pruning. A locator absent from the submitted document remains a blocking link error. If the named subject existed but was pruned as unsupported, the Association is pruned with an explicit dependency skip at its reference or predicate member. Deterministic serialization reconstructs the exact current Predicate locator from the admitted graph rather than copying the submitted spelling.

An Association does not own an Ordering in the current format. Scaffold collections select a named Ordering where their presentation requires one. This avoids imposing one sequence on every traversal use and avoids pretending the current Rails profile supports the newer fallback default_order behavior.

Filtering an intermediate record is different. Shinar's chat.members follows the already-qualified chat.active_memberships Association and then membership.user; it does not attach membership.active as the final Predicate on User records. Semantic validation needs to resolve the Association's result Entity, confirm that its Predicate belongs there, and reject Predicate cycles.

A many-to-many relationship remains a join Entity with two References plus direct and indirect Associations. It is not one Reference whose two ends both store collections.

When the join has only its two References and no user-facing data or independent lifecycle, author the default detail projection against the indirect destination collection. IDs and ordinary timestamps alone do not change that choice. A Credit's role or an Order Item's quantity makes the relationship itself useful content; retain those attributes and any explicitly selected join controls. This is an authoring choice using existing Associations and Scaffold projections (repository-only), not a Compiler rewrite or a physical-column heuristic. Relationship mutations create or destroy the join record. Destination mutations retain their separate meaning and authorization.

Every Reference carries one nonempty targets array. One member is the ordinary single-target case; two or more members form the complete closed target set. This removes the old single-versus-union object variants without making openness ambiguous. The derived referencing-side Association provides one semantic reader across either current Rails realization. Rails polymorphism can supply it directly; an exclusive arc needs a generated reader and writer over its physical foreign keys. The polymorphic integrity research (repository-only) compares open polymorphism with closed exclusive arcs.

Supplying References during create

Account ownership is a common request-level source rather than a user-selectable relationship. It belongs to the generated create behavior that consumes it, not to the reusable Reference. Oscar Party's Bookmark Scaffold therefore contains this create binding:

{
  "reference": "bookmark.user",
  "value": {
    "kind": "environment",
    "name": "current_account"
  }
}

The derived forward bookmark.user Association exists for traversal. The Bookmark Scaffold does not list that Association among create inputs because the submitting user does not select it.

The first Rails direction applies the binding in the generated create request before authorization and persistence:

@bookmark = Bookmark.new(bookmark_create_params)
@bookmark.user = current_account
authorize! @bookmark, to: :manage?
@bookmark.save

This is not belongs_to :user, default: -> { Current.user }. Jobs, tasks, tests, and direct model callers must supply ownership explicitly. That avoids a hidden dependency on request state and lets authorization inspect the fully initialized record.

Bindings use the same tagged Value algebra as defaults and other value-bearing contexts. Semantic validation proves that the Reference accepts the Value's result type, that an environment path begins at the correct runtime record, and that no input, default, or second binding conflicts with the server-owned assignment.

A direct associated create form supplies its inverse Reference from the authorized route-bound parent. For movie.ratings, Add opens /movies/:movie_id/ratings/new and the form posts to /movies/:movie_id/ratings. The shared Rating handler builds through that Movie's ratings Association. If the inverse is a selected create input, this scoped form omits its control, option query, and strong parameter; other parent and standalone forms keep their remaining inputs. A server binding to that inverse still conflicts with the route-owned assignment.

Decision 0015 (repository-only) records the earlier Scaffold boundary; sketch/0.23 moves request-level assignment into create bindings. Associated creation reuses the target Entity's scaffold.create definition through scoped New and POST routes without requiring a top-level create route.

A forward Association can describe its owning record

An Entity's primary_descriptor may select one of its singular referencing-side Associations. This does not make the physical foreign key a Field. It asks the Compiler to follow the named traversal and the referenced record's own descriptor when emitting a concrete display use. It does not add a model-wide display method.

Dunbar150's Like has no authored Fields. Its descriptor can still be:

{
  "primary_descriptor": {
    "association": "like.target"
  }
}

The closed target set contains Post and Comment, so semantic validation must prove that both Entities have primary descriptors. Descriptor paths may cross several Associations, but a cycle is invalid because it could never produce a final display value. The Association must be an unqualified, referencing-side direct traversal over a required Reference. An optional Reference cannot guarantee a whole-record display value for every valid persisted record. The Rails target writes the resolved reader chain into each generated use site instead of delegating through to_s. When closed targets select different descriptors, that use site contains an explicit branch over the target types rather than assuming a common reader.

Cardinality describes more than one fact

Three independent constraints must remain separate:

Fact Natural owner Question answered
Local requiredness Reference May a referencing record have no target?
Stored-edge multiplicity Reference May several referencing records point to the same target?
Qualified traversal bound Association How many records may this named traversal return after its Predicate?

required already represents the first fact. It can lower to foreign-key nullability for a direct single-target Reference.

The pre-refoundation model represented the second fact as a one_to_many or one_to_one cardinality enum. The current format recovers the durable meaning as an optional positive boolean on the Reference:

{
  "subject_uuid": "019f9425-5412-72c1-84fd-3714217552d7",
  "key": "profile",
  "name": "Profile",
  "required": true,
  "one_to_one": true,
  "on_referenced_deleted": "delete_referencing_record",
  "targets": ["profile"]
}

When one_to_one is true, at most one referencing record may point to each target. The Rails profile needs Reference-bound uniqueness feedback plus a unique physical key. Any authored referenced-side direct Association is singular. Omission or explicit false allows many referencing records to share a target. It does not mean that every target must be referenced.

The property stays on the Reference even when the Plan exposes no target-side accessor. A referenced-side direct Association derives has_one versus has_many from the Reference rather than restating maximum: 1.

The third fact is different. Dunbar150's scoped profile.leader_commitments has a maximum of 150 after applying follow.slot_consuming. That limit belongs to the Association because another traversal over follow.follower intentionally sees a different set.

An unscoped inverse bound can express still more meaning. feed_cursor.profile is one_to_one, while profile.feed_cursor carries only { "minimum": 1 }. The Reference prevents two FeedCursors from pointing to the same Profile. The Association minimum separately says every Profile should have a FeedCursor. A required feed_cursor.profile guarantees only the opposite direction: every FeedCursor has a Profile. Foreign-key nullability cannot prove that every Profile has a FeedCursor.

Conditional uniqueness is not structural multiplicity. Dunbar150 permits only one ordinary FeedEntry per Post, but catch-up FeedEntries may repeat that Post. feed_entry.post therefore remains many-to-one, and the conditional Entity Validation preserves the narrower rule. By contrast, feed_entry.catch_up_item is one-to-one because no two FeedEntries may ever point to the same CatchUpItem.

The evidence-backed direction is therefore to keep stored-edge multiplicity on Reference and retain Association cardinality for qualified collection bounds or inverse-existence rules. Decision 0013 records this serialization choice and the boundary between structural multiplicity and conditional uniqueness.

Deletion has two directions

Use SQL's endpoint names rather than parent and child, which become ambiguous for one-to-one and union-targeted edges:

referencing record -- Reference --> referenced record
Trigger Semantic question Current sketch/0.23 support
Referenced record is deleted What happens to the referencing record or its slot? on_referenced_deleted
Referencing record is deleted What happens to the referenced record? Not represented

The current first direction supports:

These are outcomes. Rails callbacks, database actions, and callback-versus-bulk deletion are target realization mechanisms.

For the Rails target, Decision 0024 (repository-only) now selects Active Record Association options, callbacks, and healthy common gems before database-owned lifecycle behavior. PostgreSQL remains the integrity backstop. A shape that needs a bespoke callback scheduler is a target gap, not a reason to turn the database or Compiler into a second Rails lifecycle engine.

The reverse direction is rarer, but not imaginary. The retained Writebook census (repository-only) observed a delegated-type spine whose deletion destroys its referenced member record. That behavior cannot be reconstructed from on_referenced_deleted.

Both directions belong to the Reference because they remain meaningful when a target-side Association is absent. The sketches briefly carried owned_by_referenced as a separate lifecycle hint. It had no independent generated consequence: deletion still depended on on_referenced_deleted, and Scaffolds, routes, forms, and Policies declared their own behavior. The current format therefore omits the hint. Revisit it only if lifecycle ownership produces a separate recurring output that cannot be derived from those structured consumers. A required Reference also cannot be nullified while its referencing record survives.

The current examples deliberately author on_referenced_deleted for every Reference. Omission must not authorize a destructive result. The deletion decision note (repository-only) records that reversal from the old cascade default.

Closed multi-target References currently state one target-deletion outcome for the whole target set. A future example should decide whether a per-target outcome is ever coherent enough to support.

A Tree names one hierarchy carried by a self-Reference. It may also name another Reference that partitions independent hierarchies.

Dunbar150's complete current declaration is:

{
  "subject_uuid": "019f9425-5412-7b08-aaab-0029b4b29f41",
  "key": "thread",
  "parent_reference": "comment.parent",
  "scope_reference": "comment.post"
}

comment.thread identifies this particular hierarchical interpretation of Comment records. It is not a Field, foreign-key slot, renderer, or promise that every Comment page displays a nested thread.

comment.parent stores the immediate parent. It must be a self-Reference from Comment to Comment. comment.post says that each Post contains an independent Tree and that a Comment's parent must belong to the same Post. A Tree without scope_reference describes one unpartitioned hierarchy for that Entity type.

Naming the Tree adds meaning that a conventional nullable parent_id does not carry by itself:

Movement is not implicit. The parent Reference owns whether its value is immutable; Dunbar150 explicitly marks comment.parent immutable. A later movable-tree design would need writable-edge, cycle, authorization, and position semantics rather than deriving them merely from Tree membership.

The current sketch/0.23 object contains only subject_uuid, key, parent_reference, and optional scope_reference. Cached root storage, descendant counts, materialized paths, closure tables, depth limits, cycle handling for raw bulk writes, tombstone deletion, and nested presentation are not implicit keys.

Dunbar150's hand-authored implementation used an adjacency parent plus a cached root and strong same-Post database constraints, as its comment-tree report (repository-only) records. That is valuable target evidence, not the selected general Rails lowering. The current Rails Capability catalog (repository-only) leaves the Tree lowering open. A popular gem such as closure_tree nominates a mechanism to evaluate; it does not by itself decide that every Tree needs a closure table or that gem configuration belongs in the Plan.

Deletion remains Reference meaning. Removing a node with descendants may restrict, cascade, reparent, or preserve a tombstone, but the current Tree shape does not choose among those outcomes. Dunbar150 leaves its nuanced tombstone behavior outside the Plan rather than disguising it as a generic Tree default.

Before Tree generation, semantic and target validation need to prove:

  1. the Tree key belongs to the containing Entity's Tree namespace;
  2. parent_reference resolves to a self-Reference owned by that Entity;
  3. scope_reference, when present, is owned by the same Entity;
  4. parent selection cannot cross the declared scope;
  5. Reference requiredness and deletion outcomes are coherent with root and descendant behavior;
  6. normal generated writes cannot create cycles, while the parent Reference's mutability governs movement; and
  7. the selected Rails lowering supports the required reads without unbounded loading or N+1 queries.

The pure FoundationPlan::Sketch023::ImportPlanning::TreeTopologySlice now resolves the links needed to retain Dunbar150's one Tree and proves their same-Entity ownership without touching Active Record. It rejects missing, cross-owner, or reused parent/scope links without returning a partial slice. A resolved parent remains persistable when it is required, non-self, or multi-target so later whole-graph analysis can report those semantic problems. The live Project graph stores that projection as one subject-bearing FoundationPlan::Tree row rather than a separate Tree-definition lifecycle. Deferred composite foreign keys require both selected References to belong to the Tree's Project and Entity. Deferred owner-local key and position constraints permit atomic swaps. Project or Entity removal cascades the contained row, while supported direct Reference destruction receives friendly link-integrity feedback.

This storage proof does not establish self-target or root-requiredness semantics, scope coherence between individual records, cycle or movement safety, node deletion behavior, or a Rails lowering.

Other recurring uses include nested categories, organizational units, threaded replies, file-system-like folders, and referral or sponsorship hierarchies. Each still needs an Entity, one immediate-parent Reference, and any partition Reference. Presentation depth and business-specific movement rules do not follow merely from being a Tree.

Current format boundary

sketch/0.23 already represents:

There is no bare derived flag on a Reference. A default describes semantic model-level initialization. The Scaffold's create binding describes generated request assignment and is never a model default. If a recurring relationship is continuously determined from other data, it needs a typed rule and a demonstrated Compiler consumer rather than a boolean that leaves the derivation unspecified.

The schema cannot resolve most of those facts across the document. The Explorer tests prove selected presentation links. The Project capture and pure Analyzer exercise all six established design-and-parity examples and match the frozen Compiler's 154 Association facts and 159 result-Entity memberships. This establishes target-independent Association semantics. The bounded Rails Compiler separately exercises one exact Reference shape: one target, boolean one_to_one, boolean immutable, no realization or default, boolean requiredness, and the exact mechanically derived same-key unqualified referencing-side Association. It emits an explicit belongs_to, a UUID foreign-key column with matching nullability and a useful leading index, then adds database foreign keys after every Entity table so self-References and cycles do not require table ordering. A nonunique Reference index is omitted only when a compatible generated composite index starts with the same column; the deterministic retained composite inherits the omitted Reference's provenance. A unique one-to-one index and a Reference whose column is only a nonleading composite member remain separate. When one_to_one is true, the index is unique and Rails validates the logical Association's uniqueness with allow_nil: true so requiredness separately owns a missing target error. Optional References allow multiple nil values. On the pinned Rails release, assigning a new unsaved target to an optional one-to-one Reference can compare its still-nil target ID against stored nil foreign keys and report :taken. Generated Scaffolds do not expose one-to-one inputs, and generated-app proof does not exercise one-to-one inverse writers. Revisit that validator behavior with nested or associated target creation. An admitted referenced-side Association over a mutable Reference becomes has_one when one_to_one is true and has_many otherwise. These declarations omit class_name when the profile's pinned Rails inflections infer the intended target. For example, belongs_to :movie and has_many :people targeting Person need no class option, while belongs_to :author, class_name: "User" retains its role alias. Required References use Rails' required-by-default belongs_to; optional References retain optional: true. The Compiler emits foreign_key when Rails would choose a different column, and no dependent option unless this exact Association carries its Reference's deletion outcome. Generated-app checks prove Movie.featured_series, Director.movies, and the self-referential Movie.children. The Compiler also admits three first-level unqualified indirect has_many forms. An unpredicated admitted direct has_many may be the through step for a derived forward source backed by a mutable or required immutable Reference, or for a predicated direct has_many source backed by a required immutable Reference. A predicated admitted direct has_many may be the through step when its source is derived forward. All three forms emit has_many ..., -> { distinct }, through: with no dependent option. The Compiler adds source: unless Rails unambiguously infers the authored source from the through target's emitted Associations. For example, has_many :series, -> { distinct }, through: :movies needs no source option, while bookmarked_movies retains source: :movie. If both singular and plural candidate Associations exist, the explicit source disambiguates them. The Rails profile owns the inference rule. Generated-app checks prove Director.series returns one Series through two Movies during both ordinary and eager-loaded traversal. Together the checks cover forward and inverse traversal, nullability, and cyclical migration ordering. Every Domain Reference now retains an ordinary non-deferrable foreign key as an integrity backstop. Rails omits on_delete, so PostgreSQL's default NO ACTION preserves referential integrity without owning the authored deletion behavior. When exactly one admitted, unpredicated, referenced-side direct Association exists, that public Rails Association carries the authored outcome through Rails' ordinary association dependency API. Restriction uses :restrict_with_error and nullification uses :nullify. Deleting referencing records uses :delete_all for a collection or :delete for one record when the emitted child has no relevant callbacks or downstream Association work. Otherwise it uses :destroy. For example, User.movie_lists destroys MovieLists so their own dependencies run, while MovieList.list_items and MovieList.shares bulk-delete callback-free children.

The Scaffold controller uses these emitted dependency facts to retain refusal feedback for a direct :restrict_with_error or one reached through :destroy. Rails rolls back an indirect refusal without copying the child's errors onto its parent, so the existing parent alert can be empty. The Screens owner (repository-only) describes controller selection and the continuation tradeoff; database-only restrictions remain exceptions.

Selection uses emitted behavior. Rails installs destruction callbacks for dependent Associations. The pinned detector also requires :destroy for a child with an emitted direct inverse backed by an ordinary foreign key, even when that inverse has no dependency callback. Its downstream integrity restriction remains effective. Emitted AASM also installs an initialization callback, which the pinned active_record_doctor 2.0.1 detector treats as behavior that deletion must preserve. Save-only validations do not require :destroy. Authored but ungenerated image, rich-text, cardinality, or Counter behavior does not count as an emitted callback. The detector remains enabled: adding a real child destroy callback later makes it recommend changing the parent to :destroy.

Generated models use ordinary Rails association loading. These dependency options need no strict_loading override, including when Rails loads a child to destroy it. Useful preloads remain part of collection rendering. The query-regression policy (repository-only) pairs Bullet detection with selected index query-growth examples; the generated-test owner describes their bounded setup. The Rails target profile records the pinned source comparison.

This keeps application mutations visible in Active Record rather than encoding SET NULL or CASCADE in PostgreSQL. Database or Relation operations that bypass model callbacks are outside this lifecycle proof. The ordinary foreign key rejects referenced-record deletion while referencing rows remain instead of silently performing a different authored outcome.

The original Rails lifecycle change (repository-only) added a strict-loading exception to every selected deletion Association. The 2026-09-18 deletion qualification (repository-only) records the later narrowed opt-outs across all eight supported combinations, ordinary reads, callbacks, integrity, and generated requests at its exact source boundary. Ordinary loading now removes the need for those exceptions. The quality owner (repository-only) describes the current query checks; Rails deletion semantics and First Draft's own strict Compiler capture remain separate.

When no eligible inverse exists, when several eligible inverses make the carrier ambiguous, or when destructive cascade References form a cycle, the Compiler preserves storage and traversal with the ordinary foreign key and emits foundation_plan.gap.reference.deletion_not_generated at the exact on_referenced_deleted pointer. It does not synthesize a hidden Association or callback scheduler. A lone restrict outcome needs no model carrier because foreign-key integrity already rejects target deletion while a referencing row remains; a generated Active Record destroy that reaches this carrier-less backstop raises the ordinary ActiveRecord::InvalidForeignKey. Ambiguous restrict carriers still produce a gap because the model does not choose an arbitrary restrict_with_error error surface. The emitted-code audit (repository-only) retains the ecosystem comparison behind that boundary. An immutable Reference emits attr_readonly for its foreign-key attribute; generated checks prove that assignment after persistence raises ActiveRecord::ReadonlyAttributeError while creation and forward traversal remain ordinary.

This release also admits the direct inverse of an immutable Reference when it is required and not one-to-one. That shape may carry the Reference's dependent: option when it is the one exact lifecycle carrier; otherwise it emits the same has_many without dependent:. Creation through the collection sets the final foreign key before the first persistence. Reparenting an existing child reaches attr_readonly and raises ActiveRecord::ReadonlyAttributeError. Without a cascade lifecycle carrier, the collection's delete, delete_all, clear, replace, collection writer, and IDs writer try to nullify the required foreign key directly. With records preloaded where Rails needs to read them, PostgreSQL NOT NULL rejects each attempt, and Active Record rolls the statement back without deleting the row or changing its foreign key.

A cascade lifecycle carrier follows conventional Rails collection semantics instead. Its selected :delete_all or :destroy makes delete, replacement, the collection writer, and the IDs writer remove referencing rows rather than nullify or reparent their immutable foreign keys. :destroy runs child callbacks; :delete_all bypasses them. Rails implements explicit collection delete_all and clear as bulk deletion even for a :destroy Association, so those two operations remain outside the callback-preservation claim. Generated representative checks preserve ordinary and eager traversal, inverse creation, and reparenting rejection while proving one conventional lifecycle-carrier removal after migration and schema loading. This remains model-plus-schema enforcement, not a database immutability fence; an explicit relation-level update_all can bypass it.

Creation through the admitted derived-forward-source collection is ordinary: Active Record gives a new join Entity row both final foreign keys before its first save. Retained removal and replacement proof uses a fresh explicitly strict owner with both the indirect target and the through-source chain preloaded. Under that tested state, default deletion, destruction, and replacement delete whole join rows and create new rows when needed. They do not reparent a persisted immutable foreign-key slot or delete the final target. delete_all(:nullify) writes the source foreign key directly; a required immutable source remains safe because PostgreSQL NOT NULL rejects that branch. An optional immutable source could bypass attr_readonly, so it remains unsupported. The same requiredness backstop rejects direct inverse nullification. Those results do not establish every unpreloaded writer branch. Ordinary Rails loading is available; an explicitly strict Relation can still reject a read required by a writer. Eager traversal alone does not qualify mutation. Synthetic generated-runtime checks retain the exact preload boundary and these branches, distinct ordinary traversal, multi-owner eager traversal, readonly assignment, and database backstops after migration and schema loading.

One additional collection is admitted only when the outer through is the exact read-only first-level collection-source shape above and the outer source is a derived forward belongs_to backed by a required immutable non-one-to-one Reference. Case Chat therefore emits exactly has_many :members, -> { distinct }, through: :participants, source: :user. From an already loaded Case, an ordinary read uses one association SELECT; a flat preload for two Cases uses three total SELECTs; cached access uses none. Active Record treats this nested reflection as read-only. build, new, create, and create! raise ActiveRecord::HasManyThroughNestedAssociationsAreReadonly before target validation, including for invalid attributes. An explicitly strict Relation can reject an unpreloaded bulk operation before that reflection check. The harness checks ordinary unpreloaded bulk operations alongside preloaded nontrivial append, delete, destroy, bulk, replacement, collection-writer, and IDs-writer changes for the same nested-readonly exception. Equal-set replacement, collection assignment, and IDs assignment are accepted no-ops. Runtime snapshots prove that every rejected branch leaves Case, Conversation, Participant, and User rows unchanged.

Case Chat's collection-source form emits exactly has_many :participants, -> { distinct }, through: :conversations, source: :active_participants. Active Record makes that reflection read-only because its source is has_many. From an already loaded Case, ordinary traversal uses one association SELECT; a flat two-owner preload uses two; cached access uses none. Its source scope contributes status: "active" to construction. Because this generated Participant also has emitted AASM behavior with no_direct_assignment: true, build, new, create, and create! raise AASM::NoDirectAssignmentError before missing-Conversation validation or persistence. An explicitly strict Relation still rejects unpreloaded bulk writers. Ordinary loading reaches the read-only reflection; the harness checks unpreloaded bulk operations alongside writer branches on an exactly preloaded owner for ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection. No branch changes a persisted Case, Conversation, or Participant.

Within that exact required immutable has_many shape, a direct Association with no cardinality may name an admitted Predicate on its result Entity. CompilationInput requires the Predicate scope and Association to come from the same sealed Project generation. The association lambda calls the exact PredicateScope entry's emitted named scope, using an explicit receiver for a keyword-shaped name such as self.if. Later application edits to that scope therefore reach the referencing Association. This preserves ordinary Rails scoped-Association behavior. A simple equality contributes its Predicate Field value to scope_for_create, so studio.active_movies.build.active is true; a focused generated-model runtime proves that default and the filtered reader. Oscar Party's two Bookmark filters, Case Chat's active Participants and open Threads, and Photogram's published Posts exercise this boundary after migration and schema loading.

The Rails scopes page preserves why the named-scope call replaced a duplicated condition, along with the preceding model/database comparisons. Existing admission, reviewed gaps, scope-name handling, and AASM protections remain in force.

These predicated Associations are filtered read traversals, not general Predicate-enforcing mutation APIs. Creating through one can produce a qualifying child when supplied or defaulted attributes satisfy the Predicate. Conflicting explicit attributes can still persist a child outside the Association's membership. Generated code must not present the filtered collection writer as enforcement of the Predicate. When the filtered Field has emitted AASM behavior, AASM's ordinary writer protects it: conversation.active_participants defaults status: "active", and build or create raises AASM::NoDirectAssignmentError. Creating through unfiltered conversation.participants instead starts the Participant as invited; accept! moves it into the active reader. A machine whose behavior is not emitted has ordinary attribute assignment; its reviewed State Machine gaps disclose that incomplete workflow behavior.

Case Chat's predicated-through form emits exactly has_many :members, -> { distinct }, through: :active_participants, source: :user. It returns distinct Users, uses one association SELECT from a loaded Conversation, three total for a flat two-owner preload, and none after caching. build and new construct only an unsaved User. Invalid create reports ordinary User validation feedback. Valid create and append attempt to build an active Participant and roll back when emitted AASM rejects direct state assignment. Conventional removal, replacement-removal, collection, and IDs writers delete only the Participant join row, preserving the User and Conversation; equal-set assignments are no-ops. Adding or swapping a User raises the same AASM error and restores the original membership. The authored leave! event also removes the User from the filtered reader while retaining its inactive Participant row. These are pinned Rails and AASM behaviors, not a Compiler-defined collection wrapper.

After local ExpressionSql Predicate scopes admit that relationship catalog, a root unfiltered exists may reuse an exact emitted cross-Entity direct required immutable has_many. Photogram's existing user.has_published_posts retains the exact predicated user.published_posts DomainAssociation and child ExpressionSql evidence. It emits where(id: Post.published.select(:author_id)), calling the child scope once and matching User id against the emitted author_id projection. The new unpredicated user.follows_anyone retains user.outgoing_follows, selects follower_id from all Follows, and matches User id through the same owner-primary-key/child-foreign-key pattern. Outer Users are naturally deduplicated. Both generated scopes return exact IDs in one SELECT after schema loading; this does not claim SQL EXISTS lowering. The existence projection cannot feed back into Association admission. Outside the two exact current-Account forms below, filtered, nested, negated, grouped, and reused collection shapes remain omitted and listed as target gaps.

The Rails scopes page also preserves the collection-existence rationale and preceding Photogram model/database comparisons. Named-scope reuse retains the foreign-key projection, single SQL statement, admission, and gaps.

One separate root unfiltered singular exists may retain the exact same-generation emitted cross-Entity optional immutable non-one-to-one derived-forward DomainReference whose deletion action is nullify_reference. Photogram's feed_occurrence.sourced_by_follow Predicate retains feed_occurrence.source_follow and emits where.not(source_follow_id: nil). Foreign-key integrity makes a non-null slot equivalent to a present Follow. Because no admitted inverse can carry dependent: :nullify, the ordinary non-deferrable foreign key rejects deletion while the referencing row remains, and the authored nullification remains an exact target gap. The scope proves stored presence rather than that missing transition. This is an outer-row IS NOT NULL condition, not SQL EXISTS. For this unfiltered singular-presence projection, required, mutable, restricted, cascading, one-to-one, self, authored-inverse, indirect, filtered, nested, negated, grouped, reused, and all broader singular shapes remain target-support gaps.

Two final Photogram scopes admit an exact typed Account relationship filter after the public Account lowering, Field-only registration inputs, and Account-self surface qualify. feed_occurrence.belongs_to_current_viewer follows the required immutable derived-forward viewer Reference and filters viewer_id. post.liked_by_current_account follows the emitted unpredicated direct post.likes collection and required immutable derived-forward like.user, then filters Post IDs through Like.where(user_id: current_account.id).select(:post_id). Each nested Expression is exactly current_record == current_account, and each scope requires current_account: explicitly. A nil, wrong-class, unpersisted, or destroyed argument returns none; generated models use no request, global, or thread-local Account state. An emitted index already leads with each filtered Reference column, so this lowering adds no index. Other relationship topologies, comparison directions or operands, nesting, Boolean composition, and Predicate reuse remain omitted and listed.

Three first-level indirect forms are admitted. An unpredicated admitted direct has_many, including the required immutable inverse, may be the through step. Its source may be a derived belongs_to backed by a mutable or required immutable Reference, or a predicated direct has_many over a required immutable Reference. Rails makes the collection-source form read-only. A predicated admitted direct has_many may instead be the through step when its source is a derived belongs_to.

The current release combines this relationship catalog, including the two exact current-Account scopes, with complete authored Scaffold analysis and Web realization. The machine reference (repository-only) names its Analyzer and Compiler releases, and the reviewed GapSet fixtures own its exact records. Two Oscar partial-create records disclose missing standalone Movie supply. Its two structurally admitted Ordering patterns represented by Oscar, application-domain, Web-icon, Policy, and residual-Validation changes do not expand the admitted relationship catalog. The Rails-first deletion change uses exact admitted inverse Associations where available and adds three Case and three Photogram gaps where no conventional carrier exists. The 2026-08-27 Codespace and generated-repository proof remains predecessor-bound; the 2026-08-28 reviewed-realization local qualification (repository-only) records bounded local proof for a predecessor release pair, not the Ordering or deletion additions.

Descriptor-owner retention and predicated-Association composition make user.bookmarked_movies, user.rated_movies, user.unwatched_bookmarks, user.watched_bookmarks, user.followed_users, user.followers, conversation.active_participants, conversation.open_threads, conversation.members, case.participants, case.members, and user.published_posts executable. Combined admission also makes user.has_published_posts executable through the exact emitted user.published_posts collection and user.follows_anyone executable through exact emitted user.outgoing_follows. The latter removes only Photogram's target Predicate gap at /application/entities/0/predicates/0. The current-Account release also makes post.liked_by_current_account and feed_occurrence.belongs_to_current_viewer executable. It removes only their target Predicate gaps at /application/entities/2/predicates/2 and /application/entities/6/predicates/3.

Admitting conversation.members historically removed 24 Case service records and exposed nine precise target records. The cumulative Policy realization now admits those participant decisions and their Scaffold consumers. Case retains the delivery import skip, target-unlowered message.notifications, and selected iOS plus three precise Reference-deletion records. Its exact Message implicit Ordering emits. Photogram's post.photos remains two service records: its pruned root and unsupported cardinality at /application/entities/2/associations/0/cardinality. The bounded one-hop Reference-select and private Policy-gated Association-descriptor consumers complete the work tracked by #417 (repository-only). Multi-target, multi-hop, and broader consumer shapes remain outside this release. #434 (repository-only) remains open for message.notifications and post.photos.

The singular-reference release additionally removes only Photogram's feed_occurrence.sourced_by_follow target Predicate gap at /application/entities/6/predicates/1. No Oscar Party or Case Chat gap changes, and no surviving record changes meaning.

The current combined-runtime qualification checks ordinary traversal, flat multi-owner eager loading, cached access, the reviewed Plans' Predicate-dependent Association consumers and Predicate scopes, including both explicit current-Account scopes, with exact IDs and one SELECT after schema loading. The scopes comprise local scopes, root collection-existence scopes, one singular-reference existence scope, and the two current-Account forms. It also checks nil, wrong-class, unpersisted, and destroyed Account arguments. The 218/184-file materialization controls pass both paths and retain broader writer/runtime proof for the exact unpredicated required-immutable-through shape alongside the public Case request and Descriptor and LinkTo lexical-local canaries. Earlier descriptor, collection-existence, singular-reference, and nested-through runs remain historical constituent evidence.

An optional immutable has_many can nullify its foreign key, so it remains a target-support gap. Every immutable has_one, an optional immutable indirect source, referencing-side aliases, broader nested-through paths, indirect Associations with their own Predicate, predicated-through shapes with a collection source, other predicated direct Associations, cardinality-qualified Associations, multi-target, defaulted, or realized References, polymorphism, exclusive arcs, broader descriptor chains, and descriptor consumers beyond public index/show, public Reference selects, and the one private Policy-gated index remain outside this release.

Before calling the Reference API exhaustive, later format and target work need to settle:

  1. how the reverse deletion direction and an explicit no-effect outcome are represented;
  2. whether closed target sets ever need per-target deletion outcomes;
  3. a Rails Tree lowering and its integrity, movement, deletion, and query guarantees;
  4. complete target-language name-collision and Association-lowerability checks; and
  5. import reconciliation for referencing-side aliases, broader indirect paths, broader Predicate consumers, and authored cardinality.

References marked “repository-only” name implementation or internal material outside this public guide. They are intentionally not links. Publishing a design does not prove its implementation.