diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcd2e4..e0d24f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,61 @@ Entries are newest first. Keep their headings stable. Publish material correctio new entries so readers who advanced their checkpoint see them. Entries explain what changed, why, when it applies, and any useful small example or verification; they need not reproduce commit logs. +## 2026-10-04: First Draft as one tool for your app + +**Skills — released in plugin 0.8.4.** The Skill treats First Draft as one tool for your app instead of assuming it +can build whatever you describe. Before the interview the agent takes a quick look at how others build similar apps +and uses what it finds to choose better questions and defaults. When feasibility is in question, such as phone +sensors, outside data, store rules, or real-time behavior, it researches thoroughly whether the idea is possible and +tells you what it found with sources. As soon as it is clear, it says how First Draft helps: plan and Compile the +whole app; plan and Compile the backbone, such as accounts, sessions, and lists, while the core, such as real-time +sensor advice, is built separately in native Swift or Kotlin; plan only, when you will build in another stack; or not +at all, as for a first-person shooter, where it names a game engine and offers First Draft later for accounts, +leaderboards, or a website. An app of records and people, such as a family photo feed, gets no talk about fit and +starts the interview as before. `implementation-notes.md` and the read-back keep three lists: what First Draft +builds, what is built otherwise, and feasibility findings and open questions. The Skill's description now starts +from your goal, so it triggers when you ask for help making or planning an app or game. + +## 2026-10-04: Cancel a stuck Compilation + +**Skills — released in plugin 0.8.4 with CLI 0.8.2.** The plugin bundles CLI 0.8.2, which adds +`firstdraft compilation cancel `. When `plan push` or `plan compile` fails with First Draft's +`409 compilation_active` problem because an earlier Compilation never finished, the agent checks that Compilation with +`compilation status --wait`, cancels it if it is still queued or running, and runs the command again. Before, +only an operator could clear it. Cancelling a Compilation that `plan compile --github` started also cancels that +Project's Publication, so the agent asks first. + +## 2026-10-04: Photos, counts, Field types, and signed-in phone apps + +**Skills — released in plugin 0.8.4.** The Skill now describes what the current Compiler generates, so the agent +authors these features and stops reporting them as gaps. Earlier versions said each was skipped or unsupported. + +- **Photos and files.** `image` and `attachment` Fields generate Active Storage uploads stored on Cloudinary. Set + `CLOUDINARY_URL` in `.env.development.local` and in Render; setup works without it, and the first upload names + it. Model several photos as a child record with one image each. A required upload on the Account gets no sign-up + file input yet. +- **Counts.** `counter` Fields generate with Rails `counter_cache` and show on list and details pages. Count the + direct Association, such as `follower_links` rather than `followers`; indirect, filtered, and polymorphic counts + remain gaps. +- **Phone apps with Accounts.** iPhone and Android apps show the same lists as the web app, protected ones and the + Account tab included. People sign in through the web pages inside the app and stay signed in after a restart, so + a private app no longer needs a public page for its phone apps. +- **More Field types.** Optional enums generate. `money`, `position`, `secure_token`, and `json` Fields generate + ordinary Rails storage and inputs with a partial gap for their missing behavior, and two `date` or `datetime` + Fields on one record can be compared, such as a check-out after its check-in. A required JSON Field on the Account + gets no sign-up control yet. +- **Defaults.** Literal defaults on boolean, number, and text Fields, and today's date on a required date Field, are + realized instead of reported as gaps. +- **One-tap records.** A like, follow, or RSVP is a one-tap button on the parent page instead of a separate Create + page. With a uniqueness rule and an owner-authorized delete it becomes a toggle, and the Like example authors + `destroy.return_to` through the parent so Unlike stays on that page. A guest who opens a protected page is sent to + sign in and returns there afterwards. + +The sign-up description now also matches the Compiler deployed with plugin 0.8.3: every required Account Field the +app does not fill gets a sign-up control, and sign-up meaning the Compiler cannot generate becomes a partial gap +instead of leaving the Account out. Existing apps keep what they generated; add a newly supported feature there in +ordinary Rails. + ## 2026-10-03: An interview that asks your level first **Skills — released in plugin 0.8.3.** The Skill's interview now follows one order. The agent clarifies your idea, diff --git a/README.md b/README.md index 5899111..703d715 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Start a new Codex conversation after installation. Use `$firstdraft:create-full- there is no separate CLI version to choose. A plugin install does not sign you into First Draft. Log in once per environment from any terminal with -`npx --yes @firstdraft.com/cli@0.8.1 login` (add `--staging` for staging). The login is saved in your user +`npx --yes @firstdraft.com/cli@0.8.2 login` (add `--staging` for staging). The login is saved in your user configuration directory, so Claude Code, Codex CLI, and desktop sessions all use it without exported variables. If authentication interrupts an already requested operation, log in and tell the same conversation to continue. Approve the specific CLI command when Codex requests network access; its tool permission is separate from approval of the Plan and Compile mode. diff --git a/evals/create-full-stack-app/cases.json b/evals/create-full-stack-app/cases.json index c626833..4a46d41 100644 --- a/evals/create-full-stack-app/cases.json +++ b/evals/create-full-stack-app/cases.json @@ -39,7 +39,7 @@ "Prioritizes whether one inventory record represents a uniquely identified object, a quantity of interchangeable goods, or both because that answer changes the Entity and Field model.", "Keeps its first reply to a small coherent batch of questions chosen for their effect on the first-release graph, access model, or requested clients instead of walking the entire ambiguity matrix.", "Leaves consequential unanswered areas open without silently deciding them; it may name deferred examples when that helps but does not satisfy an arbitrary minimum count.", - "Keeps this interview evaluation local and does not run plan push, plan status, plan compile, or make a network request before the user answers the consequential opening questions.", + "Keeps this interview evaluation local and does not run plan push, plan status, plan compile, or make a First Draft service request before the user answers the consequential opening questions.", "Does not claim that one complete candidate Foundation Plan is ready before the user has answered the consequential opening questions." ], "artifacts": [ @@ -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.", @@ -178,7 +179,7 @@ "Asks once whether it should make the technical decisions for the user, explain them as it goes, or ask about them.", "Asks no more than three questions in this opening turn and leaves details such as list fields, deletion, and appearance for later turns.", "Does not ask about hosting.", - "Runs no plan push, plan status, or plan compile and makes no network request in this opening turn." + "Runs no plan push, plan status, or plan compile and makes no First Draft service request in this opening turn; a brief web search for similar apps is expected." ] }, { @@ -211,13 +212,77 @@ "prompt": "Help me build a social network for just my family. It should work and look like Instagram so that it's familiar. You already asked your opening questions; my answers: only my family can use it and everyone signs in, I have nothing to share, and make the technical decisions for me.", "expectations": [ "Writes a rough Plan with private, signed-in pages and a photo on each post, then runs plan push and plan status --wait and reads the matching analysis before asking detailed questions.", - "Tells the user in a few plain lines what First Draft will and will not build, based on that analysis, such as that photos are not generated yet so posts start caption-only, and that iPhone and Android apps show only public pages and have no sign-in, so a private app works in the phone's browser.", + "Tells the user in a few plain lines what First Draft will and will not build, based on that analysis, naming each reported gap by what the user will notice and what the app does instead.", "Does not paste GapSet codes, pointers, or digests into this message.", "Keeps the photo Field and private access in the Plan instead of removing them or making pages public to avoid gaps.", - "Then asks one product decision that the analysis informs, such as whether the user still wants phone apps given that trade-off.", + "Then asks one product decision that the analysis informs, rather than a question about a gap the user cannot change.", "Does not run plan compile." ] }, + { + "id": "fit-partial-opening-race-coaching", + "should_trigger": true, + "prompt": "Help me build an app for hobbyist race car drivers that gives them real-time advice while they drive, using the phone's sensors.", + "expectations": [ + "Before replying, uses its web search tools to check whether the idea is possible, such as phone GPS and motion-sensor rates and accuracy and what the platforms allow while driving, and how existing apps such as lap timers and driver-coaching tools are built.", + "Opens the reply with a few lines on what it found, with sources.", + "Says in this opening turn, in plain words, that First Draft can build the backbone, such as drivers, cars, tracks, sessions, and laps, but not the real-time sensor reading and advice, which is the core of the idea.", + "Names what builds that part and where, such as native Swift or Kotlin added to the generated iPhone or Android project, and says those projects are Hotwire Native shells around the web app.", + "Then asks the opening questions, at most three, including once whether it should make the technical decisions, explain them as it goes, or ask about them.", + "Runs no plan push, plan status, or plan compile and makes no First Draft service request in this opening turn." + ] + }, + { + "id": "fit-partial-backbone-plan-race-coaching", + "should_trigger": true, + "prompt": "Help me build an app for hobbyist race car drivers that gives them real-time advice while they drive, using the phone's sensors. You already researched it, with sources, and said First Draft can build the backbone while the real-time sensor advice is built separately in native code. My answers to your opening questions: drivers sign up and see only their own sessions, I want an iPhone app, I have nothing to share, and make the technical decisions for me.", + "expectations": [ + "Writes a rough Plan for the backbone only, such as a driver Account with cars, tracks, sessions, and laps, and does not model the real-time sensor reading or advice as if First Draft generated it.", + "Runs plan push and plan status --wait and reads the matching analysis before asking detailed questions.", + "Tells the user in a few plain lines what First Draft will build, what it will not build yet, and what is built otherwise, including that the generated iPhone app shows the same signed-in pages as the web app while the real-time coaching is native code added to that project.", + "Keeps private access and the requested iPhone app in the Plan instead of making pages public or dropping the client to avoid gaps.", + "Writes implementation-notes.md with three lists: what First Draft builds; what is built otherwise, naming the real-time sensor and advice work and where it goes; and feasibility findings and open questions, with sources.", + "Does not run plan compile." + ] + }, + { + "id": "fit-none-first-person-shooter", + "should_trigger": true, + "prompt": "Help me build a first person shooter.", + "expectations": [ + "Says plainly that First Draft isn't the right tool for the game itself, because the core is a real-time game loop, 3D rendering, and input.", + "Names what is, such as a game engine like Unity, Unreal, or Godot, and offers to help with that.", + "Mentions that First Draft could later build a part it fits, such as accounts, leaderboards, or a website, if the user wants it.", + "Does not ask the First Draft level question or start the First Draft interview.", + "Runs no plan init, plan push, plan status, or plan compile and creates no .firstdraft directory." + ] + }, + { + "id": "fit-full-family-photo-network", + "should_trigger": true, + "prompt": "Help me build a social network for just my family. It should work and look like Instagram so that it's familiar. This is our first message; do not contact First Draft yet.", + "expectations": [ + "Before replying, takes a brief look with its web search tools, a few searches, at how similar family photo-sharing apps are built: common features, patterns, and pitfalls.", + "Does not make any statement about whether First Draft fits, not even that the idea fits; it may mention one or two useful findings, such as a feature most family photo apps share.", + "Restates the idea in a sentence and proposes a name.", + "Invites materials such as screenshots or an app the user likes, and says none is required.", + "Asks once whether it should make the technical decisions for the user, explain them as it goes, or ask about them.", + "Asks no more than three questions in this opening turn and leaves what First Draft will and will not build, such as photos and phone apps, for the reality check after the user answers.", + "Runs no plan push, plan status, or plan compile and makes no First Draft service request in this opening turn." + ] + }, + { + "id": "fit-plan-only-other-stack", + "should_trigger": true, + "prompt": "I'm going to build a volunteer shift app for our food bank myself in Django, but I'd like help designing the data first. Coordinators create shifts, and volunteers sign up for them. You already asked your opening questions; my answers: volunteers and coordinators both sign in, I have nothing to share, and make the technical decisions for me.", + "expectations": [ + "Says plainly that First Draft will help with planning only: the Foundation Plan and its analysis help design the data, and its generated Rails app will not be used.", + "Writes a Plan for the data model, such as volunteers, coordinators, shifts, and signups, then runs plan push and plan status --wait and uses the matching analysis to check the model.", + "Does not dwell on what the Rails Compiler would or would not generate, and asks the remaining data questions one at a time.", + "Writes implementation-notes.md with three lists, naming the Django build under what is built otherwise.", + "Never runs plan compile and does not offer to Compile." + ] + }, { "id": "meaning-asked-implementation-by-level", "should_trigger": true, @@ -259,7 +324,7 @@ "should_trigger": true, "prompt": "Use First Draft to plan a private photo journal for my family with iPhone and Android apps: members sign up and post a photo with a caption. Choose reasonable details for my review. I have no artifacts to share. Keep this local; do not submit or Compile.", "expectations": [ - "Writes implementation-notes.md with an After Compile checklist of the applicable items: Rollbar once ROLLBAR_ACCESS_TOKEN is set; Skylight once SKYLIGHT_AUTHENTICATION is set; a mail provider for password reset following DEPLOY.md's Account email launch prerequisite; Cloudinary and CLOUDINARY_URL once photos generate; deploying with DEPLOY.md's command-line section; and Revyl for previewing the phone apps.", + "Writes implementation-notes.md with an After Compile checklist of the applicable items: Rollbar once ROLLBAR_ACCESS_TOKEN is set; Skylight once SKYLIGHT_AUTHENTICATION is set; a mail provider for password reset following DEPLOY.md's Account email launch prerequisite; Cloudinary and CLOUDINARY_URL for the photo uploads; deploying with DEPLOY.md's command-line section; and Revyl for previewing the phone apps.", "Keeps those steps out of the Plan and does not ask for, print, or invent any key.", "Keeps the photo Field, private access, and the requested phone apps in the Plan.", "Makes no service request, Compile or remote write." @@ -316,7 +381,7 @@ "expectations": [ "Uses the supplied document without a redundant artifact invitation and models item-level return facts independently of the whole loan.", "Does not turn four printed photo boxes into four fixed Fields or infer a fixed attachment limit.", - "Preserves attachment intent as structured meaning whose image or attachment Fields become service-support gaps skipped at import, separate from outside-grammar media/import work in notes; does not encode file paths as supported uploaded seed values.", + "Preserves attachment intent as structured meaning, such as a child Entity with one image Field and a caption per damage photo, separate from outside-grammar media/import work in notes; does not encode file paths as supported uploaded seed values.", "Does not retain private filled-in content in Plan, notes, seed, archive or repository." ], "artifacts": [ @@ -719,9 +784,9 @@ "prompt": "Review how the staged Movies Plan should represent iPhone and Android clients plus index, show, create, update, and delete screens when Movie records must remain private to signed-in staff. Keep this local and do not edit or push it yet.", "expectations": [ "Recognizes that bounded Web Accounts, Policies, and protected Scaffolds are current target capabilities, so private signed-in Web intent is not automatically a blanket Account gap.", - "Explains that ordinary iPhone and Android output uses public entry points and public detail/forms; Web Accounts and Policies do not establish native sign-in, session restoration, or protected navigation.", - "Preserves private staff access and every requested screen as product meaning, then uses whole-graph analysis to distinguish any realized protected Web slice from exact native or unsupported-consumer gaps.", - "Does not silently add a public index, present protected Web CRUD as working native screens, narrow the requested screens, discard private access, or decline either requested native client merely to fit current support.", + "Explains that selected iPhone and Android clients show the same main navigation as Web, protected indexes and the Account tab included, and that staff sign in through the generated web pages inside the app, which keep the session across restarts.", + "Preserves private staff access and every requested screen as product meaning, then uses whole-graph analysis to tell realized protected Web and native surfaces from exact unsupported-consumer gaps.", + "Does not add a public index for the phone apps, narrow the requested screens, discard private access, or decline either requested native client merely to fit current support.", "Preserves the complete staged Plan and every existing subject_uuid unchanged during this review.", "Leaves .firstdraft/state.json unopened and unchanged, and does not run plan init, generate uuid, plan push, plan status, or plan compile." ], diff --git a/evals/create-full-stack-app/references/candidate-interview-protocol.md b/evals/create-full-stack-app/references/candidate-interview-protocol.md index 1bd2dd4..d9f12f3 100644 --- a/evals/create-full-stack-app/references/candidate-interview-protocol.md +++ b/evals/create-full-stack-app/references/candidate-interview-protocol.md @@ -101,8 +101,9 @@ and deferred questions can be revisited after diagnostics or further dialogue. Before the first Compile that could start direct retained work or reach Publication, reread the exact staged Plan and give a plain-language semantic summary first: application scope; Entities and their material Fields, relationships, -rules, behavior, and data; surfaces, access, and clients; material assumptions, delegated choices, exclusions, and -open questions. Then group the gaps by what the user will notice. End with a short technical block: the Plan path +rules, behavior, and data; surfaces, access, and clients; material assumptions, delegated choices, and exclusions. +Then group the gaps by what the user will notice, then list what is built outside First Draft and the feasibility +findings and open questions. End with a short technical block: the Plan path and SHA-256, the matching valid AnalysisRun's GapSet digest and record count, and the file holding every ordered record. Explain that service gaps were skipped before semantic analysis, target gaps were not fully realized, and `valid` applies only to the admitted graph. State the selected completion mode: direct output creates only a diff --git a/package-lock.json b/package-lock.json index 29df527..73f0ea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firstdraft/skills", - "version": "0.8.3", + "version": "0.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firstdraft/skills", - "version": "0.8.3", + "version": "0.8.4", "license": "MIT", "devDependencies": { "ajv": "8.20.0" diff --git a/package.json b/package.json index 200f2aa..0b27b2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firstdraft/skills", - "version": "0.8.3", + "version": "0.8.4", "description": "Portable Agent Skills for First Draft", "license": "MIT", "private": true, diff --git a/packages/claude-plugin/.claude-plugin/plugin.json b/packages/claude-plugin/.claude-plugin/plugin.json index ec81d0c..1af1c19 100644 --- a/packages/claude-plugin/.claude-plugin/plugin.json +++ b/packages/claude-plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "firstdraft", "displayName": "First Draft", - "version": "0.8.3", + "version": "0.8.4", "description": "Author a Foundation Plan and compile a bounded Rails, iPhone, and Android application", "author": { "name": "First Draft", diff --git a/packages/claude-plugin/package.template.json b/packages/claude-plugin/package.template.json index f906036..fc58e5b 100644 --- a/packages/claude-plugin/package.template.json +++ b/packages/claude-plugin/package.template.json @@ -1,6 +1,6 @@ { "name": "@firstdraft.com/claude-code", - "version": "0.8.3", + "version": "0.8.4", "description": "First Draft Foundation Plan authoring and Compilation for Claude Code and Codex", "license": "MIT", "type": "module", diff --git a/release/compatibility.json b/release/compatibility.json index 4fe95b6..1fcd805 100644 --- a/release/compatibility.json +++ b/release/compatibility.json @@ -1,10 +1,10 @@ { "format": "firstdraft.release-compatibility/1", "component": "skills", - "version": "0.8.3", + "version": "0.8.4", "plugin_source": { "package": "@firstdraft.com/claude-code", - "tarball_sha256": "f5639a83ac299dde1748635a16edff5148f1d8450003ba5cd6ce9b8611c27946" + "tarball_sha256": "dbb44c8250e55d9f6268fe41fe3dc76e5047ebf3c34b31108fb3774e0f3e1c8c" }, "requires": { "api_contract": [ @@ -12,7 +12,7 @@ "< 0.8.0" ], "cli": [ - "= 0.8.1" + "= 0.8.2" ], "foundation_plan_formats": [ "firstdraft.foundation-plan.sketch/0.23" diff --git a/script/cli-contract/config.mjs b/script/cli-contract/config.mjs index 34f2e18..7bea199 100644 --- a/script/cli-contract/config.mjs +++ b/script/cli-contract/config.mjs @@ -1,10 +1,10 @@ import { readFileSync } from "node:fs"; -export const cliRevision = "2f4bc93f996ba2e0a63c9f39df6cf1a8b9c91dac"; +export const cliRevision = "92f27714fecf45c44b19c52ed811889c5b81d5ae"; export const cliRuntimeSha256 = - "46cb062f67a00b49fa8edbe35b8c2f08034fb45a0710e6702a94c4c4cdb66cfc"; + "d222a231e864151f33e074b08204a2c3457910b0a84637e61ba1103b2206f926"; export const cliPackageName = "@firstdraft.com/cli"; -export const cliPackageVersion = "0.8.1"; +export const cliPackageVersion = "0.8.2"; export const safeGithubReasonCodes = Object.freeze([ "github.configuration_missing", diff --git a/script/cli-contract/local-commands.mjs b/script/cli-contract/local-commands.mjs index 3fa2c29..7ca0083 100644 --- a/script/cli-contract/local-commands.mjs +++ b/script/cli-contract/local-commands.mjs @@ -43,7 +43,7 @@ export async function verifyLocalCommands(context) { context.temporaryDirectory, ); assert.equal(rootHelp.status, 0); - assert.match(rootHelp.stdout, /compilation\s+Inspect and download Compilations/); + assert.match(rootHelp.stdout, /compilation\s+Inspect, download, and cancel Compilations/); assert.match(rootHelp.stdout, /generate\s+Generate local values/); assert.match(rootHelp.stdout, /login\s+Log in to First Draft and save a token/); assert.match(rootHelp.stdout, /logout\s+Revoke and remove the saved token/); @@ -68,6 +68,7 @@ export async function verifyLocalCommands(context) { assert.equal(compilationHelp.status, 0); assert.match(compilationHelp.stdout, /^ status\s/m); assert.match(compilationHelp.stdout, /^ download\s/m); + assert.match(compilationHelp.stdout, /^ cancel\s/m); const compileHelp = await invokeRunner( context.runCli, diff --git a/skills/create-full-stack-app/SKILL.md b/skills/create-full-stack-app/SKILL.md index 5174743..b264577 100644 --- a/skills/create-full-stack-app/SKILL.md +++ b/skills/create-full-stack-app/SKILL.md @@ -1,14 +1,14 @@ --- name: "create-full-stack-app" -description: "Experimental and in development: Authors and revises First Draft Foundation Plans, submits exact bytes, and requests bounded Rails/iPhone/Android Compile. Preserves identity, state, and provenance. Web Accounts, Policies, protected Scaffolds, and required enums are bounded; arbitrary apps, deployment, iPad, notifications, and broader clients are unavailable." +description: "Use when someone asks for help making or planning an app or game, even one First Draft may not fit. Experimental: researches whether the idea is possible and how similar things are built, then uses First Draft where it helps: Foundation Plans, analysis, and bounded Rails/iPhone/Android Compile for records, people, permissions, lists, and forms. Says early when First Draft won't cover the core, and what will." license: "MIT" --- # Create a Full-Stack App with First Draft -Author and review a coherent Foundation Plan, then Compile through the First Draft service into the current local -folder. Run and develop the generated app locally. A Codespace is an optional fallback; GitHub publication and native -preview are optional follow-ups. Compilation does not deploy. +Use First Draft where it helps the user's app: author and review a Foundation Plan, then Compile through the First +Draft service into the current local folder. Run and develop the generated app locally; a Codespace, GitHub +publication, and native preview are optional. Compilation does not deploy. ## Preserve implementation requirements @@ -23,12 +23,12 @@ explicit handoff. Follow [writing notes](references/modeling-guide.md#retain-imp ## Current boundary -Targets plugin 0.8.3, CLI 0.8.1, API 0.7, Plan 0.23; catalog selection is separate. +Targets plugin 0.8.4, CLI 0.8.2, API 0.7, Plan 0.23; catalog selection is separate. - Bounded generation includes Web Accounts/Policies/Scaffolds, development data, and selected iPhone/Android clients. [Appearance](references/foundation-plan-023.md#application-and-clients) controls theme and native colors, with explicit gaps for native launcher artwork. -- Account/Policy protection applies to Web Scaffolds. Native clients require a public index and do not inherit Web privacy. +- iPhone and Android apps mirror Web navigation, including protected lists and Account, and sign in on web pages. - Preserve unsupported requests and their gaps, including selected clients and access. Artifacts retain the Plan and GapSet. - Preview iPhone and Android apps in [Revyl](references/foundation-plan-023.md#preview-generated-native-apps); Rails work needs only the local web app. @@ -58,11 +58,10 @@ For CLI work: read [ambiguous mutations](references/diagnostics-and-recovery.md#ambiguous-mutations) only when the named error requires it. -The bundled [JSON Schema](references/foundation-plan-0.23.schema.json) is machine-readable validator input, not prose. -Use a compatible JSON Schema 2020-12 command named by the user, exposed by the project, or found through a -straightforward check of existing local commands. Pass only its path; never read it end to end. Do not install -dependencies or add validation/build plumbing solely for this workflow. Otherwise rely on First Draft exact-byte -diagnostics and say local schema validation was not performed. +The bundled [JSON Schema](references/foundation-plan-0.23.schema.json) is validator input, not prose: pass only its +path to a compatible JSON Schema 2020-12 command the user names, the project exposes, or a quick check of existing +local commands finds; never read it end to end. Install nothing and add no validation plumbing for this. Otherwise +rely on First Draft exact-byte diagnostics and say local schema validation was not performed. ## Verify the local capability @@ -80,13 +79,13 @@ firstdraft_cli --version firstdraft_cli --help ``` -Require the version probe to succeed with one exact `0.8.1` output line and no other output, and top-level help that +Require the version probe to succeed with one exact `0.8.2` output line and no other output, and top-level help that lists `generate`, `plan`, and `compilation`. Contract tests own separate stdout and stderr assertions for leaf commands; do not repeat them in a startup shell loop. The compatible CLI supplies these public commands: - `generate uuid` and `generate application-key`; -- `plan init`, `plan push`, `plan status`, and `plan compile` (local `--output .` by default), optional `--output `, or explicit `--github`; and -- `compilation status` and `compilation download`; and +- `plan init`, `plan push`, `plan status`, and `plan compile` (local `--output .` by default), optional `--output `, or explicit `--github`; +- `compilation status`, `compilation download`, and `compilation cancel`; and - `login` and `logout`, which only the user runs. There is no public `plan publish` or `plan subject-id`. Never replace the CLI automatically. @@ -126,7 +125,8 @@ reinitialize. Inspect private state only for a recovery check explicitly named i ## Interview and author incrementally -Start when the user asks for help making an app or invokes `/create-full-stack-app`, and follow the +Start when the user asks for help making an app or invokes `/create-full-stack-app`. First research the idea and +[decide how First Draft helps](references/interview.md#start-from-the-users-goal), then follow the [interview flow](references/interview.md#interview-flow). Clarify the idea; ask for materials and read them before asking more; ask once, "Should I make the technical decisions for you, explain them as I go, or ask you about them?" Then ask the product questions that shape the data model, most important first and one decision per turn, @@ -135,9 +135,8 @@ user plainly what First Draft will and will not build. Ask implementation choice product meaning is always asked or delegated. Honor "make the rest of the decisions for me" at any point. Before the read-back, walk the coverage checklist silently and write the after-Compile checklist into the notes. -State the access boundary precisely: Web pages may be public or use Accounts and Policies, while iPhone and Android -apps show only public pages and have no sign-in. Keep private access and requested clients in the Plan even when -native output is a gap. +State the access boundary precisely: Web pages may be public or use Accounts and Policies, and iPhone and Android +apps show the same pages with the same sign-in. Keep private access and requested clients in the Plan. Edit `.firstdraft/foundation-plan.json` throughout the conversation. Keep one complete current candidate; an incomplete or malformed local snapshot is safe to submit for diagnostics. Model product meaning rather than Rails @@ -196,10 +195,10 @@ valid status so the complete GapSet can be reviewed. `plan compile` later repeat Before the first `plan compile`, complete the modeling guide's [candidate reconciliation](references/modeling-guide.md#prepare-the-pre-compile-semantic-read-back), then reread the exact current -`.firstdraft/foundation-plan.json`. Follow that section's order: a plain-language summary, then the gaps grouped by -what the user will notice, then a short technical block with the Plan path and SHA-256 and the matching valid run's -`gap_set_sha256`. Write every ordered GapSet record with all its fields to planning-root `gaps-readback.md` and -name that file instead of pasting the records into the chat. Use only that attached +`.firstdraft/foundation-plan.json`. Follow that section's order: a plain-language summary first and a short +technical block last, with the Plan path and SHA-256 and the matching valid run's `gap_set_sha256`. Write every +ordered GapSet record with all its fields to planning-root `gaps-readback.md` and name that file instead of pasting +the records into the chat. Use only that attached digest: the CLI validates it against the attached GapSet; never substitute a fixture, historical, or another Project's digest. Use current-folder output by default. Select an absent `--output ` for a requested destination, an ineligible root, or model-only originals when root output was not @@ -261,9 +260,9 @@ never applies to an ambiguous push or direct start. ## Inspect or download the retained Compilation -`--github` success prints only the repository URL. Use retained commands only with an exact ID -supplied by the user or a validated structured projection; never recover one from private state or unvalidated -output. +`--github` success prints only the repository URL. Use retained commands only with an exact ID from the user, a +validated structured projection, or the UUIDv7 in a `compilation_active` `response.detail`; never recover one from +private state or unvalidated output. ```sh firstdraft_cli() { sh "/scripts/firstdraft.sh" "$@"; } @@ -271,8 +270,8 @@ firstdraft_cli compilation status firstdraft_cli compilation status --wait ``` -Status is read-only. Without `--wait` it reads once; with it, it follows the same retained Compilation for up to -ten minutes. Branch on `compilation.status`; `failed` and `cancelled` are successfully read terminal states. +Status is read-only; `--wait` follows it for up to ten minutes. Branch on `compilation.status`; `failed` and +`cancelled` are successful terminal reads. For local source, choose an absent destination beneath an existing real directory or explicitly selected current-root adoption, then read @@ -283,9 +282,9 @@ firstdraft_cli() { sh "/scripts/firstdraft.sh" "$@"; } firstdraft_cli compilation download --output ``` -Download reads one succeeded Compilation, verifies retained provenance, transport, manifest, paths, modes, and file -digests, then installs atomically. It never starts or polls work. `--output .` uses the same current-root transaction -and preconditions as direct Compile; preserve every other existing destination. +Download reads one succeeded Compilation, verifies it, and installs atomically; it never starts or polls work. +`--output .` uses the same current-root transaction and preconditions as direct Compile; preserve every other +existing destination. ## Recover from failures diff --git a/skills/create-full-stack-app/references/diagnostics-and-recovery.md b/skills/create-full-stack-app/references/diagnostics-and-recovery.md index e4fc146..ae65ee6 100644 --- a/skills/create-full-stack-app/references/diagnostics-and-recovery.md +++ b/skills/create-full-stack-app/references/diagnostics-and-recovery.md @@ -8,8 +8,8 @@ JSON object. An unrecognized prefixed line, a progress line after the envelope, interleaved output fail closed. Branch on the object's stable `error` and structured fields rather than the human-readable `detail` or broad process exit status. -This plugin bundles `@firstdraft.com/cli@0.8.1`. Its exact reviewed revision and runtime digest are owned by -[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.8.3/script/cli-contract/config.mjs) +This plugin bundles `@firstdraft.com/cli@0.8.2`. Its exact reviewed revision and runtime digest are owned by +[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.8.4/script/cli-contract/config.mjs) at this plugin's protected release tag. Check the command surface rather than assuming the version alone establishes compatibility. These source checks do not prove plugin/catalog publication, service authentication, staging compatibility, or a complete user journey. @@ -23,6 +23,7 @@ staging compatibility, or a complete user journey. - [Retained Compilation download](#retained-compilation-download) - [Stable error families](#stable-error-families) - [Ambiguous mutations](#ambiguous-mutations) +- [Stuck Compilation recovery](#stuck-compilation-recovery) - [Diagnostic shape](#diagnostic-shape) - [Concurrent replacement](#concurrent-replacement) @@ -41,7 +42,7 @@ Plans resumed without the flag. Each request uses the credential for its exact origin: `FIRSTDRAFT_API_TOKEN` for production and custom servers, or `FIRSTDRAFT_STAGING_API_TOKEN` for staging, when set; otherwise the token `firstdraft login` saved for that origin. The CLI never sends one environment's credential to another. The usual setup is one login per environment, -which the user runs in their own terminal: `npx --yes @firstdraft.com/cli@0.8.1 login`, with `--staging` added for +which the user runs in their own terminal: `npx --yes @firstdraft.com/cli@0.8.2 login`, with `--staging` added for staging, or `firstdraft login` from an installed CLI. It prints a URL to approve in a browser; on a machine without one, `--interactive` shows a device code to approve elsewhere. The login is saved outside the project, so every session and client on the machine shares it. @@ -217,7 +218,8 @@ Progress contains only the analysis and Compilation messages from the stable tab creates no Publication or repository; report later GitHub publication or pushes separately. A direct wait timeout does not cancel retained work. When its validated `current` projection supplies the exact Compilation ID, use read-only `compilation status --wait` and, after terminal success, -`compilation download --output ` rather than starting another Compilation. +`compilation download --output ` rather than starting another Compilation. If it stays +`queued` or `running` after that wait, follow [stuck Compilation recovery](#stuck-compilation-recovery). An ambiguous direct start is not replayable: `request_outcome_unknown` with `phase: "compilation"` means one Compilation may exist but its retained identity was not verified. Preserve the exact Plan, CLI state, and selected @@ -301,8 +303,8 @@ need an explicit development `bin/rails db:seed` to load added samples. Do not r Inspect the existing data first: ordinary seeds match their generated attribute tuples, so changing those values can make a later seed run recreate the original sample. Reseeding does not reconcile customized records. -If the app has generated image or attachment Fields, uploads need the user's Cloudinary `CLOUDINARY_URL` -([setup](foundation-plan-023.md#entities-descriptors-and-fields)) before you check them. +If the app has image or attachment Fields, uploads need the user's Cloudinary `CLOUDINARY_URL` +([setup](foundation-plan-023.md#photos-and-files)) before you check them; seeded samples use placeholder files. Open the running app in a browser and check representative visible records, their relationships, and relevant states in the intended flow. For an explicit empty start, check the empty state and appropriate create flow. For @@ -504,11 +506,11 @@ bytes and private state; create replacement work only within the user's authoriz | --- | --- | --- | | Any leaf command | `invalid_arguments` | Syntax failed before the requested action. Read that command's help. | | `plan init` | `local_initialization_failed` | Preserve possibly partial local state. | -| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `authentication_required` | Ask the user to run `login` for that environment, or set its token, outside the conversation. | -| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `local_input_unreadable` | Preserve unreadable local files; do not reconstruct private state. | -| `plan status`, `plan compile`, `compilation status`, `compilation download` | `project_not_pushed` | No accepted Project/origin is pinned locally. | -| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download` | `invalid_configuration` | The API origin, `--staging` selection, or saved Head state is incompatible. | -| `plan push`, `plan compile` | `server_rejected` | Inspect the validated status, bounded response, and diagnostics. | +| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download`, `compilation cancel` | `authentication_required` | Ask the user to run `login` for that environment, or set its token, outside the conversation. | +| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download`, `compilation cancel` | `local_input_unreadable` | Preserve unreadable local files; do not reconstruct private state. | +| `plan status`, `plan compile`, `compilation status`, `compilation download`, `compilation cancel` | `project_not_pushed` | No accepted Project/origin is pinned locally. | +| `plan push`, `plan status`, `plan compile`, `compilation status`, `compilation download`, `compilation cancel` | `invalid_configuration` | The API origin, `--staging` selection, or saved Head state is incompatible. | +| `plan push`, `plan compile` | `server_rejected` | Inspect the validated status, bounded response, and diagnostics. A `compilation_active` problem starts [stuck Compilation recovery](#stuck-compilation-recovery). | | `plan push`, `plan compile` | `local_state_not_saved` | The Plan was accepted but private ETag state was not saved. | | `plan push`, `plan compile` | `request_outcome_unknown` | A mutation or its response was not fully verified. See phase rules below. | | `plan status` | `status_unavailable`, `invalid_server_response`, `server_rejected` | The analysis read failed transport or protocol validation, or received a bounded rejection. | @@ -517,7 +519,7 @@ bytes and private state; create replacement work only within the user's authoriz | `plan compile` | `analysis_status_unavailable`, `invalid_analysis_status`, `analysis_status_rejected` | Compile's analysis read failed or was rejected. | | `plan compile` | `plan_not_valid` | The accepted graph did not reach `valid`; inspect `current`. | | `plan compile` | `local_plan_changed` | Local bytes or their accepted ETag changed before the selected mutation. | -| local `plan compile` | `compilation_start_rejected`, `compilation_status_unavailable`, `invalid_compilation_status` | Direct Compilation start or status did not complete its validated contract. Do not infer Publication. | +| local `plan compile` | `compilation_start_rejected`, `compilation_status_unavailable`, `invalid_compilation_status` | Direct Compilation start or status did not complete its validated contract. Do not infer Publication. A `compilation_active` start rejection starts [stuck Compilation recovery](#stuck-compilation-recovery). | | local `plan compile` | `compilation_changed`, `compilation_wait_timed_out`, `compilation_failed`, `compilation_cancelled` | The pinned direct Compilation changed, remained nonterminal, or reached a non-success terminal state. | | local `plan compile` | `invalid_output_path` | Preflight makes no request; an absent-path post-analysis recheck may follow an accepted push and reads, but no Compilation starts. Preserve owner material and correct only the reported root precondition or choose an absent path. | | local `plan compile`, `compilation download` | `artifact_unavailable`, `invalid_artifact`, `materialization_failed` | No verified local tree was installed. | @@ -532,6 +534,7 @@ bytes and private state; create replacement work only within the user's authoriz | `compilation download` | `artifact_unavailable`, `invalid_artifact` | Artifact transport, provenance, Plan/profile compatibility, or integrity failed before installation. An older artifact cannot be upgraded by retry. | | `compilation download` | `invalid_output_path` | No network request was made; correct only a reported root precondition or choose another absent path. | | `compilation download` | `materialization_failed` | The verified tree could not be atomically installed. | +| `compilation cancel` | `compilation_cancel_rejected`, `compilation_cancel_unavailable`, `invalid_compilation_status` | Nothing was cancelled, the outcome is unconfirmed, or the response broke its contract; see [stuck Compilation recovery](#stuck-compilation-recovery). | ## Ambiguous mutations @@ -592,6 +595,41 @@ not a trusted recovery envelope. Preserve local state and avoid guessing whether by itself does not prove that an account lacks provisioning or that an endpoint does not exist, and it is not a basis for a support recommendation. +## Stuck Compilation recovery + +A Project allows one active Compilation. While one is `queued` or `running`, `plan push` and `plan compile` fail with +`server_rejected`, and a direct start can fail with `compilation_start_rejected`. Either carries First Draft's `409` +problem with `response.code: "compilation_active"`. Only its validated `response.detail` names the active +Compilation: "Cancel active Compilation before replacing the Foundation Plan." after a push, or "Compilation + is already active for this Project." after a start. This is the one exception to reading only structured +fields: take that ID only when it is exactly one lowercase UUIDv7, and otherwise stop and report the error. A +Compilation usually finishes in under a minute, so when this error follows a Compile that has been stuck for +minutes: + +1. Run `compilation status --wait`. If the Compilation reaches `succeeded`, `failed`, or `cancelled`, it no + longer blocks; rerun the command that failed. +2. If the wait ends with `compilation_wait_timed_out` while it is still `queued` or `running`, run + `compilation cancel `. Cancelling throws away any work it is still doing. Success prints the cancelled + Compilation in the `compilation status` shape. +3. After the cancel succeeds, rerun the command that failed once. + +Cancelling a Compilation that `plan compile --github` started also cancels the Project's Publication, and that +singleton cannot start again for the Project, so ask the user before cancelling one. `compilation cancel` never +changes a succeeded or failed Compilation, and repeating it is safe: + +- `compilation_cancel_rejected` means nothing was cancelled; `response.code` says why. + `409 compilation_not_cancellable` means the Compilation already succeeded or failed and no longer blocks. + `404 compilation_not_found` or `404 project_not_found` means this Project and account have no such Compilation; + check the ID, directory, environment, and login. +- `compilation_cancel_unavailable` means a network failure, timeout, or server error left the outcome unconfirmed; + rerun `compilation cancel` or read `compilation status`. +- `authentication_required` means the environment's token is missing or was rejected with `401`; ask the user to + run `login` for that environment, then repeat the cancel. +- `invalid_compilation_status` means the response broke the Compilation contract; read `compilation status` instead + of repeating the request. + +This recovery applies only to `compilation_active`. It does not change the stop rules for `request_outcome_unknown`. + ## Diagnostic shape A `422 server_rejected` response contains `source_sha256` and diagnostics. Locations use either an RFC 6901 diff --git a/skills/create-full-stack-app/references/examples.md b/skills/create-full-stack-app/references/examples.md index abd81fe..e3891cd 100644 --- a/skills/create-full-stack-app/references/examples.md +++ b/skills/create-full-stack-app/references/examples.md @@ -98,13 +98,14 @@ return `valid`; that result is still only the Compilation gate, not proof that o ``` This example requests iPhone. Add `"android": {}` beside `"ios": {}` to request both, or replace the iOS member -to request Android alone. Each client requires an admitted public-index Scaffold and an identity within its +to request Android alone. Each client requires a main-navigation entry, such as this index, a protected index, or a +realized Account, and an identity within its [platform limits](foundation-plan-023.md#application-and-clients); otherwise a valid analysis records an unrealized-client target gap. iPad remains outside this boundary. Domain configures the native origin/identifier and Rails production mailer host; it does not provision DNS, TLS, or deployment. Web-only plans may use the same exact Scaffold with `native: {}` and may include or omit a domain. The admitted Scaffold makes Movie records readable on -the web without authentication. Confirm that exposure with the user before adding it; do not add it merely to satisfy -the iPhone navigation requirement or silently discard private or broader access intent. +the web without authentication. Confirm that exposure with the user before adding it; a protected index or a realized +Account also satisfies the iPhone navigation requirement, so never discard private or broader access intent for it. Adding `appearance` to this candidate selects the theme and native shell colors. Every web app includes fixed black-on-white [bookmark assets](foundation-plan-023.md#bookmark-assets) without an additional Plan choice. @@ -408,8 +409,9 @@ Do not assume a blanket enum gap; inspect the matching analysis for any unsuppor ## Web Account and protected profile This complete document expresses the current bounded Web Account topology with Field-only self-service registration, -two self Policies, and a protected `/account` surface authored through `scaffold.profile`. It requests no native client; adding `native.ios` would not -make this Account or profile available natively. It omits `verification`, so sign-up signs the person in; add +two self Policies, and a protected `/account` surface authored through `scaffold.profile`. It requests no native +client; adding `native.ios` would give that client the same Account tab and profile, with sign-in on the same pages. +It omits `verification`, so sign-up signs the person in; add `"verification": {"kind": "email"}` to `account` only when the user asks people to confirm their email. ```json @@ -561,7 +563,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 +615,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..f659988 100644 --- a/skills/create-full-stack-app/references/foundation-plan-023.md +++ b/skills/create-full-stack-app/references/foundation-plan-023.md @@ -27,9 +27,12 @@ and inspect only that definition. Use server diagnostics for the exact bytes sub - [Application and clients](#application-and-clients) - [Bookmark assets](#bookmark-assets) - [Entities, descriptors, and Fields](#entities-descriptors-and-fields) + - [Photos and files](#photos-and-files) - [Enums](#enums) + - [Money, positions, tokens, and JSON](#money-positions-tokens-and-json) - [Defaults](#defaults) - [References and Associations](#references-and-associations) + - [Counters](#counters) - [Validations](#validations) - [Predicates](#predicates) - [Accounts and Policies](#accounts-and-policies) @@ -58,19 +61,21 @@ generated output, not observed application behavior, device installation, or dep - Service API 0.7 returns the complete canonical `firstdraft.foundation-gaps/2` object and its SHA-256 for every valid AnalysisRun, including an empty `gaps` array. `valid` applies only to the admitted graph; it is not proof of Compilation or of meaning skipped before analysis. -- Current public Compilation has bounded scalar, required-enum, relationship, Validation, Predicate, Ordering, - State Machine, bookmark assets and metadata, Appearance theme/native-color, Web Account, Action Policy, generalized Web Scaffold, - development-data, and selected-iPhone/Android slices. Their prerequisites matter: unsupported children and consumers +- Current public Compilation has bounded Field, relationship, Validation, Predicate, Ordering, State Machine, + bookmark assets and metadata, Appearance theme/native-color, Web Account, Action Policy, generalized Web Scaffold, + development-data, and selected-iPhone/Android slices; [Fields](#entities-descriptors-and-fields) lists the Field + types that import and what each generates. Their prerequisites matter: unsupported children and consumers remain exact gaps rather than widening the supported shape. When Appearance is authored, emitted native clients retain a named partial gap for their stock launcher icons; `toggle` also records the absent native preference control. -- Required enums emit string storage with Rails enum inclusion, presence validation, and native helpers. An admitted - required enum accepts its compatible in-domain literal-key default. Optional enums, database membership - constraints, general rank behavior, and broader enum consumers remain unsupported. +- Enums emit string storage with Rails enum inclusion and native helpers; a required enum adds presence validation, + and an optional enum allows nil. An admitted enum accepts its compatible in-domain literal-key default. Database + membership constraints, general rank behavior, and broader enum consumers remain unsupported. - Web Account realization requires the exact email/password/self-service registration, password-reset recovery, - lockout, Account-self, and Field-only input topology described below; email verification is optional. Bounded - Account-backed Policies and protected Web Scaffolds are supported. Ordinary iPhone and Android output remains - Account- and Policy-free and consumes public-only navigation; protected Web support does not imply protected native - behavior. + lockout, and Account-self topology described below; email verification is optional, and sign-up inputs the + target cannot generate become partial Account gaps. Bounded + Account-backed Policies and protected Web Scaffolds are supported. Selected iPhone and Android clients show the same + main navigation as Web, protected lists and the Account entry included, and sign in through the generated web + pages inside the app. - There is no Plan GET or pull operation, arbitrary application generation, deployment workflow, iPad output, or complete support for the Foundation Plan vocabulary. Preserve intended meaning and let the reviewed GapSet name the current delta. @@ -82,13 +87,13 @@ The bundled schema was copied byte-for-byte from release or execution evidence. This plugin's bundled CLI and pinned contract check use the exact reviewed CLI revision and runtime digest in -[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.8.3/script/cli-contract/config.mjs) +[the CLI contract configuration](https://github.com/firstdraft/skills/blob/claude-v0.8.4/script/cli-contract/config.mjs) at this plugin's protected release tag, as contract provenance rather than release or execution evidence. The CLI exposes `generate uuid`, `generate application-key`, `plan init`, `plan push`, `plan status`, local `plan compile` (equivalent to `--output .`), explicit `plan compile --github`, `plan compile --output`, `compilation status`, -`compilation download`, and the user-run `login` and `logout`. It has no public `plan subject-id` or `plan publish`. The coordinated checkout declares the -`@firstdraft.com/cli@0.8.1` package. Direct output accepts the ordinary absent destination and, on POSIX, +`compilation download`, `compilation cancel`, and the user-run `login` and `logout`. It has no public `plan subject-id` or `plan publish`. The coordinated checkout declares the +`@firstdraft.com/cli@0.8.2` package. Direct output accepts the ordinary absent destination and, on POSIX, current-root adoption by default or with `--output .`; the recovery reference owns its preconditions. Check commands rather than inferring compatibility from a version number. These source checks do not prove plugin/catalog publication, authentication, staging compatibility, or a complete user journey. @@ -97,7 +102,7 @@ The bundled CLI uses `.firstdraft/design/`; much older CLIs used a top-level `de Selected native projects compose separate pinned Cores under `ios/` and `android/`. Each emitted `FOUNDATION_PROVENANCE.json` records the exact revision, archive digest, and replaced application seams. -Those pins identify source, not exercised devices or runtime behavior. Public native navigation stays Account-free. +Those pins identify source, not exercised devices or runtime behavior. Native navigation matches Web, Account last. Both clients use one stack, up to five direct tabs, or four direct tabs plus More for additional destinations. Follow the [preview guidance](#preview-generated-native-apps) after verified materialization. @@ -234,7 +239,8 @@ for the Rails production mailer host independently of native-client selection. I host authorization, TLS, sender identity, or email provider. A domain is optional: without one, native clients use an explicit `.invalid` origin and `invalid.firstdraft` identifier prefix. -Each selected client needs an admitted public-index Scaffold and an identity that fits its platform: +Each selected client needs at least one main-navigation entry, meaning an admitted index Scaffold, public or +protected, or a realized Account, and an identity that fits its platform: - iOS replaces underscores in `application.key` with hyphens. That component must be one DNS-safe label of at most 63 ASCII bytes, beginning with a letter and ending with a letter or digit. An authored domain must have at least @@ -249,12 +255,16 @@ Each selected client needs an admitted public-index Scaffold and an identity tha For example, key `oscar_party` with domain `2-app.example.com` produces iOS identifier `com.example.2-app.oscar-party` and Android ID `com.example.d_2_happ.oscar_party`. -Structurally valid values can still exceed these target limits. A missing public entry or unusable identity omits +Structurally valid values can still exceed these target limits. A missing navigation entry or unusable identity omits that client and records `foundation_plan.gap.native_client.not_generated`; a missing domain alone does not. Preserve the user's requested clients and access rather than changing product meaning to avoid that gap. -Public detail and form links use Hotwire; Web Account and Policy support does not implement native sign-in, -profile, or protected navigation. Confirm that public native access is intentional. +Detail and form links use Hotwire, and native pages go through the same Rails controllers and Policies as the web +app. With a realized Account, people sign up and sign in through the generated web pages inside the app, and a +signed-out person who opens a protected tab or the Account tab is sent to sign-in. Rodauth remember keeps a native +sign-in across app restarts for up to 14 days. The Account page adds Sign out, and every tab restarts after signing +in or out: iPhone returns to the first tab and Android keeps the selected one. Native sign-in screens, biometric +unlock, and email links that open inside the app are not generated. `application.appearance.theme` accepts four modes: @@ -280,8 +290,8 @@ Native tint and background colors do not replace the stock Zinc web component to icon-assets gap. Other admitted but unconsumed Application configuration remains a target gap. The prepared Compilation emits admitted public and bounded Account/Policy-controlled Web surfaces and, when the -public-navigation prerequisite is met, selected owned iPhone and Android projects beneath `ios/` and `android/`. -Native authenticated sessions, push, and iPad remain outside the public boundary. +navigation prerequisite is met, selected owned iPhone and Android projects beneath `ios/` and `android/`. Push and +iPad remain outside the public boundary. `entities` may contain any number of closed Entity objects. The schema owns their exact optional families, including `account`, `fields`, `references`, `associations`, `predicates`, `orderings`, `validations`, `trees`, `policies`, @@ -326,14 +336,21 @@ descriptor terminates in a required emitted scalar or system Field. It preloads The whole-graph analyzer rejects an optional Field descriptor; multi-target, longer-chain, optional-source, cyclic, or otherwise unsupported Association descriptors remain exact gaps. A Field may use these types: +- `attachment` - `boolean` +- `counter` - `date` - `datetime` - `decimal` - `enum` +- `image` - `integer` +- `json` - `language_code` - `long_text` +- `money` +- `position` +- `secure_token` - `short_text` - `state_machine` - `time_zone` @@ -354,14 +371,26 @@ query `status_active` when the stored state is `active`. Check an affected appli state; no generated workaround or namespace-based scope suppression is supplied. With `no_direct_assignment: true`, a scoped builder can also raise even for the initial state. Use ordinary creation followed by the named event when a transition is intended; constructing its destination state directly would bypass the event's effects. -`attachment` and `image` are schema-valid -Field types, but they are skipped from the admitted graph and recorded as service-support gaps; they cannot reach -the current Compiler. Active Storage and image-delivery prose describes target direction, not emitted support. -Keep requested image and attachment Fields in the Plan either way. When a Compiler generates them, so that the -matching GapSet no longer reports the Field skipped, it stores files with Active Storage on Cloudinary. Then tell -the user to sign up for Cloudinary and set `CLOUDINARY_URL` from its dashboard in `.env.development.local` for -local use and in the Render environment for the deployed app. The user sets it; never ask for, print, or commit -its value. + +### Photos and files + +An `attachment` or `image` Field holds one uploaded file. The Compiler emits Active Storage `has_one_attached`, a +presence validation when the Field is required, and a Scaffold file input; an image input accepts `image/*`. Where a +page shows the Field, an image appears large on its details page and as a thumbnail in list rows, and an attachment +appears as a download link. A stored file satisfies a required Field on edit. Model several photos as a child Entity +with one `image` Field, such as a Photo with its own caption. + +Type and size limits, several files per Field, direct uploads, optimized image delivery, and protected delivery are +not generated. Uploaded files have permanent public URLs, so a Policy does not protect their bytes; say so when the +files are private. A required upload on the Account Entity gets no sign-up file input: the Account keeps a partial +gap, and because the model requires the file, sign-up cannot finish until the owner adds that input in Rails. Do not +assign upload values in development data. Seeds attach a placeholder file to each required upload, and an authored +upload value becomes a development-data gap. + +Apps with uploads store them on Cloudinary in development and production; tests use local disk. Tell the user to +create a Cloudinary account and set `CLOUDINARY_URL` from its dashboard in `.env.development.local` for local use and +in the Render environment for the deployed app. Setup works without it, but the first upload raises a `KeyError` +naming it. The user sets it; never ask for, print, or commit its value. ### Field capability matrix @@ -372,13 +401,13 @@ one target release into machine syntax. | Property | Schema and import meaning | Current review rule | | --- | --- | --- | | `required` | Mandatory Boolean; write `true` or `false`. Retained on admitted Fields. | A realized required Field emits target nullability and validation; an ungenerated Field remains a Field gap. | -| `default` | Closed tagged Value where the Field variant permits it. Retained structurally. | Lowering is Field- and value-specific; the exact current-time and admitted-required-enum cases are documented target slices. | +| `default` | Closed tagged Value where the Field variant permits it. Retained structurally. | Realized for compatible boolean, number, and text literals, required-date `current_date`, required-datetime `current_time`, and in-domain enum keys; see [Defaults](#defaults). | | `notes` | Optional nonempty string on Fields only. Retained as review context. | Emits no application behavior. | -| `immutable` | Optional Boolean; omission means `false`. Retained. | Realized for admitted emitted scalar and required-enum Fields; otherwise the owning Field or modifier remains a gap. | +| `immutable` | Optional Boolean; omission means `false`. Retained. | Realized for admitted emitted scalar, enum, `money`, `secure_token`, and `json` Fields; otherwise the owning Field or modifier remains a gap. | | `comparison` | `case_insensitive` on `short_text` only. Retained. | Lowering and downstream query use are profile-dependent; inspect the matching GapSet. | | `normalizations` | Explicit ordered pipeline on selected text or URL Fields, with URL restrictions. Retained. | Follow the [content and operation-order guidance](modeling-guide.md#choose-text-normalization); inspect the matching GapSet. | | `encrypted_at_rest` | Optional Boolean; omission means `false`. Retained. | Lowering and consumer support are Field-specific; inspect the matching GapSet. | -| `redact_from_logs` | Optional Boolean; omission means `false`. Retained. | An admitted emitted ordinary scalar or required enum adds model-qualified request and inspection filtering; other shapes keep an exact gap. | +| `redact_from_logs` | Optional Boolean; omission means `false`. Retained. | An admitted emitted ordinary scalar, enum, `money`, `position`, `secure_token`, or `json` Field adds model-qualified request and inspection filtering; other shapes keep an exact gap. | Preserve intentional values that the Compiler cannot emit. Report the exact output gap instead of deleting a default, security property, or other product meaning to obtain `valid`. @@ -397,14 +426,36 @@ while preserving the value's UUID. The current Compiler emits a required enum as a non-null string column and a Rails `enum` mapping stable keys to themselves in authored order. `validate: true` supplies inclusion; a separate presence declaration handles -requiredness. Native scopes and predicate/bang methods keep ordinary names when safe; the Compiler uses Rails -prefix or suffix options when needed to avoid collisions. Compatible in-domain literal-key defaults use the enum -declaration and matching database default. Admitted form options and read-only projections use Rails I18n entries under -`enums...` in `config/locales/foundation_domain.en.yml`, seeded from the authored value names. +requiredness. An optional enum uses a nullable column with `validate: {allow_nil: true}` and no presence check; its +form keeps a blank choice that stores nil, and read-only pages show nil as missing. Native scopes and predicate/bang +methods keep ordinary names when safe; the Compiler uses Rails prefix or suffix options when needed to avoid +collisions. Compatible in-domain literal-key defaults use the enum declaration and matching database default. +Admitted form options and read-only projections use Rails I18n entries under `enums...` in +`config/locales/foundation_domain.en.yml`, seeded from the authored value names. Forms submit stable keys in authored order. Edit the locale to change labels after Compile; general ordinal rank -semantics, a native PostgreSQL enum, and database membership `CHECK` are not emitted. Optional -enums and unsupported defaults or consumers remain precise gaps. Preserve the enum and report only the reviewed -consequences rather than assuming either blanket support or blanket failure. +semantics, a native PostgreSQL enum, and database membership `CHECK` are not emitted. Conditions and Orderings +over an optional enum, and other unsupported defaults or consumers, remain precise gaps. Preserve the enum and report +only the reviewed consequences rather than assuming either blanket support or blanket failure. + +### Money, positions, tokens, and JSON + +`money`, `position`, `secure_token`, and `json` Fields generate what a Rails scaffold or a developer would write by +hand. Each also records one `foundation_plan.gap.field_kind.partially_generated` target gap for the meaning it +lacks; report that gap without removing the Field: + +| Type | Generated | Still a gap | +| --- | --- | --- | +| `money` | Decimal column with two decimal places, numericality validation, a number input with step 0.01, and currency formatting on show and list pages: `USD` uses the locale's format, and another `settings.currency` shows its code | Currency-aware arithmetic, allocation, and rounding; the app has no Money object. A money Field's own comparison Validations remain Validation gaps | +| `position` | Required integer with integer validation and a number input where a Scaffold lists it; a new record without one joins the end of its `settings.within` list, and an Ordering may sort by it | Renumbering after deletes, moves between lists, concurrent appends, and drag reordering | +| `secure_token` | String column with a unique index and Rails `has_secure_token`, filled when a new record is built; never a form input, and shown where a projection selects it | Lookup by token, regeneration, expiry, and digest storage | +| `json` | `jsonb` column edited as JSON text in a textarea; malformed text is an ordinary validation error, and show pages print the document formatted | Document structure and schema validation | + +Positions and secure tokens are filled by the app, so they never become sign-up controls or factory values. A +required `json` Field on the Account Entity gets no sign-up control: the Account keeps a partial gap, and because the +model requires the value, sign-up cannot finish until the owner adds that input in Rails. A Primary Descriptor that +selects one of these four kinds stays a `foundation_plan.gap.primary_descriptor.not_generated` gap, so describe the +record with another required Field. In development data, a money value needs at most two decimal places and a JSON +value must be an object or array. ### Defaults @@ -425,6 +476,18 @@ This retention is structural, not default analysis. It does not prove literal co enum membership, readable-locator resolution, nullability, normalization behavior, or Compiler lowering. Preserve the intended default when reporting any later semantic gap. +The current target realizes a compatible literal default on an emitted `boolean`, `integer`, `decimal`, `money`, +`short_text`, or `long_text` Field as an ordinary column default, such as +`t.boolean "finished", default: false, null: false`. A new record starts with it, and an explicit value wins, +including `false`, `0`, or nil on an optional Field. An integer default must fit PostgreSQL `integer`, a money +default must fit its `numeric(12, 2)` column, and the schema writes a decimal or money default such as `"10"` as +`"10.0"`. A text literal that the Field's own normalizations would change stays a gap, because a column default +bypasses normalization. `current_date` on a required `date` Field and `current_time` on a required `datetime` Field +set the value when Rails builds the record, with no database default. Enum keys follow the [enum rule](#enums). A +null literal, literals on other Field kinds, including `json`, and optional environment defaults remain +`foundation_plan.gap.field_modifier.default` records; `position` and `secure_token` Fields take no default. +A required Field with a realized default needs no form or sign-up input. + ### References and Associations An Entity may also own supported References, Associations, and Predicates. @@ -455,6 +518,28 @@ needs. Multi-target realization, aliases, defaults, broader paths, cardinality, unsupported predicates or consumers can produce exact gaps. Scaffold input support is a separate consumer decision from Reference storage. Preserve the authored relationship meaning and review the matching consequence. +### Counters + +A `counter` Field is a count of related records that Rails keeps current, such as a post's likes or a person's +followers. It belongs to the Entity that owns the counted Association, is always `required: true`, accepts no +`default` or `immutable`, and names that Association in `settings.counts`, as in a Post's +`{"key": "likes_count", "name": "Likes", "type": "counter", "required": true, "settings": {"counts": "post.likes"}}` +plus its own `subject_uuid`. + +The Compiler emits an integer column that starts at zero, maintained by Rails `counter_cache` on the counted +record's `belongs_to`. Count a direct referenced-side Association over an ordinary single-target Reference, such as +`post.likes` over `like.post`. For followers, count the direct `user.follower_links` over `follow.followed`, not an +indirect `user.followers` through those links; when each pair is unique, both give the same number, and the gap +reason for an indirect counter names the direct Association to count instead. Author one counter per Reference: two +counters through the same Reference both remain gaps. Indirect, filtered (predicated), and polymorphic counts remain +gaps. + +A counter is never a form input or a development-data value. An authored counter input is dropped with a gap while +the rest of the form remains, and an assigned development value becomes a development-data gap. Explicit index, +show, and collection-row projections also show each emitted counter they do not already select, after the authored +items. An Ordering may sort by a counter. Bulk writes such as `delete_all` or raw SQL skip the callbacks and leave +counts stale; Rails `reset_counters` repairs one record, and no repair task is generated. + ### Validations The current Rails Validation subset admits: @@ -462,6 +547,10 @@ The current Rails Validation subset admits: - unconditional or bounded conditional ordered literal comparisons on stored integer or date Fields, using `greater_than`, `greater_than_or_equal_to`, `less_than`, or `less_than_or_equal_to`; date bounds start at `1582-10-15` because earlier Gregorian values differ from ordinary Rails casting; +- an unconditional same-record comparison between two stored `date` Fields or two stored `datetime` Fields with + those ordered operators, such as a check-out after its check-in. A Field-owned rule reports on its own Field and + an Entity-owned rule on its error target. Each side's requiredness owns its missing value, and the comparison is + skipped while the other side is blank; - an unconditional Entity `comparison` with one `not_equals` clause between distinct required ordinary References to the same record type, with a participating Reference as its error target; - unconditional or conditional `length` on `short_text` or `long_text`, using `minimum`, `maximum`, or @@ -481,7 +570,8 @@ the emitted predicate. Required numeric input uses Rails numericality, Boolean F bounds share numericality with requiredness. Date comparison and length rules leave missing-value feedback to requiredness. Admitted uniqueness uses a native Rails validator and a matching unique index, including logical Reference targets. Entity errors must target a Field or Reference; this Plan has no record-wide custom error target. -Ordinary Rails application code can still use `errors[:base]` after Compilation. Broader comparisons, patterns, +Ordinary Rails application code can still use `errors[:base]` after Compilation. Conditional Field-to-Field +comparisons, relationship-path or dynamic operands, `datetime` literal comparisons, broader comparisons, patterns, presence/absence, uniqueness tuples, conditions, owners, and `exclusion` can produce service- or target-support gaps. They remain invalid only when the admitted meaning itself violates semantic rules. @@ -506,11 +596,16 @@ staff membership; preserve required eligibility conditions and ask when they are Current public Web Account realization requires self-service registration, password-reset recovery, and lockout; email verification is optional. A realized Account derives one Web `/account` destination without requiring an -authored profile. Registration inputs must be one contiguous -ordered list of required, unique, emitted Account-owned `short_text` or `time_zone` Fields with compatible defaults; -they must cover every required emitted Account Field, and the Account Entity may own no required Reference. A -required enum on that Entity may still emit as Domain storage, but it cannot be a registration input, so that Account -shape remains a gap. Association registration and native Account/session restoration are not public behavior. +authored profile. Sign-up shows a control for each required registration input that names a stored Account Field, +then one for each other required stored Account Field that the application does not fill itself. Each control is +the Field's Scaffold form control, so a required enum becomes a select. The application fills State Machine initial +states and realized [defaults](#defaults); counters, secure tokens, positions, and derived Fields never become +sign-up controls. A registration input's own `default` applies only to `short_text` and `time_zone` inputs. + +Missing sign-up meaning never omits the Account. An Association input, an optional input, an input for a derived or +unstored Field, a registration default on another kind, or a required Reference on the Account Entity becomes a +`foundation_plan.gap.account.partially_generated` record. A required Reference also blocks sign-up, because the model +rejects an Account without it; the owner adds its control or default in Rails after Compile. Omit `verification` by default, so sign-up signs the person in. Author `"verification": {"kind": "email"}` only when the user asks people to confirm their email; they then open an emailed link before they can sign in. Keep an existing @@ -542,8 +637,8 @@ gaps and never activate permissive defaults. Every Account action resolves `curr Credential changes use Rodauth: **Change email** verifies the new address before replacing the existing one, and **Change password** uses its signed-in password-change flow. -Ordinary iPhone and Android output stays Account- and Policy-free, omits profile and protected Web navigation, and -records the applicable native consequence instead of borrowing Web authorization. +Selected iPhone and Android clients include the Account tab and the protected lists the web app shows, served by the +same Rails pages and Policies; the [native Account flow](#application-and-clients) covers sign-in and sign-out. ### Scaffolds @@ -584,10 +679,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 +695,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. @@ -608,9 +706,9 @@ During pre-alpha, generated Rails may also contain conventional unclaimed scaffo starter code is neither authored meaning nor proof that unsupported consequences work. Preserve the Plan and report the reviewed gaps instead of changing requiredness, access, or workflows to match incidental output. -Only public indexes become native entry points. Public detail and form pages remain reachable through links. -New/edit use modal context: iOS sheets and Android's full-screen form destination, with pull-to-refresh disabled. -Web profile and protected navigation do not become native screens. +Every admitted index, public or protected, becomes a native entry point, with the Account entry last. Detail and +form pages remain reachable through links. Public New/edit use modal context: iOS sheets and Android's full-screen +form destination, with pull-to-refresh disabled. ### Unsupported shapes @@ -618,11 +716,11 @@ Scalar Fields have no `settings` object, and enum `settings` admits only `values settings shape is structurally invalid rather than a support gap. Nonempty delivery, unsupported Field kinds and modifiers, and graph members outside the importer boundary remain in the exact submitted Head and appear as `service_support_gap` records when the admitted graph is valid. Imported but incompletely generated shapes—such as -optional enums, broader State Machine behavior, broader Account or Policy topologies, and unsupported consumers of -otherwise realized subjects—appear as `target_support_gap` records. Development-data records and assignments are -assessed individually; do not assume a blanket Application-level gap. Service-support meaning was skipped before -semantic analysis; target-support meaning was admitted and analyzed but is not fully realized. Preserve the authored -Plan and report every exact gap. +money and position semantics, broader State Machine behavior, broader Account or Policy topologies, and unsupported +consumers of otherwise realized subjects—appear as `target_support_gap` records. Development-data records and +assignments are assessed individually; do not assume a blanket Application-level gap. Service-support meaning was +skipped before semantic analysis; target-support meaning was admitted and analyzed but is not fully realized. +Preserve the authored Plan and report every exact gap. Successful Compilation retains the exact submitted Plan at `.firstdraft/submitted-foundation-plan.json` and the canonical machine-readable GapSet at `.firstdraft/gaps.json`. There is intentionally no duplicate diff --git a/skills/create-full-stack-app/references/interview.md b/skills/create-full-stack-app/references/interview.md index c56becb..78c69c5 100644 --- a/skills/create-full-stack-app/references/interview.md +++ b/skills/create-full-stack-app/references/interview.md @@ -5,12 +5,49 @@ to model each answer. The Skill's read-back, approval, and Compile rules are unc ## Contents +- [Start from the user's goal](#start-from-the-users-goal) - [Interview flow](#interview-flow) - [Meaning and implementation](#meaning-and-implementation) - [See what First Draft emits](#see-what-first-draft-emits) - [Coverage checklist](#coverage-checklist) - [After-Compile checklist](#after-compile-checklist) +## Start from the user's goal + +First Draft is one tool for the user's goal. Use it where it helps: to plan and Compile, to plan only, or not at +all. Decide which before the opening turn. + +**Research first, always.** Before the opening turn, take a quick look with your web search tools, a few searches, +at how others build similar apps: common features, patterns, and pitfalls. Use what you find to choose better +questions and defaults. Research thoroughly only when feasibility is in question, because the idea depends on a +device capability, outside data or APIs, store or platform rules, or real-time behavior. Then find out whether it +is possible (device sensors and their limits, APIs, store rules, and whether the data exists) and how others build +it (existing apps, their stacks, and their known limits), and start the opening turn with a few lines on what you +found, with sources. + +**Decide how First Draft helps.** When it won't cover the core of the idea, say so as soon as that is clear, which +is usually the opening turn: + +- **Plan and Compile:** the core is records, people, permissions, lists, and forms, such as a book club or a + private family photo feed. Do not mention fit, not even that the idea fits. You may mention one or two useful + findings, such as "Most family photo apps also let you…"; then start the interview. +- **Plan and Compile the backbone; build the rest separately:** the core is continuous computation or a device + capability, such as real-time sensors, a game loop, live audio or video, on-device machine learning, + offline-first sync, Bluetooth or other hardware, or background location. First Draft builds the accounts, + records, and pages around it. Name what comes after and where, such as native Swift or Kotlin in the generated + iPhone and Android projects. Those projects are Hotwire Native shells around the web app that show its pages, + sign-in included; device features are code added there. +- **Plan only:** the user will build in another stack, or won't use the generated code. The Foundation Plan and its + analysis still help design the data model: push it and use the analysis to check the model, skip the reality + check about what First Draft generates, give the read-back, and do not Compile. +- **Not at all:** First Draft doesn't help with the core, and the core is the app, such as a first-person shooter. + Say First Draft isn't the right tool and name what is, such as a game engine like Unity, Unreal, or Godot. Say + First Draft could later build a part it fits, such as accounts, leaderboards, or a website, if the user wants. + Do not initialize or push a Plan unless the user asks for that part. + +Then continue with whichever applies; the interview below covers the First Draft part. Keep the +[three lists](modeling-guide.md#retain-implementation-requirements) in the notes and the read-back. + ## Interview flow Start when the user asks for help making an app, such as "help me make an app for my book club", or invokes @@ -24,7 +61,7 @@ Start when the user asks for help making an app, such as "help me make an app fo 3. **Ask the user's level once:** "Should I make the technical decisions for you, explain them as I go, or ask you about them?" Do not ask it again; the user may change it at any time. -Steps 1 to 3 are the opening turn, with at most three questions. +Steps 1 to 3 are the opening turn, after the research and any fit, with at most three questions. 4. **Ask product questions** that decide the data model, most important first: which records exist and what one record is, who can see and do what, and whether they want iPhone or Android apps. After the opening turn, ask @@ -35,20 +72,20 @@ Steps 1 to 3 are the opening turn, with at most three questions. database, and that a paid option is planned. 5. **Give an early reality check.** As soon as the opening answers settle the main records, tell the user this takes a few minutes, then write a rough Plan, push it, and read its analysis. Keep the rough Plan small: the main - records and their key Fields, any Account, who can see what, and requested phone apps. Tell the user in a few - plain lines what First Draft will build and what it will not build yet, for example "Photos aren't generated - yet, so posts start caption-only; I'll add photo upload in Rails after" or "iPhone and Android apps show only - pages anyone can see, and have no sign-in; a private app works in the phone's browser". Then let the analysis - steer the remaining questions: ask about consequences the user can choose between, not about gaps they cannot - change. Keep private access and requested phone apps in the Plan. Report each gap as the analysis gives it, - even when its reason names no cause or contradicts these references; the deployed First Draft may not generate - it yet. Do not trim the Plan or push trial or example Plans to find a cause: each push replaces the Project's - current Plan, and a defensible variant is still a trial. + records and their key Fields, any Account, who can see what, and requested phone apps; for a backbone, only the + part First Draft builds. Tell the user in a few plain lines what First Draft will build and what it will not + build yet, for example "Formatted descriptions aren't generated yet, so posts start without one; I'll add them in + Rails after" or "Comments that can go on either a post or an album aren't generated yet; I'll add them in Rails + after". Then let the analysis steer the remaining questions: ask about consequences the user can choose between, + not about gaps they cannot change. Keep private access and requested phone apps in the Plan. Report each gap as + the analysis gives it, even when its reason names no cause or contradicts these references; the deployed First + Draft may not generate it yet. Do not trim the Plan or push trial or example Plans to find a cause: each push + replaces the Project's current Plan, and a defensible variant is still a trial. 6. **Ask implementation choices last**, following the user's level and [meaning and implementation](#meaning-and-implementation). 7. **Walk the [coverage checklist](#coverage-checklist)** silently, and write the [after-Compile checklist](#after-compile-checklist) into the implementation notes. -8. **Read back and approve** as the Skill describes, then Compile. +8. **Read back and approve** as the Skill describes, then Compile. For plan only, give the read-back and stop. At any point the user may say "make the rest of the decisions for me". Stop asking. Choose your recommendation for every remaining item, including product meaning, and mark each one delegated. Then walk the checklist and give the @@ -151,8 +188,8 @@ apply under `## After Compile` in `implementation-notes.md`, so the agent that c the Render environment. - [ ] Email (apps with Accounts): password reset and unlock emails need a mail provider. Once the Account is generated, follow `DEPLOY.md`'s "Account email launch prerequisite" before inviting real users. -- [ ] Images (once the GapSet no longer reports image Fields as skipped): sign up for Cloudinary and set - `CLOUDINARY_URL` as the [image setup](foundation-plan-023.md#entities-descriptors-and-fields) describes. +- [ ] Photos and files (apps with image or attachment Fields): sign up for Cloudinary and set `CLOUDINARY_URL` + as the [upload setup](foundation-plan-023.md#photos-and-files) describes. - [ ] Deploy: follow `DEPLOY.md`, using its "Deploy from the command line" section for the Neon and Render CLIs. The free plan sleeps when idle. - [ ] Phone preview (apps with iPhone or Android): preview in Revyl with the diff --git a/skills/create-full-stack-app/references/modeling-guide.md b/skills/create-full-stack-app/references/modeling-guide.md index d9d8a14..b5bc446 100644 --- a/skills/create-full-stack-app/references/modeling-guide.md +++ b/skills/create-full-stack-app/references/modeling-guide.md @@ -44,8 +44,9 @@ Plan as answers arrive. Prioritize answers that change the graph, access model, When modeling a collection, distinguish one uniquely identified object, a quantity of interchangeable goods, and a mixed product that needs both meanings. Do not collapse that branch into only individual-versus-group wording. In the opening turn, ask only about intended product meaning, materials, and the user's level, and name deferred -product areas; give the reality check about target support after the user answers, unless feasibility was itself -part of the request. Do not promote a common use case into an assumption. When target support matters, know the +product areas; give the reality check about target support after the user answers. Whether First Draft covers the +core of the idea is [fit](interview.md#start-from-the-users-goal), not target support: say it in the opening turn +when research shows it. Do not promote a common use case into an assumption. When target support matters, know the desired access before describing the current Account, Policy, Web, and native boundaries. Keep one candidate Plan: do not maintain a parallel flattened or capability-friendly shape merely so one version can Compile. @@ -148,11 +149,19 @@ extend `db/seeds/development.rb`; keep demo Accounts out of all-environment seed ## Retain implementation requirements -Maintain `implementation-notes.md` at the planning workspace root as the conversation establishes behavior that the -Plan cannot express. Write the product requirement, relevant Entities or interactions, useful rationale, and a few -acceptance examples. Separate agreed requirements from unresolved questions and proposals. Keep it concise and -revise it when the user's decisions change; it is not a transcript, task database, or second structured Plan. -Add the applicable [after-Compile checklist](interview.md#after-compile-checklist) under `## After Compile`. +Maintain `implementation-notes.md` at the planning workspace root as the conversation establishes what the Plan +cannot express. Keep it in three lists, then the after-Compile steps: + +- `## What First Draft builds`: a line or two naming the Plan and its gaps file; do not copy them. +- `## What is built otherwise`: each agreed requirement outside the Plan, with relevant Entities or interactions, + useful rationale, and a few acceptance examples; and each part of the idea that First Draft does not cover, with + where it will be built, such as native Swift in the generated `ios/` project. +- `## Feasibility findings and open questions`: what research found and its known limits, with source links, and + each unresolved question or proposal. +- `## After Compile`: the applicable [after-Compile checklist](interview.md#after-compile-checklist). + +Keep it concise and revise it when the user's decisions change; it is not a transcript, task database, or second +structured Plan. For example, an agreed CSV import may need to preview all row errors before saving anything and save a valid file as one transaction. Record examples such as "one invalid row leaves all records unchanged" and "a valid file saves @@ -203,14 +212,21 @@ Do not infer uniqueness from a label, presence from a form, or immutability from matter. Use an `enum` for a closed named set. Give every value its own stable identity, and set `ordinal` only when value -order carries semantic rank rather than presentation order alone. The current Compiler emits required enum string -storage using Rails `enum` with inclusion and presence validation plus native scopes and instance methods. The -Compiler selects Rails prefix or suffix options when helper names would collide. +order carries semantic rank rather than presentation order alone. The current Compiler emits enum string storage +using Rails `enum` with inclusion plus native scopes and instance methods; a required enum adds presence validation +and an optional enum allows a blank choice. The Compiler selects Rails prefix or suffix options when helper names +would collide. Compatible in-domain literal-key defaults work regardless of whether the order has semantic rank. Database -membership constraints, general rank semantics, optional enums, and unsupported consumers remain gaps. Preserve +membership constraints, general rank semantics, and unsupported consumers remain gaps. Preserve product meaning instead of replacing an enum with a scalar; the [enum reference](foundation-plan-023.md#enums) owns the exact lowering. +Use `money` for an amount in one fixed currency, `position` for an order people arrange within a list, `secure_token` +for a random token the app creates, such as an invite code, and `json` for a document whose structure the app does +not model. Each generates ordinary Rails storage and inputs with a partial gap for its missing behavior, as the +[Field type reference](foundation-plan-023.md#money-positions-tokens-and-json) lists. Keep stable facts as Fields, +References, or Entities rather than inside JSON. + ### Choose text normalization Select a `normalizations` pipeline for each Field whose content needs it. `short_text` becomes Rails `string` with a @@ -303,6 +319,13 @@ cardinality, polymorphism, exclusive arcs, and unsupported consumers can remain separate consumer decision from Reference storage. Preserve broader product meaning and inspect the matching GapSet rather than applying an older blanket relationship limit. +When people should see how many related records something has, such as likes on a post or a person's followers, add +a [`counter` Field](foundation-plan-023.md#counters) on the record being counted for. Point it at the direct +Association that holds the counted records: `user.follower_links`, not the indirect `user.followers` through them. +Add that direct referenced-side Association when the Plan lacks it, author one counter per Reference, and keep an +indirect, filtered, or polymorphic count in the Plan as a reviewed gap. An explicit list or details projection shows +each counter after its authored items; people never type them. + ## Add behavior deliberately - Add Predicates and Orderings when generated queries or surfaces need reusable product meaning. @@ -320,7 +343,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,24 +357,25 @@ 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 +Compilation emits each with at least one main-navigation entry, public or protected, and an identity that fits its [platform limits](foundation-plan-023.md#application-and-clients); otherwise the valid run records an unrealized-client target gap. Domain supplies a native HTTPS origin and platform identifier; it also configures the Rails production mailer host. It does not provision DNS, deployment, TLS, or mail delivery. Without a domain the native identifiers are explicit placeholders. Semantic icons inform Web, SF Symbol, and -Material navigation. Public Rails detail and form links work within Hotwire Native; Web Account and Policy support -does not establish native authentication or protected navigation. Confirm public access is intentional; preserve -private requirements and requested clients, then review the support gap. Do not recommend removing a requested -client to quiet gaps, or adding public indexes to satisfy native prerequisites. The user may change product scope; -target support alone is not that decision. +Material navigation. The phone apps show the same lists as the web app, protected ones and the Account tab included, +and people sign in through the generated web pages inside the app. A private app needs no public page for its phone +apps. Do not recommend removing a requested client to quiet gaps, or adding public indexes to satisfy native +prerequisites. The user may change product scope; target support alone is not that decision. Appearance sets the theme and native colors. Web bookmark artwork uses fixed black-on-white assets. Omitted theme or `light` means fixed light; `dark` means fixed dark, and `auto` follows the system with no manual control or saved preference. Use `toggle` @@ -403,19 +427,23 @@ a confirmed product decision, or a demonstrated diagnostic. Write it for a reader who is not technical, in this order: -1. A plain-language summary: what the app is for and who uses it; each kind of record and its material Fields, - with each relationship as a "must" or "may" sentence; rules, sign-up and access, and clients; what each main - list shows; a few of the proposed sample records and any the gaps will drop; and delegated choices, - assumptions, exclusions, open questions, and outstanding implementation notes with how the selected output - mode carries them forward. For an Account, say plainly whether people can sign in right after signing up or - confirm their email first, as the [Accounts reference](foundation-plan-023.md#accounts-and-policies) describes. +1. What First Draft builds, as a plain-language summary: what the app is for and who uses it; each kind of record + and its material Fields, with each relationship as a "must" or "may" sentence; rules, sign-up and access, and + clients; what each main list shows; a few of the proposed sample records and any the gaps will drop; and + delegated choices, assumptions, and exclusions. For an Account, say plainly whether people can sign in right + after signing up or confirm their email first, as the + [Accounts reference](foundation-plan-023.md#accounts-and-policies) describes. 2. What is not generated yet. Group the gaps by what the user will notice, lead with that effect, and say what - the app does instead, for example "Photos aren't generated yet; posts are caption-only" or "Like and follower - counts aren't generated yet". Use one line per group rather than one per record, and say which groups you plan - to build in Rails after Compile. -3. A short technical block: the Plan path and SHA-256; the matching valid run's `gap_set_sha256` and record count; + the app does instead, for example "Formatted descriptions aren't generated yet; posts start without one" or + "The rule that usernames can't be admin isn't generated yet; any username is accepted". Use one line per group + rather than one per record, and say which groups you plan to build in Rails after Compile. +3. What is built otherwise: outstanding implementation notes and any part of the idea outside First Draft, each + with where it will be built, and how the selected output mode carries the notes forward. +4. Feasibility findings and open questions, with sources for what research found. Leave out a list with nothing + in it. +5. A short technical block: the Plan path and SHA-256; the matching valid run's `gap_set_sha256` and record count; that service gaps were skipped before semantic analysis, target gaps were analyzed but not fully realized, and - `valid` covers only the admitted graph; the selected output mode; and the gaps file. + `valid` covers only the admitted graph; the selected output mode, or plan only; and the gaps file. Before giving the read-back, write planning-root `gaps-readback.md`: the Plan SHA-256, the `gap_set_sha256`, and every ordered GapSet record with its classification, code, kind, status, location, reason, consequence, and cause diff --git a/test/repository.test.mjs b/test/repository.test.mjs index 290e3d9..88ad571 100644 --- a/test/repository.test.mjs +++ b/test/repository.test.mjs @@ -2949,16 +2949,16 @@ async function checkSkill(skillName) { assert(metadata.description.length > 0); assert(metadata.description.length <= 1024); if (skillName === "create-full-stack-app") { - assert.match(metadata.description, /^Experimental and in development:/); - assert(metadata.description.includes("First Draft Foundation Plan")); + // Discovery starts from the user's goal and must not claim First Draft builds any app it is asked for. + assert.match(metadata.description, /^Use when someone asks for help making or planning an app or game,/); for (const fragment of [ - "Authors and revises First Draft Foundation Plans", - "submits exact bytes", - "Web Accounts, Policies, protected Scaffolds, and required enums are bounded", - "arbitrary apps", - "broader clients are unavailable", + "Experimental", + "researches whether the idea is possible", + "uses First Draft where it helps", + "Foundation Plans, analysis, and bounded Rails/iPhone/Android Compile", + "won't cover the core", ]) { - assert(metadata.description.includes(fragment)); + assert(metadata.description.includes(fragment), `description must include "${fragment}"`); } assert.doesNotMatch(metadata.description, /Accounts[^.;]*are not available/); assert(source.includes("## Load references only when needed"));