Generated Rails tests
Status: Implemented for the bounded generated application. Broader coverage is an evidence-backed direction.
Every generated application carries its own RSpec suite. The Compiler emits FactoryBot factories, model examples,
request examples, system specs for constructible forms, and query-count checks for selected indexes. Each example
exercises behavior the Compiler generated or a page that calls behavior it did not write, using records that bounded
setup can derive. A scope the Compiler did not write raises until the owner writes it, so the examples that load its
pages fail through it. A deletion outcome a refusing polymorphic target leaves out fails its own example, and so does
a Validation the model declares with an empty method. The README's
Behavior to write lists them all. When setup cannot supply
valid data, the generated README lists the missing test under Test coverage to complete instead of emitting a
placeholder. The application's own bin/ci runs the suite. Application CI owns application behavior,
while First Draft and Core qualify the Dev Container lifecycle and the production-image harness upstream.
Foundation quality (repository-only) states the verification policy these
tests serve, and the Rails target profile routes to each subject's lowering.
On this page
- Test ownership
- Stack and spec paths
- Model examples
- Request specs
- System specs
- Query-count specs
- Test coverage to complete
- Behavior to write
- Factory samples
- Bounded setup and sequences
- Code and checks
Test ownership
Generated application CI owns application behavior, lint, security, setup, and the production-image smoke.
First Draft owns the fixed profile's Dev Container lifecycle qualification upstream. Core tests its exported
application, and the same upstream harness can test an actual committed Compiler output. Its specialized script
and workflow are excluded from the application archive. Applications retain .devcontainer, setup, tests, and CI
without a remote First Draft dependency. After changing dependencies or development-container configuration,
owners rebuild and test that environment or add dedicated coverage; ordinary PR CI does not detect every breakage
there. Compiler identity substitution and composition are qualified here against actual Compiler
output. Core's former Photogram
rename smoke duplicated that substitution with a second implementation; it and the Core layer-inventory test are
retired instead of being carried into each app. The upstream Dev Container lifecycle gate exercises a renamed
checkout, setup, boot, CI, and a warm restart; its separate configuration-only template smoke is redundant.
Application owners can replace the starting home action or add attachment processing without a test requiring the
original template shape. State Machine, Policy, and Scaffold behavior tests remain application-owned.
The application production smoke builds its actual Docker image, runs the normal entrypoint with a disposable
PostgreSQL database, checks applied migrations plus /up and /ready, and requires a non-root runtime user. It
follows the application's declared runtime configuration. Root-page status and a particular non-root UID are not
requirements: an authenticated application may redirect / to sign-in. Application owners test their own jobs and
channels. Core's archive-excluded production harness owns package exclusions, a real production 404, and synthetic
Cache/Cable/Queue concurrency under a named Core configuration. The synthetic job does not ship or reserve an
application model name. Core CI automatically qualifies exported Core; the same harness accepts a clean committed
generated repository. The application smoke does not cover production
page rendering or provider configuration; deployment remains separate.
Applications retain three guards for built-in production Queue worker/dispatcher and Cable polling defaults.
They enforce a conservative one-second floor after clearing environment overrides, explain idle database traffic,
and link the application's editable policy in DEPLOY.md. They measure neither billing nor a universal safe
interval. Default intervals and runtime overrides remain unchanged.
Stack and spec paths
The Rails target emits RSpec examples and FactoryBot factories under ordinary spec/ paths. Core configures the
Rails generators to continue that stack. Factories provide valid model setup; request examples send explicit HTTP
parameters and log in through Rodauth. Factory creation does not stand in for registration or its Sequel write path.
Account request examples separately exercise the default page's guest guard and the emitted Terms/registration
assignments through HTTP. Authored-profile guest coverage is reused. Registration samples must be valid, and the
assignment example needs at least one value distinct from its default; missing setup is disclosed in the README.
The Account owner (repository-only) describes this bounded coverage.
Request and Policy setup uses ordinary FactoryBot records without blanket reloads or preloads. Descriptor reads and
membership assertions retain their intended relationships; application controllers still preload the associations
their requests need. Core's Rails :rescuable test setting renders exceptions with HTTP mappings, such as scoped
404s, and raises unmapped exceptions such as NoMethodError. Resource suites need no global exception override.
Dedicated error-page specs scope production-like presentation and deliberate 500 rendering to their own examples.
Standalone resource forbidden HTML examples for a signed-in account check status, format, a nonempty body, and
access or nonmutation assertions. A denied guest's HTML example instead expects the redirect to /login and the same
nonmutation checks. For a GET page it then signs in and expects Rodauth's redirect back to that page, unless the
Account factory is unavailable. Shared error-page specs own the heading, explanation, and recovery navigation.
Non-HTML denials keep their empty-response checks; hidden scoped records still return 404.
An authored name can coincide with a Core spec basename. In that case its resource suite uses
spec/requests/<collection>/scaffold_spec.rb; an otherwise colliding State Machine suite uses
spec/models/<stem>/state_machine_spec.rb, and a colliding form system suite uses
spec/system/<collection>/forms_spec.rb. RSpec discovers these ordinary subdirectories. Unaffected suites retain
flat paths, and test-file ownership does not reserve additional Entity names or rename application models and routes.
A Field name can also coincide with a method that FactoryBot's definition block already answers, such as sequence,
after, or concern. FactoryBot would read sequence { 1 } as its own sequence method, so that factory attribute
uses FactoryBot's documented add_attribute(:sequence) { 1 } reserved-word
form.
Other attributes keep the bare title { "title" } form, and these names stay available to Fields.
The current Compiler emits ordinary progressing factories and resets only named bounded sequences. It retains Core's
Rails :rescuable test default and ordinary FactoryBot setup. Expected scoped 404s and explicit 403s retain their
checks; unmapped exceptions such as NoMethodError raise.
Model examples
The bounded application layer gives each constructible Entity a factory and model examples for its emitted required
and optional values, normalization, numerical input, References, and supported unconditional validation boundaries.
Ordinary presence, length, and numericality requirements use Shoulda matchers with a valid factory subject where
available. Required numerics use numericality and explicit nil rejection; booleans retain false acceptance.
Numericality matchers qualify only authored bounds, such as .is_greater_than_or_equal_to(0) on a money Field.
Compiler tests check the integer four-byte range and the money decimal(12, 2) bound, so removing either from a
generated model fails no generated example. A URL Field's example checks an address, a blank value, and a rejected
value against the :url message, which isolates UrlValidator from requiredness and other rules on the attribute.
The validator's own spec/validators/url_validator_spec.rb uses an anonymous ActiveModel class and type: :model, the spec type where Shoulda includes its matchers. An image Field's example uploads the PNG fixture and
then the text fixture, and checks only the :image error kind. spec/validators/image_validator_spec.rb checks the
rule on an anonymous ActiveModel class with test doubles. When a page shows an enum value,
spec/helpers/application_helper_spec.rb gains an enum_label example. It builds the first such record with its
first enum key and expects the authored label from the locale, beside the uploaded_image_tag examples that image
Fields add. Optionality uses allow_value(nil).for(:attribute) to check the whole attribute rather than one
validator's nil option. A cycle of required relationships, the only factory limitation, keeps independent attribute
examples on described_class.new and discloses examples needing complete records. Normalization examples use
Shoulda's normalize
matcher
with concrete from and to values for the authored pipeline, including blank-to-nil cases. These examples
exercise Rails' public normalizer; future custom setters or persistence interactions need their own application
behavior tests. Assignment and persistence qualification remains in the upstream normalization
smoke (repository-only). An exact length of one combined with
blank-to-nil normalization checks its upper boundary with :wrong_length; the empty lower sample becomes nil, which
the emitted length validator allows. Uniqueness examples create a validated seed and use validate_uniqueness_of on
the authored logical error target, with all physical scope attributes. Case-insensitive columns use
case_insensitive; downcased values use ignoring_case_sensitivity alongside the retained normalization example. A
text seed unchanged by swapcase also uses ignoring_case_sensitivity: duplicate rejection remains checked, but
that seed cannot establish case behavior. When more than one realized uniqueness declaration shares the logical
error target, or that target is a one-to-one Reference, the example instead builds the complete duplicate tuple and
checks :taken on the target. The matcher cannot distinguish overlapping validators with the same error message;
changing a scope can still violate another rule. All model validators remain. Revisit these handwritten fallbacks
when the pinned matcher can isolate overlapping same-message validators, so that matcher examples pass for one
Entity that declares both uniqueness(title, studio) and uniqueness(title). The repeated saved-seed validity
check is omitted; create-time and update-time validation can differ. Existing setup limitations and
application-specific persistence and database checks remain. Date-boundary examples retain explicit Gregorian Date
inputs; the comparison matcher's stringified samples change calendar interpretation near the admitted 1582-10-15
boundary. Realized Reference deletion rules receive concrete model examples where the existing factories supply
their records. They check cascading removal, retained records with cleared References, or rejected deletion with
both rows retained. A cascading join example also checks that its other required referenced records survive.
Restriction follows the realized model rejection or ordinary foreign-key exception. A polymorphic target that
refuses its own deletion keeps its example: a restriction passes, and a
cascade or nullification fails with ActiveRecord::RecordNotDestroyed until application code writes it. When a
record cascades to two children and one restricts the other, an example deletes the record with both and checks that
all three are gone, which proves the declaration order the Associations owner
describes. These examples assert outcomes rather than dependent: options; factory cycles and unsupported
conditional setup retain actionable coverage omissions. State Machine event examples use ordinary persisted records
and event methods; attribute-only checks use new records.
Generated model tests exercise initial state, closed-domain storage, direct-assignment rejection, allowed and rejected events, and transition effects. A stubbed event keeps its allowed and rejected examples. The Compiler's Oscar runtime qualification checks the AASM method census used by collision admission. Dependency versions belong in the Gemfile and lockfile; generated tests do not freeze a gem's private methods or configuration. Omissions are not duplicated as skipped desired-behavior tests. No generated example covers a named Predicate or Ordering scope directly; request and query-count examples reach it through the pages that call it.
Request specs
Policy examples exercise permitted and denied records and actual relation results. Realized Scaffold resources get
complete request examples for their routes where setup is derivable, including persisted writes, invalid required
input, and protected access. Other-Account denial examples name the record owner separately from the signed-in user.
Denied updates retain the expected 404 for scoped lookup, a sign-in redirect for a guest at a Policy gate, or 403
for a signed-in account at a Policy gate. They compare every persisted target-record attribute and intentionally
omit a blanket model-row-count invariant; creation count checks and destroy existence checks remain.
Error-kind examples use RSpec's ordinary predicate matcher on record.errors,
keeping the error object and expected attribute/type available in a failure. They do not reduce the requirement to
generic invalidity or translated message copy. Protected descriptors receive their own permitted setup. Examples
assert observable data and access behavior rather than exact scaffold copy, HTML classes, helper inventories, or
default redirect destinations. Index and show examples find their record by Rails'
dom_id, the record
identity that Turbo Streams also target: the index row's dom_id(record, :list) and the show page's dom_id(record).
A text or UUID descriptor is checked as well. An index that filters rows by Policy answers a guest or another Account
200, so those denial examples check that the record's row is absent. Without that check, an index that dropped its
Policy scope would pass them. A parent's collection page whose rows a child Policy filters gets a listing example:
the signed-in account's child appears on that page and in the parent page's preview, found by
dom_id(record, dom_id(parent_record, :habits)) for a Goal's Habits. When that Policy compares a Reference other than
the parent, a second example gives the account's parent another Account's child, which must be absent from both
pages. Without it, a collection page or preview that dropped the child Policy scope would pass its suite. On the
Account profile the parent is the account itself, so a Policy on that Reference gets only the first example. A child
Policy that goes through the parent Reference on another page, a membership rule, or an indirect Association keeps
the page under Test coverage to complete. Collection pages whose rows no child Policy filters get no listing
example. Request specs do not have view helpers, so specs that use dom_id include ActionView::RecordIdentifier,
the module that Turbo's own test assertions delegate dom_id to. A collection-level gate denial needs no collection
records, so it remains covered even when a valid collection factory cannot be derived. Member requests and scoped-list
exclusions retain the records their assertions need. Each associated create form receives independent New and create
request examples, including associated-only forms. A nested resource's listing, New, and create examples share its
own suite, such as spec/requests/goals/habits_spec.rb for a Goal's Habits, as its controller is
Goals::HabitsController; its create journey is spec/system/goals/habits_spec.rb. Each selected standalone POST
receives its own coverage independently of New, an authored return, or associated forms. A partially generated
create, standalone or associated, with no source for a required value keeps an application-completion step instead of
a false successful example; the other context's coverage remains independent. Permitted setup satisfies the parent
page, collection gate, and create Policy; collection item filters do not gate New. POST examples check submitted
values, the route-bound parent, and each server binding, including competing submitted IDs for current-Account
bindings. Write examples
check image and attachment inputs with be_attached, since an upload reads back as an attachment. An optional upload
that an Edit form can remove gets an update example that attaches a file, submits only its remove_<name> flag, and
expects no attachment; the Account profile gets the same example. Protected routes
receive guest denials and, for a parent record Policy, another Account's parent request. Denied POSTs preserve the
record count. Parent and collection Account gates also get a signed-in denial when a direct Account Reference rule can
independently deny access. Separate parent and collection record Policies and create Policies needing custom draft
setup retain actionable README omissions. A Message created from its Conversation can have a create Policy that
delegates to the Conversation page's own record Policy through the URL-bound parent Reference:
allowed_to?(:manage?, record&.conversation, with: ConversationPolicy). The permitted parent already passes that
Policy, so the New, create, invalid, and other-Account examples and the browser journey need no other setup and are
emitted. Invalid-input examples omit an editable required value, never the URL-bound parent. No-input POSTs need no
model parameter; parent and Account spoof checks remain in the emitted examples. For CREATE, existing direct or
conjunctive Account Reference Policy setup can supply an authorized editable value; a nested create submits the
signed-in account itself, since its parent setup already covers any delegated rule. New retains its before-input
setup limits. A shared create
Policy that needs an editable Account Reference cannot authorize the empty New record; its omission asks the owner's
agent to complete application form-entry authorization while preserving submitted-record authorization. More fixtures
cannot repair that behavior. The emitted New page therefore denies every request, which conflicts with the
code-worth-keeping requirement and is a known Compiler defect (repository-only).
Omitting such a Policy input in a standalone or associated POST checks authorization denial rather than 422. A
successful form write without an authored return expects a redirect, follows it with follow_redirect!, and expects
the landing page to answer 200. The example does not name the default destination, which owners may change. It
follows only when its own account can read the landing page. That page is public, an index that filters rows by
Policy, a gate that admits any signed-in account, the write's own current-Account gate, or a record Policy that the
write already satisfies. A record Policy is
satisfied when it is the write's own or when the write's current_account bindings set every Reference it compares, and
an update does not reassign a Reference that Policy reads. Associated creates return to their parent's collection
page, or to the parent's own page after a one-tap create, and the setup already passes those pages' Policies. The
Account profile returns to the signed-in account's page. A delete without an index lands on its sole parent's page
when the deleter may read it, and on the home page otherwise; a parent page that is public or behind the delete's own
gate is its only landing. All of these follow. Otherwise the example checks only for a redirect with a Location, and
the README lists the landing under Test coverage to complete. One listing names an
update whose Policy admits an account that the record's show page denies; that account's save returns to a 404.
Its next step returns the write to a page every such account may read, because an example account that both Policies
admit would hide the 404. Request-only creates check 201 Created for public and otherwise
supported protected endpoints; selecting New is not required. Required-input failures retain 422, while
authorization denials retain their own status. A supported, explicitly authored return gets an ordinary redirect_to
expectation for the invoked standalone definition, associated form, or Account profile. Profile returns take
precedence over update returns; dynamic destroy destinations are captured before deletion. These authored-return
examples do not follow the redirect, so protected return-page setup remains a separate browser-coverage concern.
A scoped Reference input gets its own examples. Each page's
search has examples in that page's spec, such as new_place_options in
plants_spec.rb, signed in as the page's own examples sign in: it lists the signed-in Account's readable target
record and not another Account's; a picker that does not search has New, or Edit when its form has no New page, check
its select the same way. Every search also lists only the record whose descriptor contains the upper-cased query,
stops at 20 of 21 records, matches a wildcard pair such as %_ literally, and ignores a NUL byte. It refuses each
requester its page refuses, as JSON: a signed-out request, an Account whose Reference that the gate reads points
elsewhere, or another Account's edited record or parent, with 403 or 404; a profile search sends a signed-out
visitor to /login. When the Account can read only one target record, such as through a one-to-one owner
Reference, the matching and limit examples become listed coverage gaps.
Create and update submit the other Account's record and expect 404 without a write; a create whose own Policy
reads that Reference expects that Policy's 403 instead. Other permitted writes create their scoped Reference values
through the target's Policy for the signed-in Account. When that Policy is a membership the Account carries itself,
such as User.member_place, setup assigns the signed-in Account's own Reference instead of creating another Account.
An immutable membership Reference cannot be reassigned, so those examples become listed coverage gaps. When the
edited record's own Policy reads the Reference, Edit and update reuse the record's current value, because that Policy
already ties it to the signed-in Account and a new value could fail it.
System specs
Constructible resource forms also receive owner-local RSpec system specs. They visit the actual new/edit form, address controls by their public HTML names, select native options by value, and check persisted values after submission. These locators preserve repeated labels and authored Unicode whitespace; Core's accessibility checks confirm that controls have labels. Each supported form keeps its valid-first-submission journey. Ordinary rejection and non-persistence remain in model/request examples; required text does not add a generic browser recovery journey. Compiler browser qualification retains representative Turbo error re-render, retained input, correction, and relationship context. Owners add recovery journeys for concrete browser interactions or regressions as needed. This deliberately reduces per-form downstream recovery coverage. Associated creation visits each emitted parent context and checks its persisted inverse Reference. Protected forms use existing factories and bounded Policy setup, then sign in through the browser. Server bindings receive persisted-value assertions. Each journey whose action sends a success message checks that the page shows it; a one-tap toggle sends none. Core supplies Capybara, Selenium, and Turbo synchronization; the Compiler adds no browser framework. These examples do not assert a default redirect, other scaffold copy, or layout.
Selection reuses the existing bounded test data. Scalar text, number, checkbox, enum, and time-zone controls are supported; time-zone expectations use the canonical identifier submitted by the select. Date and datetime inputs use Capybara's typed values. Reference selection uses the visible picker and a qualifying related record with a distinguishable descriptor; a scoped input's record is created through the target's Policy for the signed-in account, and a public form cannot offer it. The native select's public name identifies its enhanced control. A searching picker types the record's label into its search, as a person would; another picker's option value identifies the label to choose. This does not test calendar navigation or the picker library's primitives.
Conditional validations, unconstructible factories or Policy setup, and inputs without useful bounded values retain
explicit missing browser-coverage disclosures. New forms need authorization before editable values are supplied; setup
cannot replace that application behavior with a permissive draft. Distinct parent and collection record gates need
owner-written setup; a parent's Account environment gate does not qualify its record for a collection gate. A
submit-only form has no generated browser journey. Its continuation suggests a submit-and-result browser example when
the action merits one; it does not ask the owner to invent editable inputs. Request proof remains separate. Associated
form setup includes the parent, create, and collection gates. Collection item filters only select displayed records;
they do not add sign-in or qualifying-record setup for New. The return collection may omit the created record.
Standalone forms account for their explicit return or default show/index page's Policy. Associated forms without an
explicit return use their already-qualified parent collection. An item-scoped index return needs its sign-in setup and
may show an empty collection; an environment gate still needs its Account setup. Direct return setup follows the
selected record: scaffold_record reuses the associated parent's setup, while mutation_record uses the mutation's
own setup. Sharing an Entity and Policy does not qualify a different record. Same-Policy reuse requires authorization
of that record; an Account environment gate qualifies only the Account. A protected return through one unchanged
Reference uses a qualifying related factory. A different Policy on the mutation record, conflicting Policy-owned
References, or longer protected return paths need explicit setup. These records support the submitted form's
navigation without asserting its destination. An edited Reference used by the mutation record's show Policy also needs
qualifying browser input, even when the form and show page use the same Policy. Other request and model coverage,
including denied access, remains independent.
Query-count specs
Selected index requests also compare query growth with n_plus_one_control at two and five records. Their ordinary
FactoryBot setup supplies readable records before measurement; the request must return their text or UUID descriptors
and each record's row dom_id, since factory descriptors can repeat.
Selection reuses a complete successful index setup and permits public access, current-Account gates, or a direct
Account Reference rule that can own several records. Account indexes, complex record rules, and any authored
uniqueness in the setup's factory dependencies remain application-owned query-test work. This conservative boundary
avoids exhausting finite sequences or reusing a unique Account relationship; it does not attempt to solve arbitrary
multi-record constraints. Bullet's separate N+1 checks still run across the emitted examples.
Test coverage to complete
When bounded setup cannot supply valid data, the generated README adds a Test coverage to complete section. Each entry names the affected behavior and spec or factory path, the concrete setup limitation, and the owner's next step. Selection and disclosures use the same derivation. Independent runnable examples remain; no empty, skipped, or pending placeholder example substitutes for coverage. Setup omissions describe missing tests. These disclosures do not change the frozen reviewed GapSet or introduce a Foundation Plan testing DSL, a generic constraint solver, or an LLM during Compilation. Straightforward derivable setup belongs in the Compiler. Each unwritten scope also lists its own spec here, in its model's spec path, because no generated example pins what a named scope returns.
Behavior to write
When the Compiler cannot lower a Predicate or Ordering, it emits an unwritten scope that
raises NotImplementedError, and the generated README adds a Behavior to write section. Each entry names the
scope and its model, says what it returns or how it orders, and names the pages that call it. It then names the
request specs with an example that requests those pages: the page's own index or show example, the Account profile
example, or a collection listing or one-tap toggle on that page. A gated collection's page adds none, and a profile
whose read Policy may deny an account adds no profile example. Their entries say which Policy admits the accounts the
page raises for. Without such an example, the entry names the model spec to add. An Ordering entry spells out its
terms; a Predicate entry points at its rule in .firstdraft/submitted-foundation-plan.json. The README also says that
NotImplementedError is a ScriptError, so generic rescues miss it and production answers 500, and that bin/ci and
a published repository's CI stay red while those specs fail. The introduction says to delete an entry once its specs
pass. Its Checks section says that until then the Tests: RSpec and Tests: System steps fail through that behavior and
every other step should pass. Its setup steps run the specs before the first preview. A deletion outcome left
unwritten gets an entry too: the model, the outcome, the Reference it follows, and the model spec whose deletion
example fails until it is written. A refusing polymorphic target fails it with ActiveRecord::RecordNotDestroyed, and
a target whose conventional inverse name is taken with the foreign key's ActiveRecord::InvalidForeignKey. A Counter
that counter_cache cannot maintain gets an entry whose example creates one counted record and fails with
RSpec::Expectations::ExpectationNotMetError while the column reads 0. A Reference default gets one too; its model
example builds a record without the Reference and fails the same way, and a default read from the signed-in Account,
which only the create request has, is listed under Test coverage to complete instead. A required Reference with a
model default gets no example that expects its absence to be rejected, because the written default supplies it before
validation. When the section lists anything but scopes, its introduction and the Checks sentence say "behavior"
instead of "scopes".
The generated AGENTS.md tells the agent to run the specs first and to write the behavior that Behavior to write
lists until its specs pass, then delete the entry: a stubbed scope or event raises NotImplementedError, a stubbed
Policy rule returns false, and the spec of other left-out behavior fails with the error its item names. It also says
to write every listed item, including a denying rule that no failing spec names. The first preview does not wait for
that work. The agent first writes any item whose entry says Home raises, then starts the preview and tells the user in
one line which pages deny or raise until the rest are written, without asking. The agent debugs any other failure as
its own: it fixes the cause, never skips, deletes, or loosens an example to get a green run, and changes a spec only
when the spec contradicts the behavior the application should have. When the cause is generated code, it adds one line
under "First Draft defects" in .firstdraft/design/implementation-notes.md and keeps working. The
owner's 2026-10-12 decision (repository-only) set this
order. A failure report (repository-only) that would send such defects to
First Draft is proposed.
The request specs those pages already get are the scope's dependents. They stay emitted only when their setup holds for every correct implementation of the authored meaning. An unwritten Ordering never decides whether a record is listed, so the index's listing and query-count examples stay and fail through the scope. An unwritten Predicate's listing example stays only when every operand is a stored-value Field of its own Entity and the authored rule admits the factory record. Otherwise Test coverage to complete lists it, because a correct implementation might exclude that record.
Unwritten Policies
A Policy stub gets an entry too. It names the rule and its relation scope, points at
the authored Policy in .firstdraft/submitted-foundation-plan.json, and lists the pages the rule guards without its
scope, which deny every account, and the pages that look records up through the scope, which raise. It names Home
when Home shows a raising index and the Policies that delegate to the stub, which follow it.
A stub's meaning is unknown to the Compiler, so no generated example asserts whom it, or a Policy that delegates to it, admits or denies. Spec setup treats both as unsatisfiable, so every example that needs an allowed or a denied record moves to Test coverage to complete, with a next step that waits for the stub. What remains holds for every correct implementation:
- the Policy spec's denial of a missing record, and of a guest when a conjunct the Compiler did lower needs the Account;
- one agreement example per stub with a relation scope, which creates factory records and checks, for two accounts and a guest, that the scope returns exactly the records the rule allows. It fails through the raising scope until both are written, so the stub's message names its spec file. Without factories for the record and the Account, the stub names the Policy spec to add instead. A Policy that delegates to a stub gets none, because a guard such as an unset Account Field could keep its scope from reaching the stub;
- every other emitted example, unchanged. An example that requests a page calling the scope, such as a New page whose Reference options come from it or a destroy that follows its redirect to the stubbed index, fails through the stub.
Unwritten events
A stubbed State Machine event gets an entry that names its bang method and model,
the states it moves between, and each effect it applies, points at the transition in
.firstdraft/submitted-foundation-plan.json, and says to add the event to the aasm block and delete its three
stub methods. The section's events introduction says that no generated page fires an event, so only its State
Machine examples fail, and that seeds and spec setup never fire one.
The State Machine spec keeps the examples an AASM event gets: one per source state it moves from and one per other
state it rejects. Each holds for every correct AASM implementation of the authored transition and fails at its first
may_ call until the event is written, so the stub's message names that spec. The examples check only the effects
that each fit the generated shape, an optional datetime Field on the same record set to the current time or nil, and
whose target no emitted Validation reads, since the examples' fixed values could otherwise make the record invalid
before the event runs. Test coverage to complete lists every other effect for the agent to check after writing the
event. Without a factory, the stub names the spec to add, and Test coverage to complete lists the transition as for
any event. Development seeds and Policy spec setup search only generated events. A seeded record whose state needs a
stubbed event is left out, and an example whose setup needs one moves to Test coverage to complete, with a reason that
names the event.
An optional State Machine has no AASM machine, so its stubbed events keep
only their allowed examples, which check the stored state rather than an AASM predicate. How a hand-written event
refuses is not known, so Test coverage to complete lists each event's rejected states instead. Its applicability is
left out behind an empty before_validation callback, and one example, "role begins in member where it applies",
saves a new record that meets the condition and expects the initial state. It fails with
RSpec::Expectations::ExpectationNotMetError until the callback is written. The Compiler builds that record, and the
allowed examples' records, when the condition is an equality with a literal on the record's own Field or on a Field
one Reference away, or a conjunction of those, and no emitted Validation reads a Field or Reference it sets.
Otherwise Test coverage to complete lists the example, and the allowed examples use the plain factory record.
Compiler qualification expects exactly those failures, as a strict expected failure does. The harness asks the
Compiler which scopes and events it left unwritten, including each stubbed Policy's relation scope, which controller
actions call each one, and which emitted examples request those pages. A stubbed Policy's agreement example calls its
scope directly, and a stubbed event's State Machine examples call the event, so their failures count outside a
request. It does not name an example for a gated collection's scope, or the profile example when the profile's read
Policy may deny its account, because that account may not reach the scope. A results formatter records each example's
outcome, its failure's cause chain, and the actions its requests reached. A failure counts only when one of those
scopes raised it, directly or as the cause of ActionView::Template::Error, inside a request to an action that calls
the scope. A system spec that visits a raising page, such as Core's baseline pages spec visiting a stubbed Home, fails
its assertions about the error page and then gets the stub back from Capybara, so RSpec reports both together. That
failure counts when one of its errors is the stub, raised in an action that calls it, and each other error is an
assertion rather than another exception. The harness also asks which behavior the Compiler left out. The Compiler
names each item by its GapSet entry's code and pointer, because several Field modifiers on one Field share a readable
path. Each example that pins left-out behavior carries the error class its failure raises, and the failure counts only
with that class: ActiveRecord::RecordNotDestroyed for a deletion outcome a polymorphic target refuses,
ActiveRecord::InvalidForeignKey for one a foreign key rejects, and RSpec::Expectations::ExpectationNotMetError for
left-out State Machine applicability, a left-out Validation, a Counter, or a Reference default. When the Compiler
cannot supply such an example's setup, Test coverage to complete lists the example instead. Left-out behavior with
neither a pinning example nor such an entry fails qualification. Every example the Compiler names must fail. The
harness identifies an example by its spec path and its own description, so it rejects a pinning example that shares
both with another example. An example that pins left-out behavior sits outside any context block, and its
description names the subject it pins, so no other example in its spec shares it. Other examples may still share a
description, for example when two authored length rules on one Field share a boundary. Any other failure, a pending
example, or an error outside an example fails qualification. So does a bin/ci step that reports no result, or a red
step other than RSpec. The map stays in the harness, not in the generated application, the GapSet, or the API.
Behavior to write covers unwritten scopes, Policy stubs, stubbed events, left-out State Machine applicability, left-out deletion outcomes, left-out Validations, Counter maintenance, and Reference defaults.
Left-out Validations
Rails calls a validation on every save, so a Validation the Compiler cannot lower is a write-path skeleton rather than
a stub that raises. Its model declares validate :method and an empty private method, named after the Validation's
key, whose comment names the Validation, its source pointer, and its spec. A Validation whose owner or error target
has no emitted storage stays only in the GapSet.
The model spec gets one example per skeleton, enforces booking.guests_within_capacity. It builds a factory record
that the rule rejects and expects the error kind the Rails validator for that rule adds:
it "enforces booking.guests_within_capacity" do
record = build(:booking)
record.venue.capacity = 1
record.guests = 2
record.validate
expect(record.errors).to be_of_kind(:guests, :less_than_or_equal_to)
end
The kind is pinned so that another rule cannot pass the example in the left-out rule's place: :exclusion,
:taken, :invalid, :blank (:inclusion for a Boolean), :present, and for a comparison the Rails comparison
validator's kind for its operator, with equals as :equal_to and not_equals as :other_than. Setup uses the
shared evaluator and bounded derivation: factory samples, the literals the rule names,
at most four Fields assigned to switch its condition on, and operands at most one required Reference away. A
uniqueness example saves one record and repeats its whole tuple on a second. Two related records that the factory
builds apart already break an equality between them. The rejected value must pass the generated rules on the same
attribute, so the example cannot pass through one of them. When setup cannot build such a record, Test coverage to
complete lists the example instead, with the reason.
Each skeleton gets a Behavior to write entry naming its method, model, Validation, source pointer, and error key, and
the GapSet consequence says the model declares an empty method for it. A left-out uniqueness also keeps the unique
index its tuple names, as data integrity describes. Qualification expects each
example to fail with RSpec::Expectations::ExpectationNotMetError.
An expected failure alone does not show that the example passes once the rule is written. The write-path smoke
therefore runs a completion probe after its ordinary qualification. It replaces each empty method with a hand-written
completion, as an owner would, and then requires every example to pass, FactoryBot.lint to build every factory, and
db:seed:replant to seed the development data from an empty database. A pinned example that stays red, a factory
that breaks a completed rule, or a seed record that does fails the smoke.
Factory samples
A factory infers a value for every rule the Plan authors on its record, lowered or left out. A text sample is the
first candidate that every emitted rule accepts and that no authored rule is known to reject. The candidates are the
Field's own key, a few stock words, the development data's literals, and strings that an admitted format generates
itself, shortest first: RubyPatternSamples enumerates the profile's character-class grammar, so \A[A-Z][0-9]+\z
offers A0. The evaluator checks length, format, exclusion, and literal and
same-record comparisons after the Field's normalizations and comparison mode, and a candidate that breaks a left-out
exclusion is passed over. A required Reference that an unconditional equality rule ties to a record one required hop
away takes that record, as venue { booking&.venue }, which still builds when an example sets booking: nil.
Where inference runs out, the factory still carries its best sample: the plain sample for the Field's kind. A comment
at the top of the factory names each rule the sample may break, such as # movie.unique_code needs distinct values, and these samples repeat, so a second record breaks it. Edit the sample if a spec fails. Factories are editable
setup; a spec that fails through one points at the sample to fix. Only a cycle of required relationships leaves an
Entity without a factory.
Bounded setup and sequences
Ordinary unique text uses conventional FactoryBot progression, such as sequence(:title) { |n| "title_#{n}" }.
Reaching the Compiler's 1,000-candidate search budget does not cap an ordinary text sequence without a format
constraint. Dates likewise progress in a direction without an authored limiting comparison when the sampled run
succeeds. These patterns do not promise valid values at every mathematical counter; Rails validation and storage
remain the boundaries if a suite reaches a real limit.
Constraints that end a sampled run early retain finite sequences. Arbitrary formats also retain bounded samples;
1,000 matches do not prove future matches. A format that no counter suffix fits, such as letters only or a fixed
width, steps through String#succ from a matching sample, as sequence(:code, Enumerator.produce("AA000", &:succ).take(1000).each), which stops after the values every rule accepts. For a two-character field, the chosen
c1 through c9 pattern describes this setup, not the whole valid domain. Generated RSpec support calls
FactoryBot.rewind_sequence(:factory, :field) only for emitted bounded sequences before each example, because
transactions do not reset Ruby sequence state. Exhaustion within one example remains visible. Ordinary and
application-added sequences keep progressing, and an application without bounded sequences receives no reset support
file. The existing missing-setup disclosures remain.
Date and datetime samples also satisfy same-record Field comparisons for the write that submits them. Factories, request and system writes, and nested creates share this rule. A write moves only the Fields it submits. Every other Field keeps what the record holds: the factory's value for an update, its current-time default, or nil, which makes Rails skip the comparison. Ordinary samples start in 2000. A comparison with a sequenced Field uses its first value. Contradictory rules stop after a bounded number of passes, and Rails validation reports the failure. Setup assigns at most four values to meet conditional rules.
An example whose write compares a sample with a current_date or current_time default freezes time with Active
Support's travel_to Time.utc(2026, 1, 1) do ... end block. For example, an update form may submit checked_out_at,
which must be after checked_in_at, and leave checked_in_at at the current_time default it received when the
example created the record. A sample that must follow the frozen time moves to 2027, offset by the write's variant, so
that update submits Time.utc(2028, 1, 1). Other examples run at the real time. A factory therefore assigns a
current-time default a literal sample when a comparison reads it against another value the factory sets, which keeps
factory records valid on any date. spec/support/time_helpers.rb includes ActiveSupport::Testing::TimeHelpers when
a model compares a Field with a current-time default. Development seeds run at the
real time and leave such comparisons unproved.
Conditional rules in setup
Every value a factory, seed, or example writes is checked against the Entity's conditional rules by one evaluator
over the Plan's authored meaning, FoundationPlan::Analysis::ValidationEvaluator. It reads a record's stored
values and answers whether a rule's when selects the record: true, false, or unknown when the condition reads
something it cannot see, such as a path through an Association. The Plan's own unknown, a test that reads a null,
selects nothing, as Rails' Boolean condition does for a total operand.
- Factories. When a factory record's own values switch on a conditional presence rule, the factory supplies the
optional Field or Reference it needs, as a hand-written factory would. A Profile whose initial lifecycle state
requires its User gets
association :user, factory: :user. A unique Field gets a sequence, even when a required Reference in its tuple already varies, because examples often create several records for one parent: an integer, decimal, money, date, or datetime counts up from its sample within its authored bounds, a URL takes"https://example.com/#{n}", and an enum or language-code Field walks its closed domain once. - State Machine examples. Before saving a record in an example's source state, setup assigns the values that state's conditional rules need. Before firing an accepted event, it assigns what the destination state needs and the event's effects do not set. An example whose record cannot satisfy them is listed under Test coverage to complete.
- Form writes and Policy setup. A request example whose submitted values break a conditional rule, and Policy setup whose record breaks one in any state its events pass through, are listed under Test coverage to complete.
Code and checks
The emitted suite runs through the application's ordinary bin/ci without reading .firstdraft/ or an authoring
context file. Compiler qualification separately checks composition, deterministic paths, dependency and method
censuses, and the exact reviewed GapSet. Its private Minitest helpers belong to this repository and do not require an
emitted Minitest test suite. Broader conditional validation, query, deletion-graph, and application browser coverage
remain follow-up work (repository-only). The private
AssociatedCollections browser qualification waits for the / destination of its turbo: false Login form before
explicitly auditing that page, so the audit does not scan the old Login document. Its initial Login visit
retains automatic auditing.