From 818e756c0e3f4759fa85a2e5d125b9e95d26882e Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Sun, 4 Oct 2026 19:05:25 -0500 Subject: [PATCH] Return Unlike to the parent and cite the Guide firstdraft/firstdraft#1005 ships the one-tap Compiler change tonight: a no-input associated create renders as a button on the parent page and returns there, and a uniqueness rule plus an owner-authorized destroy turn the button into a Like/Unlike toggle. The destroy keeps its own destination, so Unlike returns to the parent only when the Plan authors destroy.return_to through the parent Reference; otherwise it goes to the ordinary delete destination, which is Home for a Like with no index. The Skill's Like example authored no such return, so an agent copying it would send every Unlike to Home. The Like fragment is now a byte-for-byte copy of the Guide's No-input records example, including destroy.return_to through like.post, and the modeling cue and example link the public Guide anchor. The caveat that a deployed Compiler may still show a separate Create page is removed because publication follows the deploy. The associated-create return and entry-point passages name the no-input exception so they no longer contradict it. The same pull request sends guests who request a protected page to sign-in and returns them afterwards; the signed-in gate example now says so. The one-tap-like-record eval expects the authored return; it was not run. The Like fragment passes the schema test in test/repository.test.mjs. The tarball digest is left for the 0.8.4 release branch, which repacks it. --- evals/create-full-stack-app/cases.json | 1 + .../references/examples.md | 115 +++++++++++++----- .../references/foundation-plan-023.md | 11 +- .../references/modeling-guide.md | 16 +-- 4 files changed, 101 insertions(+), 42 deletions(-) diff --git a/evals/create-full-stack-app/cases.json b/evals/create-full-stack-app/cases.json index c626833..46ce948 100644 --- a/evals/create-full-stack-app/cases.json +++ b/evals/create-full-stack-app/cases.json @@ -150,6 +150,7 @@ "Models Like as its own Entity with required immutable References to Recipe and to the Account Entity, plus one-like-per-member uniqueness over both References.", "Gives Like's scaffold.create no inputs, binds its Account Reference to current_account, and authorizes it with a Like Policy or the signed-in gate.", "Selects destroy in Like's resource_routes, authorized by a Like Policy that allows only when its Account Reference equals current_account.", + "Sets Like's destroy.return_to to the Recipe show route with record from mutation_record through Like's Recipe Association, so Unlike stays on the Recipe page.", "Adds create_form {} to the displayed likes Association on the Recipe details page and leaves new and create out of Like's resource_routes.", "Models each cooking as its own no-input record created from the Recipe page, with no uniqueness rule over Recipe and the member and no destroy, so a member can record the same recipe more than once.", "Says plainly in the read-back \"one tap to like, one tap to unlike\" and that each Cooked it tap adds another record.", diff --git a/skills/create-full-stack-app/references/examples.md b/skills/create-full-stack-app/references/examples.md index abd81fe..a38bc77 100644 --- a/skills/create-full-stack-app/references/examples.md +++ b/skills/create-full-stack-app/references/examples.md @@ -561,7 +561,8 @@ GapSet consequences. ## Signed-in gate and one-tap records -These fragments come from a private family social network where every page requires sign-in. The Account Entity +These fragments come from a private family social network where every page requires sign-in. A guest who opens +any page is sent to sign in and, after signing in, returns to the page they asked for. The Account Entity `member` already has `read_self` and `update_self` Policies for its profile, like the example above. It adds one gate Policy with its own operation; any signed-in member passes because the gate record is the current Account. An Entity may have only one Policy per `operation`, so the gate must not also use `read`: two `read` Policies on `member` @@ -612,74 +613,126 @@ would use `"public"`, since the page's gate already applies. } ``` -This Like fragment is a copy of the example in the Foundation Plan Guide's No-input records section, which defines -the pattern and what Rails generates for it. A like is made by a tap: the post comes from the page and the member -from the signed-in Account, so Like's create has a binding and no `inputs`, and Like selects neither `new` nor -`create`. A member can take a like back, so Like is a toggle. Its uniqueness rule over `like.post` and `like.member` -allows one like per member per post, and its `destroy` is authorized by `like.manage_own`, which only the like's -member passes. With both, the post page shows one button that reads Like or Unlike: one tap to like, one tap to -unlike. Follows, RSVPs, upvotes, and bookmarks are toggles too. A record that may repeat, such as a "mark as read" -event or a check-in, keeps the no-input create but gets no uniqueness rule or `destroy`; a required time on it uses -a `current_time` default. Today's deployed Compiler may still show a separate Create page until the one-tap change -ships. +This Like fragment is a copy of the example in the public Guide's [No-input records](https://firstdraft.github.io/firstdraft/docs/architecture/design/foundation-plan.html#no-input-records) +section, which defines the pattern and what Rails generates for it. A like is made by a tap: the post comes from +the page and the member from the signed-in Account, so Like's create has a binding and no `inputs`, and Like selects +neither `new` nor `create`. A tap on Like creates the like and returns to the post page. A member can take a like +back, so Like is a toggle. Its uniqueness rule over `like.post` and `like.member` allows one like per member per +post, and its `destroy` is authorized by `like.manage_own`, which only the like's member passes. With both, the post +page shows one button that reads Like or Unlike: one tap to like, one tap to unlike. A toggle also authors +`destroy.return_to` through the parent Reference, here `like.post`, so Unlike stays on the post page; without it, +Unlike goes to the ordinary delete destination, such as Home. Follows, RSVPs, upvotes, and bookmarks are toggles +too. A record that may repeat, such as a "mark as read" event or a check-in, keeps the no-input create but gets no +uniqueness rule or `destroy`; a required time on it uses a `current_time` default. ```jsonc { - "subject_uuid": "019fb300-0000-7000-8000-000000000110", + "subject_uuid": "01a10369-3055-7edd-bb07-1d3835cd140b", "key": "like", "name": "Like", - "primary_descriptor": { "association": "like.member" }, + "primary_descriptor": { + "association": "like.member" + }, "references": [ { - "subject_uuid": "019fb300-0000-7000-8000-000000000111", + "subject_uuid": "01a10369-3055-7fdf-b8c1-89fa2e048fbe", "key": "post", "name": "Post", - "targets": ["post"], + "targets": [ + "post" + ], "required": true, - "immutable": true, - "on_referenced_deleted": "delete_referencing_record" + "one_to_one": false, + "on_referenced_deleted": "delete_referencing_record", + "immutable": true }, { - "subject_uuid": "019fb300-0000-7000-8000-000000000112", + "subject_uuid": "01a10369-3055-77bf-9eb6-72e155679db8", "key": "member", "name": "Member", - "targets": ["member"], + "targets": [ + "member" + ], "required": true, - "immutable": true, - "on_referenced_deleted": "delete_referencing_record" + "one_to_one": false, + "on_referenced_deleted": "delete_referencing_record", + "immutable": true } ], "validations": [ { - "subject_uuid": "019fb300-0000-7000-8000-000000000113", + "subject_uuid": "01a10369-3055-79e4-a51d-1d7ef064798c", "key": "once_per_member", "kind": "uniqueness", - "targets": [{ "reference": "like.post" }, { "reference": "like.member" }], + "targets": [ + { + "reference": "like.post" + }, + { + "reference": "like.member" + } + ], "nulls": "distinct", - "error_target": { "reference": "like.member" } + "error_target": { + "reference": "like.member" + } } ], "policies": [ { - "subject_uuid": "019fb300-0000-7000-8000-000000000114", + "subject_uuid": "01a10369-3055-7254-8716-e913cc529b55", "key": "manage_own", "operation": "manage", "allow_when": { "kind": "comparison", - "left": { "target": { "association": "like.member" } }, + "left": { + "target": { + "association": "like.member" + } + }, "operator": "equals", - "right": { "kind": "environment", "name": "current_account" } + "right": { + "kind": "environment", + "name": "current_account" + } } } ], "scaffold": { - "resource_routes": ["destroy"], + "resource_routes": [ + "destroy" + ], "create": { - "bindings": [{ "reference": "like.member", "value": { "kind": "environment", "name": "current_account" } }], - "authorization": { "policy": "like.manage_own" } + "bindings": [ + { + "reference": "like.member", + "value": { + "kind": "environment", + "name": "current_account" + } + } + ], + "authorization": { + "policy": "like.manage_own" + } }, "destroy": { - "authorization": { "policy": "like.manage_own" } + "authorization": { + "policy": "like.manage_own" + }, + "return_to": { + "kind": "resource", + "entity": "post", + "route": "show", + "record": { + "from": "mutation_record", + "through": [ + { + "association": "like.post" + } + ] + } + } } } } diff --git a/skills/create-full-stack-app/references/foundation-plan-023.md b/skills/create-full-stack-app/references/foundation-plan-023.md index ee310ac..9c800dc 100644 --- a/skills/create-full-stack-app/references/foundation-plan-023.md +++ b/skills/create-full-stack-app/references/foundation-plan-023.md @@ -584,10 +584,11 @@ the required application form-entry work into [implementation notes](modeling-gu Qualifying fixtures do not fix that application behavior; add successful New/browser coverage after repairing it. Omit `return_to` when the conventional interaction is intended: standalone New/Edit returns to the saved record, -scoped associated create returns to its collection, destroy returns to the record's collection, and profile updates -return to Account. If a preferred record or collection route is unavailable, Rails uses the admitted collection -or home fallback. An explicit override remains authored meaning and must itself be lowerable; it does not silently -become a default. The browser supplies no destination URL, hidden return input, or history-based redirect. +scoped associated create returns to its collection, a no-input associated create returns to its parent's page, +destroy returns to the record's collection, and profile updates return to Account. If a preferred record or +collection route is unavailable, Rails uses the admitted collection or home fallback. An explicit override remains +authored meaning and must itself be lowerable; it does not silently become a default. The browser supplies no +destination URL, hidden return input, or history-based redirect. A selected standalone create without New or an explicit return uses an empty `201 Created` response on success and `422` for validation failure, even when its definition also serves associated forms. Authorization denials keep @@ -599,6 +600,8 @@ Admitted details collections show a bounded preview with a separate paginated fu attributes remain under `params[:credit]`. The authorized parent association builds the child, with no hidden parent input; a competing submitted parent does not replace it. Shared target new/create actions select the form for that context and retain its other inputs and validation errors. +A [no-input create](modeling-guide.md#add-behavior-deliberately) shows a one-tap button in place of the Add link, +with no scoped New page. Ordinary child edit/update/destroy routes stay flat. Preview rows contain selected properties and an optional authorized details link, without inline mutation controls. If no target show route is selected, its flat mutation routes can remain unlinked starter code. Do not invent a show route or discard authored properties to fill that gap. diff --git a/skills/create-full-stack-app/references/modeling-guide.md b/skills/create-full-stack-app/references/modeling-guide.md index d9d8a14..90402d6 100644 --- a/skills/create-full-stack-app/references/modeling-guide.md +++ b/skills/create-full-stack-app/references/modeling-guide.md @@ -320,7 +320,7 @@ consumers, cursor pagination, Field and Association inputs, server bindings, ass optional return overrides. Omit `return_to` for conventional interaction defaults; discuss navigation only when product intent needs an exception. The [worked return examples](examples.md#deliberate-return-overrides) show the complete record operand and independent associated-success override. A supported associated `create_form` supplies -a scoped New page, not an inline form in the details card. +a scoped New page, or a one-tap button for a no-input record, not an inline form in the details card. Every request and displayed Association declares public access or a Policy binding. The exact Web Account/Policy slice can protect supported surfaces and provide a Web-only Account profile; unsupported Policies and dependent consumers remain exact gaps. Read the Foundation Plan reference for the current prerequisites. Do not @@ -334,12 +334,14 @@ Name each list's fields in the read-back as a delegated choice unless the user c When the user describes something people do with a click, such as a like, follow, RSVP, bookmark, or upvote, use the Foundation Plan's no-input record pattern: a create with no `inputs` under the parent's `"create_form": {}`, -with every value bound from context. The Foundation Plan Guide's No-input records section defines it, and the -[Like example](examples.md#signed-in-gate-and-one-tap-records) copies its example. When each person does it once -and can take it back, it is a toggle: also add a uniqueness rule over the parent Reference and the Account -Reference, and select `destroy` authorized by an owner Policy. Records that may repeat, such as "mark as read" -events or check-ins, get neither. Say it plainly in the read-back, such as "one tap to like, one tap to unlike". -Today's deployed Compiler may still show a separate Create page until the one-tap change ships. +with every value bound from context. The parent page shows a one-tap button, and a tap returns to the parent page. +The public Guide's [No-input records](https://firstdraft.github.io/firstdraft/docs/architecture/design/foundation-plan.html#no-input-records) +section defines the pattern, and the [Like example](examples.md#signed-in-gate-and-one-tap-records) copies its +example. When each person does it once and can take it back, it is a toggle: also add a uniqueness rule over the +parent Reference and the Account Reference, select `destroy` authorized by an owner Policy, and author +`destroy.return_to` through the parent Reference so Unlike stays on the parent page. Records that may repeat, such +as "mark as read" events or check-ins, get no uniqueness rule or `destroy`. Say it plainly in the read-back, such as +"one tap to like, one tap to unlike". Select `native.ios` and `native.android` independently when the user wants those owned projects. Ordinary Compilation emits each with at least one admitted public navigation entry and an identity that fits its