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:
- Definition and storage ownership. The referencing Entity contains the Reference definition. Its generated table receives the foreign-key representation.
- 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 Reference belongs to its referencing Entity, not to a foreign-key Field;
- a physical key column is Compiler output rather than a semantic Field;
- every relationship reader is an Association in the live Project graph, including the derived forward reader
conventionally lowered to
belongs_toby the Compiler; - the first Foundation derives one same-key, unqualified referencing-side direct Association per Reference, while additional direct and indirect Associations remain explicit in the serialized Plan;
- Scaffolds display and accept a relationship through its Association rather than treating its storage Reference as a form value; and
- a closed multi-target Reference may lower to several columns or to an ID-and-type pair, so no single Field can own it in every realization.
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:
bookmark.userandrating.userare different facts owned by different Entities;follow.followerandfollow.leaderare parallel facts with the same target; andcomment.parentis a self-Reference whose owner and target are both Comment;user.bookmarked_moviesnames thebookmarked_moviestraversal even when its display name isBookmarked movies.
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:
- the owner prefix matches the containing Entity;
- every typed path resolves in the expected namespace;
- paths are unique within that typed namespace;
- every derived same-key forward Association can be materialized without colliding with an authored Association; and
- endpoint ownership, indirect source ownership, and dependency topology are coherent.
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 derived
side: "referencing"Association starts at the Entity that owns the Reference and reaches its target; and - an authored
side: "referencing"Association can name an additional, possibly Predicate-qualified traversal from that same owner; and - an authored
side: "referenced"Association starts at one target Entity and reaches records that own the Reference.
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:
restrict: reject target deletion while referencing records remain;nullify_reference: keep each referencing record and clear its Reference; anddelete_referencing_record: delete each record that owns the Reference.
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.
Trees group related self-Reference meaning
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:
- roots have no parent;
- non-roots point to another record of the same Entity;
- a partitioned Tree constrains the parent to the same partition;
- target lowering may derive hierarchy-safe Associations, integrity checks, indexes, and query helpers.
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:
- the Tree key belongs to the containing Entity's Tree namespace;
parent_referenceresolves to a self-Reference owned by that Entity;scope_reference, when present, is owned by the same Entity;- parent selection cannot cross the declared scope;
- Reference requiredness and deletion outcomes are coherent with root and descendant behavior;
- normal generated writes cannot create cycles, while the parent Reference's mutability governs movement; and
- 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:
- Reference ownership through Entity nesting;
- one nonempty closed target set;
- typed readable paths and a logical local slot;
- local requiredness;
- optional one-to-one stored-edge multiplicity;
- one target-deletion direction;
- a derived same-key forward Association, authored direct Associations on either side, and indirect paths composed only from Associations;
- explicit create bindings for server-owned Reference assignments;
- a minimal Tree over a parent self-Reference and optional partition Reference; and
- qualified cardinality on Associations.
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:
- how the reverse deletion direction and an explicit no-effect outcome are represented;
- whether closed target sets ever need per-target deletion outcomes;
- a Rails Tree lowering and its integrity, movement, deletion, and query guarantees;
- complete target-language name-collision and Association-lowerability checks; and
- import reconciliation for referencing-side aliases, broader indirect paths, broader Predicate consumers, and authored cardinality.