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

On this page

Rails Scaffolds

Status: Implemented for the admitted Scaffold surfaces below. Other surfaces are listed gaps.

One immutable ScaffoldLowering result owns every admitted surface and precise omission. Scaffold lowering lists the admitted representative surfaces, and the reviewed GapSet lists each omitted surface or child. Generated controllers, views, and forms are ordinary Rails scaffold source: resources routes, model-bound forms, Pagy pagination, .preload relations, and direct redirects. A surface the target cannot realize is omitted without deleting its admitted siblings.

On this page

Scaffold lowering

ScaffoldLowering joins the complete retained Scaffold graph to exact submitted-source and same-generation Domain, relationship, query, Account, Policy, route, and descriptor evidence once. Its frozen result owns admitted definitions and concrete route consumers; typed inputs, bindings, recursive render nodes, authorization decisions, query and cursor plans, associated forms, returns, nested preload paths, and source-addressed omissions. Public, item-authorized, environment-gated, and projection-gated consumers remain distinct. Protected collection and member lookup starts from the admitted Policy relation. Action and collection-wide gates authorize before presentation loading; views guard protected content. Web profile decisions target current_account. The renderer does not scan raw graph rows, synthesize an authorization scope, or infer a return route.

The admitted representative definitions are:

Plan Entity Definitions Selected Rails routes
Oscar Party Movie, Person index, show, create, update, destroy all seven resource routes
Oscar Party Credit create, update, destroy create, edit, update, destroy
Oscar Party Bookmark index, show, create, update, destroy all seven resource routes
Oscar Party Rating create, update, destroy create, edit, update, destroy
Case Chat User profile, update singular /account and /account/edit
Case Chat Case index, show index, show
Case Chat Conversation index, show index, show
Case Chat ConversationThread show show
Case Chat Message create create
Case Chat Notification index, show index, show
Photogram none none none

Admission can preserve safe partial meaning. A selected unsupported Predicate or Ordering remains an exact consumer gap, and the index or admitted collection calls its unwritten scope, which raises until it is written. Deterministic id ordering remains only for a selected Ordering that no unwritten scope stands in for, and that fallback order is never reported as the authored Ordering. Current output accepts no generic-text fallback for an unsupported Field kind and no unrestricted lookup before a protected decision. Unsupported children are omitted without deleting admitted siblings, but an authored nonempty projection whose every child fails never becomes an implicit descriptor view. An input or binding the target cannot lower, such as an unsupported control type or relationship shape, is left out on its own; the rest of its form stays. A return the target cannot emit falls back to the mutation's default destination. Unsupported route pairs, Policy ownership, and associated forms omit their exact consumer. Each of these stays listed in the GapSet. Selected iOS and Android clients reach these same Web pages, protected ones included, through the shared main navigation. The Scaffold owner (repository-only) owns the detailed contract.

Lower each admitted authored definition through one exact-source, input-owned ScaffoldLowering result. Standard resource routes use Rails resources; Case's Account-owned profile uses fixed Web-only /account and /account/edit routes resolved from current_account. A retained definition needs a concrete consumer: index/show/destroy need their matching route; create needs a create route or admitted associated-create consumer; update needs an edit/update route or Account. new requires create, and edit requires update. A selected request-only create needs no New page, associated form, or return destination. Public and otherwise supported protected creates use the same admission rule; Policy shape controls access. Required-value coverage follows the actual consumer: each associated form may supply its exact inverse Reference. A create that lacks a source for a required value in its inputs, bindings, or realized defaults keeps its ordinary endpoint or associated form, with a partially_generated gap on that route or form. Saving fails validation until the owner's agent supplies the value, and the README lists the successful create example as test coverage to complete. Membership authorization adds no separate direct-input requirement to an associated form.

A binding assigns its value in new and create before authorization, as @category.merchant = current_account.merchant, Date.current, or Time.current. A longer path, a literal, or a value of another kind is left out and listed.

Routes and guards follow the code-worth-keeping requirement. The Web renderer emits a narrow resources ... only: set from the authored resource_routes whose definitions survive. A protected consumer whose Policy the Compiler cannot lower stays behind a stubbed Policy that admits nobody. The 2026-10-08 (repository-only) and 2026-10-10 (repository-only) decisions record why.

Web files and routes

WebScaffoldFiles consumes the immutable input-owned Scaffold lowering and same-generation collision claims. It selects one ordinary task per generated routes, navigation, controller, locale, pagination, Scaffold view, form, profile, or integration-test path. Each task renders only its final path and keeps the normal task owner, SQL guard, and failure identity. The generalized renderer replaces legacy public-index output whenever the admitted graph needs protected authorization, recursive projections, query choices, bindings, associated forms, dynamic returns, cursor pagination, or profile behavior. A legacy public-only shape may still take the byte-for-byte predecessor renderer, but the two projections never claim the same authored surface or output path.

Core's Domain-routes and shared-navigation seams remain application-global. They combine admitted resource routes, the Account route with optional authored profile details, without transferring ownership to Account, Policy, or an Entity controller. Resource entries use conventional Rails controllers and helpers. The profile uses singular /account routes and current_account; it is not a resources :users member. Duplicate human navigation labels remain valid because stable identities and collision-admitted paths are distinct.

Each associated collection is a nested resource with a namespaced controller: resources :credits, only: %i[index new create], module: :movies inside resources :movies routes to Movies::CreditsController, whose views live in app/views/movies/credits/. A parent whose plural names a constant the target already defines, such as a Rail's Rails, nests its controllers under its own model instead, as module: :rail and Rail::InspectionsController, so they stay out of the framework's module. An uncountable parent's plural is its model too, as in Equipment::ChecksController. Such a model is also its nested controllers' module, and each of those controllers is named after one of its Associations, so a target class that shares such a name is rooted: has_many :inspection, class_name: "::InspectionController" beside Equipment::InspectionController. An exclusive-arc writer on that model checks record.is_a?(::InspectionController) for the same reason. The main navigation marks a nested page's parent section current, and the Account profile's collection pages mark Account. Scoped inputs omit the URL-bound inverse Reference. Optional Plan overrides use direct routes.

Mutation shapes

The predecessor public-only projection admits a narrow shape: exact public indexes, a bounded public create/update form, an optional show, and a destroy that requires the complete show-bearing shape. It still feeds native qualification and the byte-for-byte predecessor renderer, and it explains anything else with a source-addressed warning. When analysis has the exact submitted source, the general Scaffold lowering above owns every page, and the GapSet gives each omission its own reason. Analysis then drops the projection's shape warnings and keeps only its route collisions, which still describe emitted routes. This projection reads Primary Descriptors only through reader chains, so it leaves out a record that a computed label describes.

Record display

Resolve each Entity's primary_descriptor into an explicit reader chain wherever generated UI needs a whole-record label. A direct descriptor reads its Field or system Field. An Association descriptor follows a singular referencing-side traversal and continues through the target Entity's descriptor. Do not override to_s; emitting ordinary source at each use site lets one generated view evolve without changing every other view. A multi-target Reference behind that Association branches over its closed targets when their descriptors differ. Semantic validation still resolves every locator and rejects an optional Association and descriptor cycles. Descriptor use does not cancel authored encryption or log-redaction behavior. The current Compiler reads a direct required emitted scalar or system Field, or one required ordinary single-target forward Association hop ending in one, and preloads that hop. Any other descriptor becomes a label each use site computes. An optional Field reads movie.title.presence || "#{movie.model_name.human} ##{movie.id}", an enum Field shows its value's label, and any other shape, such as a longer chain, shows the model name and id and is listed as partially generated. Reference options for a computed label are ordered by id and are not searched.

Public Web admission consumes the same-generation semantic Primary Descriptor receipt. It accepts one required ordinary single-target forward Association hop ending in a required emitted scalar or system Field, retains exact source/provenance, and emits the explicit reader chain plus preload for index and show. Public Scaffold Reference selects reuse that descriptor. The one-hop form eager-loads its Association, orders by terminal value and target id, and maps labels and IDs from one result query. The private Policy-gated index authorizes first, then reuses the descriptor reader, preload, and provenance. Multi-hop, multi-target, and broader consumer shapes, and any record a computed label describes, are omitted and listed at their service, semantic, or target boundary. A foreign target or profile is caller misuse at this exact-profile boundary and fails before any projection is built. When whole-Application qualification has already admitted an exact ordered Domain catalog, the web projection consumes that same catalog.

Pagination

Index relations apply the admitted authorization scope before Predicate, Ordering, preload, and pagination. Cursor pagination requires an input-owned deterministic keyset; over an unwritten Ordering it serializes the timestamps that the authored terms name. Root show/profile collection previews use Pagy Countless with explicit page: 1, limit: 5. Pagy queries at most six rows and display five, using the extra row only to decide whether to offer View more…. This final list row uses an ordinary Rails link with a plain-text label rather than another heading. It opens the dedicated collection page from its beginning, independent of its page size or pagination mode. The parent page's page parameter cannot move a preview. Previews do not count the collection. Every admitted collection derives a one-level parent-scoped read route and a separate paginated page, even without a target global index. That page is its nested resource's index. Show/edit/update/destroy routes stay flat; associated New and create use the collection URL. The full request repeats the parent show/profile authorization and the projection's gate or item scope before querying its records. Its rows preserve the same Predicate, Ordering, supporting content, and separately authorized destination links. Like a top-level index, the page loads its records once in the view with .load.any? and has two empty states through shared/ui/empty_state. A collection labeled with its Entity's plural reads "No habits yet", as an index does; any other label keeps its own case, "Nothing in Upcoming deadlines yet", read from the same translation as the page's title. A page past the end, which Pagy returns empty, links back to page 1. A preview keeps one compact line, because it is pinned to page 1.

This uses ordinary Rails Associations and named GET routes. The pinned Rails 8.1.4 route-name check rejects duplicate explicit names; the mapper accepts as: without changing the path. Pagy 43.6.1 supplies offset pagination by default, Countless for previews, and keyset for an authored cursor choice. No new pagination library or Plan setting is needed, and nesting stops at one level.

Cursor requests use Pagy's native decoder and Active Record adapter. Malformed Base64/JSON may recover to page one. Some crafted shapes, extreme values, or NUL-containing strings can still raise server errors; the application promises neither exact-arity admission nor universal 400 responses. It adds no duplicate decoder, per-column validator, size cap, or global exception rescue. Normal HTTP transport limits still apply. Ordered timestamps use the documented serialization hook to write iso8601(6) values; Rails' default millisecond JSON representation can otherwise repeat or skip rows. Other cursor values keep native serialization, including legitimate long text values.

Small shared/_pagination and shared/_cursor_pagination partials render pagy.series_nav and pagy.next_tag directly with ERB's HTML-rendering syntax. Their relative translation scopes preserve labels and accessibility text. The pinned Core styles their ordinary .pagination class. Pagy escapes its URL parameters and expects application-supplied options to be trusted or escaped, as its safety guide explains. The emitted labels are fixed translations. A second sanitizer allowlist and forwarding helper add no application behavior.

Select each pagination partial from emitted indexes and full collection pages, including Account profile collections. Emit PAGE_LIMIT = 24 only in a controller with one of those actions; five-record previews keep their inline limit. Every index and full collection page shares the shared.pagination.empty copy for a page past the end, even when no offset partial is emitted. Preview collection copy includes View more; full collection pages do not use that label.

Associated collections

Direct has_many and the admitted indirect direct-has_many/forward-Reference shape start from parent.association. Rails retains its parent constraint and admitted intermediate Predicate; the emitted indirect Association already uses distinct for one destination per identity. The same Relation receives the selected authorization scope, presentation Predicate, Ordering, and preloads. Pagy's pinned Active Record adapter qualifies cursor comparisons with the destination table, so these through queries need no outer ID subquery to avoid ambiguous columns. The displayed Association supplies the final URL segment. Index and nested collection projections, other indirect shapes, and conflicting browse paths remain explicit projection gaps. A repeated Association presentation or the reserved edit segment cannot share one browse path.

A helper-name collision preserves the collection and its URL. Existing resource, profile, Account, and pinned Core helpers retain their names. A scoped browse or New helper adds _collection only when its ordinary name is occupied, repeating the suffix until it avoids both allocated helpers and other scoped helpers' preferred names. Thus Order.items may use order_items_collection_path beside OrderItem's order_items_path, while their paths remain /orders/:order_id/items and /order_items. Sibling scoped helpers follow the same allocation rule. Nested resources would derive another helper from the segment, so a collection whose allocated helper differs, such as this one or an Account's rent_due collection, which Rails would name account_rent_due_index, keeps explicit routes to the same controller: get "/orders/:order_id/items", to: "orders/items#index", as: :order_items_collection. A scoped form uses polymorphic routing when model inference selects its actual helper; aliases, collision suffixes, and inflection differences otherwise supply an explicit compiled URL. This changes no authored meaning and adds no gap; conflicting browse paths still follow the omission rule above.

Preview and full-page rows contain projected values and an optional destination link, with no edit/destroy controls. Selected and authorized edit/destroy actions appear on the record's detail page when show is generated. Otherwise their flat routes and edit form remain generated without a scaffold link; the application owner adds an appropriate entry point. A Plan without show gets these routes from its authored resource_routes and authorization, with no gap or compilation refusal. When the Compiler omits a selected show, for example because its read Policy's class name is taken, the show definition's gap names the routes left without a link: Selected edit, update, and destroy remain at /posts/:id/edit and DELETE /posts/:id with no generated link. An omitted index likewise names the selected new and create routes that lose its New link. The gap does not name a request-only create or update, which never had a link, or a destroy that a one-tap toggle's undo button still reaches. A one-tap create that is not a toggle, such as a check-in, has no undo button, so the gap names its destroy. Empty and small collections have a browse route but no View more… row. An admitted associated create has an Add link at every collection size on both surfaces; both links open the same scoped New page with its direct Association/create binding. Only actual gates produce a collection visibility decision; public and item-scoped cards render directly. A protected create checks its own Policy after the collection gate permits presentation. Public create needs no permission flag or view branch.

Projections

Field projections use typed readers. Association projections use the qualified relationship and target Primary Descriptor; omission of the entire projection requests descriptor fallback only when the author truly omitted it. A nonempty authored projection whose every child is unsupported does not become an invented descriptor view. Index and show descriptors use their typed reader or computed label, with Rails localization for date/datetime and true/false copy for Boolean values. A required projected Field renders its value plainly, because its NOT NULL column always holds one; optional projected Fields retain missing-value copy; projection and descriptor headings call human_attribute_name. An admitted target show route turns an authored Association descriptor or child-bearing projection into one separately authorized link; denial preserves the already-authorized text. The renderer does not synthesize an unauthored reverse Association. Partial siblings survive. Oscar's Movie Credit collection demonstrates that boundary: the collection and nested Person association survive with the admitted billing_order scope, and required enum storage admits the Role display and authored-label input. The associated Credit create form is admitted. Bookmark priority likewise admits create/update controls and index/show display, while the index that selects its rank Ordering keeps a gap and deterministic id fallback beside the emitted scope. The Movie index calls the unwritten highest_rated scope instead. Each emitted Counter is shown by default: an explicit index, show, or collection-row projection gains the Counters it does not already select, after its authored items.

Forms

Create and update inputs map Boolean, date, datetime, decimal, integer, language-code, long-text, short-text, time-zone, URL, enum, and direct Association controls with exact requiredness, length maximum, option descriptor, parameter, attribute, and numeric-validation facts. Raw-input integer and decimal validation runs before Active Record coercion. A time-zone control displays human-readable Rails zone labels while submitting canonical TZInfo identifiers, preserving an existing admitted identifier through edit. An enum control displays authored value names and submits stable keys in authored order. Each required emitted destination has an admitted create-time source: control, binding, a realized Field default, required State Machine initial-state source, a Counter's counter_cache default of zero, or, only in associated-form context, the exact inverse Reference. An authored Counter input is dropped and listed without omitting the rest of the form. Conditional-presence owners remain actual controls in both forms. Derivations are not sources in this release, and derived controls are omitted. New and associated New select the create form body; Edit selects the update body. Those same consumers select the form partial, strict locals, dropdown loaders, and form copy. Request-only writes retain their admitted strong parameters and response behavior. Within one Entity, contexts with identical ordered controls share a concrete partial after omitting the route-bound inverse. Different controls or order emit separate partials, selected explicitly by New or Edit and reused on failed submissions. Each partial has only its required option locals and static error-field mapping; fields have no action or parent-type visibility branches. Matching accepted parameter names share one <resource>_params method, regardless of control order or action authorization, and a controller with one helper also names it <resource>_params. Different create and update allowlists keep create_<resource>_params and update_<resource>_params. A nested controller's helper leaves out the inverse its URL supplies; a Create action without editable inputs retains no parameter helper. Index actions build and bind a New-link candidate only for authorization against that record, including its required preloads. Public and current-Account New links need no candidate; ordinary New-page setup remains. Model-bound form_with infers the ordinary scope, URL, and HTTP method. Scoped creates pass the parent and child models; helper aliases, inflection differences, and singular /account retain explicit URLs. Forms use novalidate, so Rails' server validation owns every error message.

Each text, number, select, checkbox, and file control renders inside Core's shared/ui/field partial. The partial owns the fieldset.field wrapper and the control's error list, and it yields the control's aria attributes; the label and control stay in the form's ERB with Basecoat classes:

<%= render "shared/ui/field", form:, attribute: :name do |aria| %>
  <%= form.label :name, class: "label" %>
  <%= form.text_field :name, required: true, class: "input w-full", aria: %>
<% end %>

The list's ID is Rails' form.field_id(:name, :errors), and the control's aria-describedby names it only while the attribute has errors. The date and Reference pickers keep their own partials and use the same ID scheme, and their enhanced controls add _enhanced to the control's ID, with errors at <control ID>_enhanced_errors. Forms render no namespace, because no page renders a form twice; the error summary's heading uses form.field_id(:errors).

A date control receives the tightest inclusive ISO min and max from emitted unconditional date-literal comparisons. Strict bounds move one calendar day inward. Bounds outside the control's four-digit year range, conditional comparisons, and unsupported comparisons supply no HTML or calendar restriction. The form's provenance includes the Validation subjects that supply its selected bounds. The native date input and shared React calendar use those bounds as input guidance; the generated Rails validators remain the persistence boundary.

Generated locale entries use Rails activerecord.models, activerecord.attributes, and helpers.submit keys. Ordinary model-bound labels use Rails form.label inference; application helpers.label overrides work normally. Projection headings and shared controls retain explicit human_attribute_name lookups. References emit the authored label for both the logical Association and _id attribute; shared controls explicitly select the logical Association label so controls and errors share that name. A Reference select retains an empty choice. A searching picker loads only the current choice; any other loads every option: a direct descriptor uses scalar order and pluck; tuples pass through unchanged unless boolean/date/datetime labels require localization. A one-hop descriptor uses one eager-loaded query ordered by the terminal value and target id, then maps labels and IDs without traversal queries. Only required References add the HTML required attribute. Protected forms authorize their initialized record before querying options.

Reference option scope

A Reference input lists and accepts only the target records that one Policy connects to the signed-in Account. The Policy under which the input's own Scaffold displays the same Association decides. Without a single such Policy, the target's admitted show authorization decides, or its index authorization when the target selects no show; the screens owner (repository-only) explains the order. When each Account owns its Places, a Plant form searches the Places that PlacePolicy lets the signed-in Account manage and loads its own:

@plant_place_id_options = authorized_scope(Place.all, type: :active_record_relation, as: :manage, with: PlacePolicy)
  .where(id: @plant.place_id).pluck(:name, :id)

After create or update authorizes its record, it assigns each submitted ID through that same relation, so an ID outside it raises ActiveRecord::RecordNotFound and answers 404, as a hidden record's own URL does. A request-only create and the Account profile edit check their submitted IDs the same way. A public target read keeps every record and adds no lookup. A current_account gate adds nothing when the form's own authorization already proves it: the same gate, or a gate that admits every signed-in Account on a form that requires one: one that authorizes current_account or whose every Policy branch needs the Account, unlike visible == true or owner == current_account. Any other gate omits the input. A Policy whose other consumers are all gates or create forms still gains its relation_scope for the input, as UserPolicy gains relation_scope(:read) when Lease pages display their tenant under user.read. When that Policy is a stub, its scope raises, so the form's pages raise until it is written. A target that has Policies but no Policy chosen this way omits the input, so the form never lists records that those Policies might deny. An Account target gets an email field instead: the model keeps the typed address in an attr_accessor, and create or update assigns User.find_by(email: params[...].to_s.strip.downcase) when it is present. A required Reference that matches nothing fails belongs_to; an optional one gets a presence error. A target without Policies keeps every record. Each omitted input is a listed scaffold.input.not_generated gap, and a required Reference left without a create author follows the incomplete creation (repository-only) rule.

Server bindings run before persistence. A value read from the Account needs a non-public Account-backed context, so it never silently turns a public create into an authenticated request. For a guest it reads nil (current_account&.merchant), and authorization then sends the guest to sign in.

Implemented. A picker whose target has a stored text Primary Descriptor, direct or one hop away, searches the server as the person types instead of loading every record (repository-only) into the form. The controller that renders the form answers each page's search with an action beside the page's own, routed as get "/plants/new/place_options", to: "plants#new_place_options", defaults: {format: :json}:

def new_place_options
  @plant = Plant.new
  @plant.owner = current_account
  authorize! @plant, to: :manage?, with: PlantPolicy
  pattern = "%#{Place.sanitize_sql_like(params[:q].to_s.delete("\0"))}%"
  places = authorized_scope(Place.all, type: :active_record_relation, as: :manage, with: PlacePolicy)
    .where(Place.arel_table[:name].matches(pattern))
    .order(name: :asc, id: :asc).limit(20)
  render json: places.pluck(:name, :id).map { |label, value| {label:, value:} }
end

It runs the page's callbacks and checks, as edit_place_options runs set_plant and the update's, an associated New page's search its parent's rule and collection gate, and the profile's its sign-in, so a search admits exactly whom its page admits. It then lists from the scope that the page's select used, which create and update also find each submitted ID through. It skips verification only when its page does. matches is a case-insensitive ILIKE, sanitize_sql_like keeps % and _ literal, a NUL byte is dropped, and results stop at 20.

A non-text or system-field descriptor, a redacted or encrypted descriptor Field, whose text a search would log, the legacy public-index renderer, and registration keep the full list. Without JavaScript, a searching select offers only the current choice and a blank, so a New page whose required Reference searches cannot create its record.

Associated creation

An associated-create form is limited to one direct unpredicated inverse has_many, one admitted target create definition, no conflicting parent binding, and one unambiguous parent/target context. It derives its scoped New and POST routes without requiring a selected standalone Create. Parent cards and full collection pages link to a dedicated scoped New page, except for a no-input record. Their create-link authorization builds and binds a child only when its Policy checks that child; an environment gate checks current_account directly. Each parent's nested controller, such as Movies::CreditsController with movie_credits_path and new_movie_credit_path, serves that parent's New and create; the child's own controller keeps only its standalone actions. Its parent finder loads the parent, and each action authorizes it before building the child through the parent's Association. Parents whose forms take the same inputs share one partial, such as credits/_form, which each nested New page renders by that path with parent_record: @movie. A form that only nested New pages render requires that local and builds form_with model: [parent_record, credit]; one that an Edit or standalone New page also renders defaults it to nil and compacts the array. The form and strong parameters omit exactly the route-bound inverse input; other selected inputs and server bindings remain. Each form request owns its remaining option queries, so separate parent forms may share target Association inputs without sharing form state. The form infers its POST route from the parent and child models when that matches the selected helper; aliases and inflection differences use an explicit compiled URL. Source pointers and form identifiers are not a generated request protocol. Cancel and default success return to the scoped collection; only an explicit associated return_to overrides success. A profile collection does not acquire this resource-member context. Predicated collections, indirect creation, longer return chains, duplicate parent/target contexts, and omitted target definitions remain gaps. The plain New page uses ordinary Rails navigation and can later be enhanced at the same URL; no modal, inline form, or new-destination attachment workflow is inferred. Creating a Credit selects an existing Person plus the role.

No-input records

When the parent and the create bindings supply every value, the associated create has no form. The Foundation Plan pattern shows the authored shape: a Like under a Post whose Member comes from current_account. Both collection surfaces render button_to Like.model_name.human, post_likes_path(post) in place of the Add link, behind the same create permission. No scoped New route, new action, New page, or form partial is emitted for that context unless another context or a standalone New still needs them. The nested create action redirects a successful request to the parent's detail page, post_path(@post), unless the create_form authors return_to. A failed request redirects to the same destination with errors.full_messages.to_sentence in the alert, because there is no form to render again.

The button becomes a toggle when three conditions hold:

  1. one current_account binding fills the signed-in Account's Reference, and the route fills the parent's;
  2. an unconditional uniqueness Validation covers exactly that pair of References; and
  3. destroy is selected and its record Policy compares the same Account Reference with current_account.

The parent's show action and its nested controller's index then run one scoped query, @post.likes.find_by(member: current_account), inside the existing collection permission. They keep the record only when allowed_to?(:destroy?, record) passes. With a record, the card shows button_to "Unlike", like_path(@own_like), method: :delete; without one, it shows the create button. The record takes its Entity's name: a Product's saves keep @own_save, though Rails names that route safe_path. Two toggles over one Entity on a page fall back to their collections' names, such as @own_follower_link. The undo label is Un plus the lowercased name for follow, like, pin, star, subscribe, and watch, and Remove <Entity> otherwise. It lives in the show page's collections.undo locale entry and in the nested index's own undo. The destroy request keeps its own lookup, authorization, and destination, so a Plan that wants Unlike to return to the Post authors destroy.return_to through like.post. Collection rows still carry no edit or delete control; the toggle belongs to the parent's collection header.

Generated request examples create through the scoped URL, follow the redirect, and expect the page to load; only an authored return_to is named. A toggle also gets an example that creates, finds the undo form on the parent page, destroys, finds the create form again, and creates once more. The browser example clicks the same buttons inside the collection card.

Preload

Public, item-authorized, and gate authorizations remain distinct. A protected collection or member lookup starts from the exact Policy relation scope when required. Environment gates authorize current_account; projection gates authorize the parent scaffold record. Relations use ordinary .preload(...) with the nested graph needed for Policy decisions, controls, and presentation. For example, a Message reply needs both its author for display and its thread's Conversation members for the read decision. Rails batches those association loads for the selected records. Denied associated records may enter server memory; allowed_to? still guards their content, and destination-link and control checks retain their separate decisions.

Action and collection-wide gates authorize before presentation loading. A gate that needs the scaffold record loads its decision dependencies first. Direct record preloading remains useful after that gate, for an existing current_account, and for a newly built record; these contexts do not have a relation awaiting materialization. Preload paths stay relative to the record that owns them. The nested graph avoids repeated lazy loads and batches traversal queries; authorization scopes, member checks, mutation checks, Ordering, and pagination remain in place. A full collection page preloads only what its parent's authorization reads, because it renders no parent presentation beyond the descriptor. A child built through @conversation.messages.build already holds its Conversation when Rails infers the inverse, so its preload leaves out a path that reaches only the parent or what the parent's lookup already loaded. Deeper non-public projection shapes remain omitted pending separate qualification. A representable preload graph alone does not establish support or alter the reviewed GapSet.

The pinned Rails 8.1.4 preload API accepts nested Hashes and Arrays and batches separate association queries. Action Policy 0.7.6 supplies the record checks and controller API. These APIs preserve the separate choices of which records belong to a relation and which content or actions are authorized for each record.

Web main navigation is target-specific. It admits public indexes, current-account-protected entries, explicit current-account gates, and one authenticated /account destination with optional Policy-controlled profile details. Selected iOS and Android clients lower this same navigation into native tabs, Account and protected entries included. The Scaffold slice introduces no target-neutral navigation contract.

Web navigation consumes this exact Scaffold result. Public indexes link directly; item-authorized entries require a current account; explicit gates call the selected gate rule; and profile links only after the profile rule. Fixed /account remains separate. Ordinary iOS and Android clients consume their public_only: true navigation projections, do not inherit profile or protected Web entries, and receive precise partial rows when the Web-only consequence needs disclosure. No target-neutral Scaffold navigation projection is invented.

Mutation destinations

Derive the Screens defaults (repository-only) from the emitted interaction. Standalone New/Edit redirects to the saved record; scoped associated create returns to its collection, and a no-input one returns to the parent's detail page. Delete controls appear on record details, and destroy defaults to that record's collection. Without one, it lands on the detail page of the record's sole parent: the one Entity whose detail page lists this Entity through a required Reference. When the deleter may not read that parent, or no sole parent exists, destroy lands on the existing home route. The emitted destroy checks the parent's show authorization with allowed_to? before deleting, unless that check is the Account gate the destroy already passed. If a preferred form-return detail is unavailable, use its admitted collection, then home. Profile editing returns to profile_path. Optional Plan return_to overrides retain their admitted resource meaning. Emit direct route expressions, including explicit overrides. Resolve a record-dependent destroy destination before deletion. Omitted override routes and current-location overrides remain gaps; they do not silently become defaults.

Supported redirects consume no submitted destination URL and need no validated_return_to, hidden URL, or mutation-record placeholder token. Ordinary New/Edit links have no navigation query parameter. Scoped forms derive their parent only from the URL and preserve its lookup, authorization, inverse assignment, and validation-error rendering without submitted context fields. Destroy retains its authored record lookup and authorization; it consumes no parent context. The removed collection-row controls no longer need a separate contextual-delete helper. Supplied URLs cannot override these server-owned destinations. Referer and session history play no role. Rails' public url_from remains appropriate if future supported behavior needs a submitted location; the current slice does not need one.

Failed form-backed creates and updates render the same form with values, errors, and parent context. Successful form-backed mutations use 303 See Other. A selected standalone create without New or an explicit return uses empty 201 Created/422 Unprocessable Content responses even when associated forms share its definition. The scoped invocation retains its form, parent, and redirect. Authored standalone returns retain their redirects without a form; a request-only update returns empty 422 on failure. Destroy emits ordinary destroy! and its 303 redirect. A rescue covers a :restrict_with_error or polymorphic before_destroy refusal, reached directly or through emitted dependent: :destroy Associations:

def destroy
  @household.destroy!
  redirect_to households_path, status: :see_other,
    success: t(".success", descriptor: @household.name.truncate(60))
rescue ActiveRecord::RecordNotDestroyed => error
  redirect_to households_path, alert: error.record.errors.full_messages.to_sentence, status: :see_other
end

Only a completed delete reaches its success message. A refusal redirects to the same destination with the alert alone. error.record is the refusing record. Rails rolls an indirect refusal back without copying the restricted child's errors onto its parent, so the parent's own errors would leave the alert empty. The review of a Rails pull request points to exception.record.errors for this. Through a one-to-one cascade, pinned Rails' has_one calls the child's non-bang destroy and aborts the parent without an error, so error.record is the requested record with no errors: the refusal keeps every record, and the alert is empty. The GapSet lists each restriction reached that way. Save validations, AASM initialization, and database-only restrictions do not add the rescue, and no runtime callback inspection or broad exception rescue is emitted. A developer adding a destroy restriction later may also need to add controller feedback. This follows the direct-route structure of the Rails 8.1.4 scaffold; the decision (repository-only) records the comparison and retained context.

Success messages

Each generated create, update, and destroy whose success ends in redirect_to sets a Rails flash after status:, as the Account update does:

redirect_to goal_path(@goal), status: :see_other,
  success: t(".success")

Failure branches and head responses keep their current shape. The key follows the Rails I18n guide's lazy lookup: <controller_path>.<action>.success, such as goals.create.success, in config/locales/scaffolds.en.yml. A missing key fails the generated request specs through raise_on_missing_translations. The copy is short and past tense: "Goal created.", "Goal updated.", and "Goal deleted." Only destroy names the record, and only when the Entity's primary descriptor is a short text Field on the record itself. It reads that Field through @habit.title.truncate(60) into "Habit “%{descriptor}” deleted.", because the deleted record is no longer on the page it lands on.

A one-tap toggle stays silent: its create, and the destroy behind its undo button, redirect without a message, because the button's state already shows the result. A one-tap create that is not a toggle, such as a check-in, does send its message. When one create serves both a toggle parent and a form-backed parent, the toggle branch alone stays silent.

The Delete confirmation keeps its title and adds one static sentence per Entity naming the Associations a delete removes with the record, such as "Deleting this goal also deletes its habits." The Compiler derives it from the cascading dependent: Associations that ReferenceDeletion emits, :delete_all and :delete included because they remove the same rows. It names no individual records. The UI owner describes the toast that shows these messages.

HTML-only responses

Generated controllers are HTML-only: the shape of the Rails 8.1.4 scaffold without jbuilder, with no respond_to blocks and no .json.jbuilder views. No Plan can request a JSON surface, so applications do not ship the jbuilder gem either; Rails frameworks explains why. Other formats get ordinary Rails answers:

The Compiler adds no application-wide format restriction, which would answer the Policy denials above with 406 instead of 403. An owner who adds an API can run bundle add jbuilder, which also brings back its scaffold generator, or use render json: as the Rails API guide does.

Generated source

Local names

Generated resource vocabulary stays conventional. The Compiler preserves authored public model, controller, route, instance-variable, parameter-scope, helper, and table projections; it does not globally rename every record to a the_... form. Public index record scopes and one-hop Reference-option maps allocate their compiler-private bindings. The mutation index declares its unparenthesized link_to helper; its remaining helpers are parenthesized or use _path-suffixed names that cannot equal the unsuffixed record binding. A nested controller's parent keeps its own name, such as @goal from set_goal; a parent of the child's own Entity becomes @parent_comment. Other generated Scaffold scopes use fixed compiler-private locals. Generalized Scaffold output uses @cases.each do |case_record| for reserved case, preserving public Case vocabulary. The retained legacy public-index and private Policy-gate allocator lets an authored-derived local win unless it is a target-profile Ruby keyword or collides in scope; later helpers move only when needed. There, Movie uses movie, Case uses the_case, and a helper already named the_case becomes the_the_case. An Entity named Descriptor may keep descriptor while a same-scope display helper becomes the_descriptor. The mutation index reserves link_to, so Entity Link To uses the_link_to there while retaining public LinkTo, /link_tos, and link_to_path. The current Analyzer still blocks generated constants colliding with Class, Module, or Thread as an implementation exception; the target rule treats them as gaps and omits or partially generates affected output unless no bootable residual exists. The Rails profile owns the exact keyword set and allocation rule. Redirects and Cancel links use route expressions rather than shared return_to or cancel_to instance variables. A form partial takes its record under the Entity's own name, as the Rails scaffold does: posts/new renders render "form", post: @post, cancel_to: posts_path, and _form declares <%# locals: (post:, cancel_to:) %>. The same allocator protects that local: it gains the_ when the name is a Ruby keyword or one of the form's other names (aria, cancel_to, form, form_url, or parent_record), so Case forms take the_case:. Scaffold, Account, and the retained public-index forms share this rule, as does the Account page's registration_details partial.

The renderer emits ordinary inspectable Rails source from the immutable admitted projection. Resource routes use the exact helper, path, controller, action, and parameter names that target qualification checked. Singular profile routes are separate from resources; omitted or incoherent route and definition pairs do not enter task discovery and remain listed gaps.

Projections and display

A projection recursively renders admitted Fields and Associations in authored order. A singular Association without children displays its qualified target Primary Descriptor. Root show and profile collections receive their own scoped query for direct Associations and the admitted indirect shape. A singular item immediately inside a collection may retain its own item Policy. Collection projections on indexes or below another Association projection, other indirect query shapes, and deeper non-public presentation remain precise omissions.

Date and datetime values use Rails localization with the standard :long format, including the year in the default English locale. Applications can override the locale's date and time formats. A URL property links to its address in a new tab, as URLs describes; a URL Primary Descriptor still links to its record. False remains “No” and zero remains 0.

A required Field renders plainly: <%= l(event.starts_at, format: :long) %>, number_to_currency(order.total), or a Yes/No ternary. Only a nullable Field keeps missing-value copy, in a (event.ends_at).then { |display_value| … } block that shows t("shared.values.missing") for nil. Reference option labels follow the same rule; their Primary Descriptors are always required.

Enum labels go through one helper, enum_label(post, :status), which the Compiler adds to ApplicationHelper, with a spec, only when a generated page shows an enum. It reads the enums.<model>.<field>.<key> entry with I18n.t rather than the t view helper, so a value key ending in _html still renders as escaped text. No enum translation library or model wrapper is generated. A State Machine whose model declares its AASM behavior shows post.aasm(:status).human_state, which reads the authored State name from AASM 6.0.0's translation keys, activerecord.attributes.<model>.<field>/<state>, and falls back to a humanized state name instead of raising. A storage-only State Machine, such as an optional one, has no AASM machine for its Field, so its view reads the same key with I18n.t. The scalar Field label stays beside those keys, so the attribute name and state names can both be translated without a nested-key collision. A nullable enum or State Machine Field checks nil? before any of these calls.

Details and new/edit forms use the index card width. A details card has one h1, then right-aligned ghost actions with matching icons: an explicitly named index destination such as “All movies”, Edit, and Delete. Native requests omit the web index-return control. A card without any of these actions has no toolbar, and a collection card without an Add or one-tap control has no action list. The scalar primary descriptor is not repeated below its title. Other properties use one-column dt/dd markup without attribute dividers; top-level associated collections occupy sibling cards. Form action buttons use the stock large preset. A standard Tailwind container query stacks full-width form actions in narrow cards and uses natural-width actions at the named @md breakpoint; fields and actions have no divider.

An authored Association projection contributes a details destination only when its target show route and exact authorization survive admission. Its descriptor is an ordinary link; selected child properties may carry a View control. A row whose projection omits its own descriptor keeps a separately authorized View control for its own show route. This keeps multiple authored Association destinations usable without nested links or choosing one destination for the entire row. The renderer never invents an unauthored reverse Association or removes explicit join properties.

Rails collection rendering composes entity-owned row partials. An index uses, for example, credits/_list_item; a matching associated projection reuses it. A parent's own projection keeps the Entity's name in the nested resource's directory, such as movies/credits/_credit and people/credits/_credit, shared by that parent's preview and full collection page. Both name it by its root-qualified path, as Campfire's account page renders accounts/users/_user and Herb's actionview-no-implicit-partial rule asks. render @credits would find the file only from the nested controller, and only when the segment is the table name; a Unit's requests collection lists Maintenance requests. A show page composes its main record partial, such as movies/_movie, and sibling collection cards. Only selected presentations are emitted. Strict locals name the record and its presentation options; DOM IDs distinguish a main record from its rows in each collection. This provides reusable Rails and Turbo targets without adding automatic live updates.

Inputs and copy

Create and update accept only admitted controls and strong parameters. An enum select and every admitted read-only enum projection, through enum_label, look up the same enums.<model>.<field>.<key> entry in config/locales/scaffolds.en.yml. The generated English catalog preserves each authored value name exactly; select options retain authored key order and the empty choice. Editing that locale or adding another Rails locale changes both displays without recompiling the Plan. Submitted and stored values remain stable keys. Enum admission and State Machine presentation are unchanged.

Screen, action, form-submit, and pagination copy follows the emitted pages and controls. Model, attribute, Reference, State, and error labels remain available, as do translations used by retained Core components. Enum labels, the Yes/No and “Not set” value copy, and the navigation label are written only where an emitted page, form, controller, or helper reads them. With an Account, authentication.en.yml labels the navigation beside its sign-in links. An Edit form can also remove a stored optional upload. Account edit/form/update copy follows the selected editable self surface. A custom profile's Association View and collection labels follow those rendered projections.

Server bindings run after submitted inputs; current_account is valid only for an admitted Account Reference. Associated create builds through the authorized parent's collection, applies bindings, authorizes that initialized record, and then saves it. The scoped form omits a selected inverse-parent input and excludes its submitted value; standalone and other-parent forms retain their remaining inputs. A create whose context accepts no editable input calls ordinary build or new without a parameter helper, so a normal submission need not include a model parameter; each controller decides this for its own create. A server binding cannot repeat the parent assignment.

Static collection/profile returns carry no record. A mutation-record show may use no step or one required Reference step; the latter uses its foreign key without loading an unneeded record. Destroy cannot return to the deleted record's own show.

Controller callbacks

Entity controllers follow the Rails 8.1.4 scaffold: callbacks first, then public actions, then private methods. Oscar Party's MoviesController opens with:

before_action :authorize_manage_movie_catalog, only: %i[index new create edit update destroy]
before_action :set_movie, only: %i[show edit update destroy]
verify_authorized

The private set_movie runs @movie = Movie.find(params.expect(:id)). A member finder appears when two or more of show, edit, update, and destroy run the identical lookup, Policy scope and preloads included. Its only: list names exactly those actions in scaffold order. An action whose lookup differs keeps its own, as the public show beside scoped writes does in the Rails Wishlists guide. A show that preloads presentation data the writes do not read also keeps its own lookup, so edit, update, and destroy load nothing they ignore. Route IDs read params.expect, as the scaffold does. Record authorization stays in each action. The finder only looks the record up, so a record outside the Policy scope still answers 404 before any authorize! runs.

An Account gate such as authorize! current_account, to: :manage_movie_catalog? becomes a callback named for its rule. It is declared before any finder, so it still runs before the lookup it guards, and it covers every action that opens with that gate. A gate that follows a lookup, such as a collection gate, stays in its action.

A nested controller, such as Goals::HabitsController, loads its parent for every action in before_action :set_goal from params.expect(:goal_id), after any Account gate its parent or create opens with. The lookup goes through the parent's Policy scope when its show page's does, and each action then authorizes the parent as that page does. A parent whose show page is public or gated keeps that page's unscoped find, and its gate runs as there. index reads the collection, and new and create call @goal.habits.build.

The Account profile's AccountsController declares before_action :set_account after before_action :authenticate, as the editable Account page does, and so does each profile collection's controller, such as Accounts::LeasesController. Generated applications set raise_on_missing_callback_actions, so each only: list names emitted actions; a callback that covers every action omits only:.

Authorization in generated source

Every protected mutation authorizes before writing. Generated controls repeat the action's consumer rule.

Account always resolves current_account. The Account link and credential forms require authentication.

Remaining target gaps include unsupported Field/control kinds, longer or optional return traversals, broader relationship query plans, richer formatting, state-machine controls, and deeper protected nested descendants. The reviewed GapSet fixtures hold the exact representative matrix.

Code and checks

A separate narrow browser-only Movie Catalog qualification uses a one-hop Association Primary Descriptor for Reference inputs. Its option query eager-loads that hop, orders by the terminal descriptor and target id, and maps label and ID pairs from one result query. The target Entity's public form selects the descriptor Association explicitly. The script materializes that application, verifies its exact manifest, migrates and schema-loads fresh databases, and repeats the same request and persistence checks after both setup paths. It proves create with the required Director and either no Consulting Director or a selected Consulting Director; nil-to-target, target-to-another-target, and target-to-nil Consulting Director updates; logical conditional-presence errors and corrective resubmission; required and optional HTML requiredness; one-query Association-descriptor option ordering, strict eager loading, zero-query cached label reads, deterministic descriptor options, and no option query during successful writes. The generated forms do not expose unselected attributes. Model-invalid writes render logical errors with 422; valid writes persist only admitted inputs and redirect with 303 to the selected same-Entity index or mutation-record show. A submitted non-nil optional Reference ID whose target no longer exists can still reach the database foreign key and raise; friendly stale-option handling remains later work. Index titles reach the detail page. That page renders the independent Movie descriptor followed by notes and title in authored order, preserves multiline text, escapes authored HTML, omits unselected data, and includes back and edit links. Ordinary missing records return HTTP 404; index/edit links and forms carry no return URL. The former unsafe destination 400 checks were retired because the controller no longer consumes a submitted location. The separate redirect-default smoke (repository-only) checks that an extra submitted URL cannot change the server-owned destination. The generated iPhone project remains outside this browser-only show proof.

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.