@vibes.diy/prompts 14.3.139 → 14.3.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms/access.md CHANGED
@@ -633,6 +633,67 @@ The leaderboard is just the access model: read the public `track:` channels and
633
633
 
634
634
  **Per-object collaboration is the shape of every second or shared thing** (the second worked example above), and certainly of anything that says "invite", "join", "people can join", "collaborate", "share with", "together", "with my partner/team", or names a board/canvas/room/whiteboard a group co-edits — each shared thing is ONE object its members reach directly; it needs no owner. Use the per-object recipe: a channel per object, named from the record (`board:${doc._id}` on the object, `board:${doc.boardId}` on its children); the creator self-grants at creation (`grant: { users: { [user.userHandle]: [ch] } }`); child docs gate on `ctx.requireAccess(ch)` so any member edits any child in it (not just their own); a member-authored `share` doc grants a peer the same channel; a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable. **Two traps to avoid:** don't build it as an open public feed where each person only owns their own items (that abandons the shared membership), and don't gate it behind a single writer (members self-serve via share/request). The app owner is not special in the data model: any signed-in person creates their own object and invites a friend into it, and the rule keys on `user.userHandle` or the object's creator, never on `user.isOwner`.
635
635
 
636
+ **A page a person sets up for their own visitors is a product its provider owns** — a booking page, a storefront, office hours, a menu, a class sign-up. "My page", "my clients" and "my shop" are the words of the service provider speaking, who is usually the person who made the app and is never assumed to be: any signed-in person can set up a page of their own, a provider can run more than one, and nothing in the rules or the screens asks who owns the vibe. The page is a document like any product (`_id: "page-" + crypto.randomUUID()`), its provider in a write-once `creatorHandle`, readable by everyone on its own `page:<id>` channel, and every record a visitor makes on it carries that page's id in `pageId`, so bookings live under their page rather than beside it. A visitor's record is private between that visitor and the page's provider: it routes to the page's inbox channel, which the page branch grants to its provider alone, and to the visitor's own `user:<handle>` channel, which grants them their own; what other visitors need to know — that a time is taken — rides a separate marker holding only the page and the time, routed to the page's public channel; a client who cancels deletes their booking, and the provider or the scheduled run frees the time. A count of what is left — seats in a class, loaves in this week's bake, spots in an evening — rides the same kind of marker: each visitor's record comes with a twin holding only the page, the item and how many it takes, routed to the page's public channel, and what's left is the item's total minus those twins, so the visitors' own records stay between each of them and the provider. Each page has its own link (`#page=<pageId>`): the provider's view shows it to copy and share, a visitor who opens it lands on that page's open times, and someone who opens the app with no page in the link sees the pages they run and can set up a new one. Clients can offer services too, by setting up a page of their own.
637
+
638
+ ### `access.js`
639
+ ```js partial
640
+ export function bookings(doc, oldDoc, user, ctx) {
641
+ if (oldDoc && oldDoc.type !== doc.type) throw { forbidden: "a record keeps its type" }; // a delete arrives as the stored record, type and all
642
+ if (!user?.userHandle && !user?.backend) throw { forbidden: "sign in" };
643
+ const me = user.userHandle;
644
+ const fixed = (...fields) => {
645
+ if (oldDoc && fields.some((f) => doc[f] !== oldDoc[f])) throw { forbidden: "a record keeps its page and its people" };
646
+ };
647
+ const safeId = (id) => {
648
+ if (typeof id !== "string" || !/^[\w-]+$/.test(id)) throw { forbidden: "bad id" };
649
+ return id;
650
+ };
651
+
652
+ // A page: a product anyone signed in sets up and owns. Everyone reads it;
653
+ // its inbox channel is its provider's alone, and holding it is what lets them edit.
654
+ if (doc.type === "page") {
655
+ const page = "page:" + safeId(doc._id);
656
+ // A page is closed (`closed: true`), never deleted: a deleted id could be set up again by
657
+ // someone else, and the inbox its bookings still route to would be theirs.
658
+ if (doc._deleted) throw { forbidden: "close the page instead" };
659
+ if (doc.creatorHandle !== (oldDoc ? oldDoc.creatorHandle : me)) throw { forbidden: "set up your own page" };
660
+ if (oldDoc) ctx.requireAccess(page + "/inbox"); // only its provider edits it
661
+ return { channels: [page], grant: { public: [page], users: { [doc.creatorHandle]: [page + "/inbox"] } } };
662
+ }
663
+
664
+ // Every other record lives under a page.
665
+ const page = "page:" + safeId(doc.pageId);
666
+ fixed("pageId");
667
+
668
+ // A taken time: only its page and time, no name, readable by every visitor. The first write takes
669
+ // it; freeing it is the provider's, or the scheduled run's once its booking is gone.
670
+ if (doc.type === "slot") {
671
+ if (doc._id !== "slot-" + doc.pageId + "-" + doc.start) throw { forbidden: "a slot is named for its page and time" };
672
+ if (Object.keys(doc).some((k) => !["_id", "_rev", "_deleted", "type", "pageId", "start"].includes(k))) throw { forbidden: "a slot carries only its page and time" };
673
+ if (oldDoc) {
674
+ if (user.backend) ctx.requireBackend("scheduled");
675
+ else ctx.requireAccess(page + "/inbox");
676
+ }
677
+ return { channels: [page] };
678
+ }
679
+
680
+ // A booking: read by its client and the page's provider, nobody else.
681
+ if (doc.type === "booking") {
682
+ fixed("clientHandle", "start");
683
+ if (user.backend) ctx.requireBackend("scheduled"); // the reminder run marks it sent
684
+ else if (doc.clientHandle !== me) ctx.requireAccess(page + "/inbox"); // the provider manages any booking on their page
685
+ const client = "user:" + doc.clientHandle;
686
+ return { channels: [page + "/inbox", client], grant: { users: { [doc.clientHandle]: [client] } } };
687
+ }
688
+
689
+ throw { forbidden: "unknown document type" };
690
+ }
691
+
692
+ // Summary: anyone signed in sets up booking pages of their own, which everyone can read, and clients book open times on them; each booking is read only by its client and the page's provider, and a slot record carrying only its page and time tells other clients the time is taken. A page carries its provider's handle in creatorHandle and is closed rather than deleted; a slot and a booking name their page in pageId, and a booking its client in clientHandle.
693
+ ```
694
+
695
+ The provider reads their bookings by `pageId` and sees exactly the ones on their pages; a client reads by `clientHandle` and sees exactly their own. The rules never read the vibe owner: the person who made the app sets up a page the same way as the second provider who opens it, and a remix starts with nobody special in it either. A live calendar feed of a page's bookings follows the same line — the provider subscribes to the bookings on the pages they run (the calendar skill's _A page's bookings, for the provider who runs it_).
696
+
636
697
  **Ownership is just the object graph** — whoever authored or created a doc owns it (`doc.authorHandle === user.userHandle`, checked against `oldDoc` on updates). There's no broadcaster shape to reach for by default, and **owner-only publishing is a dead end** — never gate the content itself on `requireRole("owner")`. **A blog, magazine, or publication is public read + author-owned posts, with the owner controlling the _author roster_:** the owner approves authors with a grant doc (`if (doc.type === "author") { ctx.requireRole("owner"); return { channels: ["blog:authors"], grant: { users: { [doc.authorHandle]: ["blog:authors"] }, roles: { owner: ["blog:authors"] } } }; }` — the **one** place `requireRole("owner")` belongs, gating who may author, never the posts); a post then gates on `ctx.requireAccess("blog:authors")` (membership) and is author-owned (`doc.authorHandle === user.userHandle` + the `oldDoc` check), so once approved each author's post is _their own_ object — only they edit it, and they moderate the comments on it (a comment is allowed if it's your comment **or** you own the post: `doc.authorHandle === user.userHandle || doc.postAuthorHandle === user.userHandle`). A personal blog is just this with a roster of one. Always gate write UI on `useVibe(dbName).can`.
637
698
 
638
699
  The personal blog's post branch is small — public read is the blog's resting state, declared inline on every post result (readers are the point of publishing):
package/llms/calendar.md CHANGED
@@ -131,7 +131,10 @@ backed by a `backend.js` that keeps serving each user's current items as they
131
131
  add more. So this feature is always three pieces working together — a
132
132
  `backend.js` (the `scheduled` tick + `fetch` handler in "Complete backend.js"
133
133
  below), a per-user token, and a `webcal://` link in the UI. Emit all three and
134
- the calendar stays in sync on the user's device automatically.
134
+ the calendar stays in sync on the user's device automatically. On a page people
135
+ set up for their own visitors (a booking page), the creator's feed holds the
136
+ bookings made on their page, which their clients wrote — see _A page's bookings,
137
+ for the provider who runs it_ at the end.
135
138
 
136
139
  A calendar subscription is a live URL the phone re-fetches — `backend.js`
137
140
  serves it and the app links to it with `webcal://`, so the user's items keep
@@ -390,3 +393,61 @@ fetch the API window with `globalThis.fetch`, key rows by the same item id,
390
393
  prefer the live row over the stored snapshot, skip items the API marks
391
394
  cancelled, and keep the snapshot path as the fallback when the API is down or
392
395
  the row is past the window.
396
+
397
+ ## A page's bookings, for the provider who runs it
398
+
399
+ On a page people set up for their own visitors — a booking page, office hours,
400
+ a class sign-up — the page is a product its provider owns, and any signed-in
401
+ person can run pages of their own (the access skill's _A page a person sets up
402
+ for their own visitors_). So the feed the provider subscribes to is **the
403
+ bookings made on the pages they run**, not the items they wrote: a client
404
+ writes every booking, and the provider writes none. Keep the same three pieces
405
+ and the same per-user token; change only what the tick files under each token.
406
+ A booking names its page in `pageId` and a page names its provider in
407
+ `creatorHandle`, so the tick reads the pages first, then files each booking
408
+ under its page's provider and, separately, under its client — the client's own
409
+ feed is their own bookings, the provider's is everyone's on their pages, and
410
+ neither ever sees another client's:
411
+
412
+ ### `backend.js`
413
+ ```js partial
414
+ export async function scheduled(event, ctx) {
415
+ const docs = await ctx.db.query({ db: "bookings" }); // admin-mode read
416
+ const users = new Map(); // handle → [{ itemId, event }], the shape `fetch` above serves
417
+ const tokens = new Map();
418
+ const pages = new Map(); // pageId → the page, whose creatorHandle is the provider who runs it
419
+ // A booking's `start` is an ISO instant, which `toItem` reads as-is; it lasts its `minutes` (30 by default).
420
+ const file = (handle, b, title) => {
421
+ const k = String(handle).toLowerCase();
422
+ if (!users.has(k)) users.set(k, []);
423
+ const start = new Date(b.start);
424
+ if (isNaN(start)) return;
425
+ const end = new Date(start.getTime() + (b.minutes || 30) * 60000).toISOString();
426
+ users.get(k).push({ itemId: String(b._id), event: { date: b.start.slice(0, 10), time: b.start.slice(11), endtime: end.slice(11), title } });
427
+ };
428
+ for (const d of docs) {
429
+ if (d.type === "page" && d.creatorHandle) pages.set(d._id, d);
430
+ if (d.type === "caltoken" && typeof d.token === "string" && /^[A-Za-z0-9_-]{16,64}$/.test(d.token) && d.userId) {
431
+ tokens.set(d.token, String(d.userId).toLowerCase());
432
+ }
433
+ }
434
+ for (const d of docs) {
435
+ if (d.type !== "booking" || typeof d.start !== "string" || !pages.has(d.pageId)) continue;
436
+ const page = pages.get(d.pageId);
437
+ const provider = page.creatorHandle;
438
+ file(provider, d, `${d.clientName || d.clientHandle}`); // the provider: every booking on their pages
439
+ // The client: their own bookings, once — a provider booking their own page is filed already.
440
+ if (d.clientHandle && String(d.clientHandle).toLowerCase() !== String(provider).toLowerCase()) file(d.clientHandle, d, page.name || "Appointment");
441
+ }
442
+ const optedIn = new Set(tokens.values());
443
+ for (const h of [...users.keys()]) if (!optedIn.has(h)) users.delete(h);
444
+ cache = { users, tokens };
445
+ }
446
+ ```
447
+
448
+ A person who both runs a page and books on someone else's gets both in one
449
+ feed. Name the calendar for the page (`X-WR-CALNAME` from the page's name)
450
+ when the subscriber is its provider. The reminder the day before rides the
451
+ same tick: walk the bookings, `ctx.notify` each client whose time is within a
452
+ day, and mark the booking sent, a write the booking's rule admits from the
453
+ scheduled run.
@@ -391,6 +391,7 @@ export default function App() {
391
391
  - Swap `SERVICES` and `TIMES` for the practice's own; several practitioners make the slot `${person} ${day} ${time}`, so each one's calendar fills on its own.
392
392
  - Keep one document per booking and derive who holds a slot from them; a shared "calendar" document with a taken flag per slot loses one of two bookings made at the same moment. Moving a booking is cancelling and booking again; never edit the `slot` in place, or the clash rule loses track of who was first.
393
393
  - Bookings are readable so everyone can see which times are free, so keep private details (a phone number, health notes) out of the booking document, which is why the example asks only for a name; `access.js` can hold those in a separate document only the practice and the booker read.
394
+ - When people set up booking pages of their own — "my clients", "my page", a provider and the people who book them — each page is a product its provider owns, and a booking is read only by its client and that page's provider: the access skill's _A page a person sets up for their own visitors_. The wizard stays the same, but free times come from that page's nameless `slot` records rather than from reading everyone's bookings, and the card lists the viewer's own bookings (or, for the provider, the bookings on their page).
394
395
  - Whoever reads the bookings (the practice's own day view included) applies the same earliest-`createdAt` rule, so everyone agrees who holds a slot; `access.js` can reject a booking whose `createdAt` is far from now, so a wrong clock can't jump the queue.
395
396
  - A new write surface gets its own verdict: `can.create(draft)` with the fields the write stamps, `can.edit(doc)` or `can.delete(doc)` on the document it changes, always on `useVibe` of the database it writes.
396
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.139",
3
+ "version": "14.3.141",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,10 +39,10 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/api-types": "14.3.139",
43
- "@vibes.diy/call-ai-v2": "14.3.139",
44
- "@vibes.diy/identity": "14.3.139",
45
- "@vibes.diy/use-vibes-types": "14.3.139",
42
+ "@vibes.diy/api-types": "14.3.141",
43
+ "@vibes.diy/call-ai-v2": "14.3.141",
44
+ "@vibes.diy/identity": "14.3.141",
45
+ "@vibes.diy/use-vibes-types": "14.3.141",
46
46
  "arktype": "~2.2.6",
47
47
  "json-schema-faker": "~0.6.3"
48
48
  },
@@ -406,6 +406,7 @@ The control and its handler check the **same complete candidate** that `database
406
406
  Write this turn's `access.js` against precisely these shapes: the person's own records route to the channel named by their handle and grant it on every write; a friend's write into that tree needs the channel a `member` record granted them; a second or shared object routes to the channel named by its own `_id` and grants its creator that channel and its admin channel; membership grants the place it names. Keep each record's type, author and place fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes the place and the weaving id from the displayed weaving, and the rule holds them fixed on subsequent writes. Nothing writes on load or on mount: the first screen opens on the person's tree because the channel is named by their handle, and every record exists because the person made it.
407
407
 
408
408
  - **Keep the opening view useful.** The person arrives inside their own tree: the first screen is the app at work, named for the app, with a rename that writes the settings record when they want a name of their own. The header carries the app's name once; a separate name line becomes useful after a rename or a second object. A second-object form joins the object list when useful. Show the person's own tree, each tree a membership lets them into, and the whole readable type-query result of second objects, including ones someone else made; `creatorHandle` identifies their makers.
409
+ - **A page set up for visitors is a product its provider owns.** "My booking page", "my clients", "my menu" are the words of the service provider speaking — usually the person who made the app, never assumed to be — so every signed-in person can set up pages of their own (`_id: "page-" + crypto.randomUUID()`, a write-once `creatorHandle`), what visitors make on it carries its `pageId` and is read only by that visitor and the page's provider, and each page has its own `#page=<pageId>` link its provider copies to share. Clients can offer services too, by setting up a page of their own. No rule or screen reads who owns the vibe. The access skill's _A page a person sets up for their own visitors_ has the rules.
409
410
  - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + place + ":" + handle, type: "member", studioHandle: me.userHandle, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }` for the person's own tree, or the same record with `loomId` for a loom. Every member record carries the same `studioHandle` as the tree it joins — the field the rules require of every record in a tree — in the `can.create` preview that decides whether the card shows and in the record the invite saves, both built by one `memberFor`. Construct that value after the picker resolves the handle, gate it with `can.create`, then persist the same value. The `member` branch admits exactly that shape: in a tree the writer grants their own channel, on a second object its admin grants that object's channel, and the subject, place and writer stay fixed. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When the app is a person's own collection, this same control is its first-screen share, with the line "Yours alone — add a friend to share it" beside it; otherwise, when sharing was not asked for, offer inviting as a closing option.
410
411
  - **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
411
412
  - **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the place's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
@@ -417,7 +418,7 @@ When the user's prompt hands you **concrete example data** — an image of items
417
418
 
418
419
  **Required starting state is saved by an action, never by the app on load.** A person's own action — a tap, a submit, a pick — is what starts a write; nothing writes on load or on mount. A person's own starting state needs no record: their records live on the channel named by their handle, and the view renders its defaults from constants until the person's own action saves one. App-wide named containers or participants the request requires — the requested pottery studio and its named apprentice — are written by the agent building the app with `put_doc` as the owner during the build, once the rules that admit them are live, and read back; that save is the owner's own action. Give each one a stable identity, write parents and their membership or role grants before what depends on them, and let the opening view show them when they are there and an inviting add flow until they are. `access.js` admits exactly those records, and the app's code holds the handlers a person uses to add more. Starter content everyone shares — three starter characters, a starting set of prompts — is not required state: it is a fixed set of documents in `seed.json`, and the app's code writes no copy of it for each visitor. Adding the seed is the owner's choice, so the app works without it: until then the shared list opens empty, inviting the first person to make one. `seed.json` stays the optional example-content import: a name present only there describes an offered import, not an existing record or grant.
419
420
 
420
- **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
421
+ **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. A page someone sets up for their own visitors — a booking page, a shop's menu, office hours — is a product its provider owns: the provider is usually the person who made the app and is never assumed to be, so any signed-in person who opens the app can set up a page of their own, its provider in a write-once `creatorHandle`, with what visitors make on it carrying the page's id and read by that visitor and the page's provider (the access skill's _A page a person sets up for their own visitors_); the seed's page is `"@owner"`'s own page in that shape. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
421
422
 
422
423
  **The seed dresses the opening screen.** The screenshot and the owner's first look both see one phone screen, so write the seed for that screen: the richest, most representative state of the app lives on the view it opens to, filled with just enough records to cover that one screen at the size the view draws them — a few rich cards, or a handful more compact rows — a lived-in week rather than a wall. Give that view one of each state the app knows how to show, so every feature is visible at first glance: a canoe club's trip board opens on a trip already paddled, one coming up next and one just posted with its put-in note; a sourdough starter log opens on a feed marked done, one due next, and a fresh loaf with its note. Every sample belongs to the owner, and its names, notes and numbers read like a real person's, so the first screen looks like someone has been happily using the app for a while.
423
424
 
@@ -410,6 +410,7 @@ The control and its handler check the **same complete candidate** that `database
410
410
  Write this turn's `access.js` against precisely these shapes: the person's own records route to the channel named by their handle and grant it on every write; a friend's write into that tree needs the channel a `member` record granted them; a second or shared object routes to the channel named by its own `_id` and grants its creator that channel and its admin channel; membership grants the place it names. Keep each record's type, author and place fixed across edits. A bobbin's `_id` equals `bobbin-<authorHandle>-<weavingId>`; the client takes the place and the weaving id from the displayed weaving, and the rule holds them fixed on subsequent writes. Nothing writes on load or on mount: the first screen opens on the person's tree because the channel is named by their handle, and every record exists because the person made it.
411
411
 
412
412
  - **Keep the opening view useful.** The person arrives inside their own tree: the first screen is the app at work, named for the app, with a rename that writes the settings record when they want a name of their own. The header carries the app's name once; a separate name line becomes useful after a rename or a second object. A second-object form joins the object list when useful. Show the person's own tree, each tree a membership lets them into, and the whole readable type-query result of second objects, including ones someone else made; `creatorHandle` identifies their makers.
413
+ - **A page set up for visitors is a product its provider owns.** "My booking page", "my clients", "my menu" are the words of the service provider speaking — usually the person who made the app, never assumed to be — so every signed-in person can set up pages of their own (`_id: "page-" + crypto.randomUUID()`, a write-once `creatorHandle`), what visitors make on it carries its `pageId` and is read only by that visitor and the page's provider, and each page has its own `#page=<pageId>` link its provider copies to share. Clients can offer services too, by setting up a page of their own. No rule or screen reads who owns the vibe. The access skill's _A page a person sets up for their own visitors_ has the rules.
413
414
  - **Invite with the actual membership candidate.** When the ask names sharing, a group or other people, include `HandleInput` from `useViewer()` and a handler that saves `{ _id: "member:" + place + ":" + handle, type: "member", studioHandle: me.userHandle, userHandle: handle, addedBy: me.userHandle, authorHandle: me.userHandle }` for the person's own tree, or the same record with `loomId` for a loom. Every member record carries the same `studioHandle` as the tree it joins — the field the rules require of every record in a tree — in the `can.create` preview that decides whether the card shows and in the record the invite saves, both built by one `memberFor`. Construct that value after the picker resolves the handle, gate it with `can.create`, then persist the same value. The `member` branch admits exactly that shape: in a tree the writer grants their own channel, on a second object its admin grants that object's channel, and the subject, place and writer stay fixed. A resolved `invite:<id>` is stored as the subject just like a handle. Await a real membership write and readback before saying a named person is in. When the app is a person's own collection, this same control is its first-screen share, with the line "Yours alone — add a friend to share it" beside it; otherwise, when sharing was not asked for, offer inviting as a closing option.
414
415
  - **Persist the sharing choice.** A public/follower toggle writes the field its rules read (`isPublic`, `sharedWithFollowers`), with the full edited candidate checked before saving. Its who-can-see line sits beside that control and describes the channel the rules actually share. Follow `access.md` for follower consent and channel granularity.
415
416
  - **Protect supporting records.** A membership record containing contact details or a capability token belongs only to its intended readers; choose its private channel accordingly while its grant admits the recipient to the place's channel. Keep contact/token-bearing types distinct from ordinary content and out of content queries and Ask inputs.
@@ -421,7 +422,7 @@ When the user's prompt hands you **concrete example data** — an image of items
421
422
 
422
423
  **Required starting state is saved by an action, never by the app on load.** A person's own action — a tap, a submit, a pick — is what starts a write; nothing writes on load or on mount. A person's own starting state needs no record: their records live on the channel named by their handle, and the view renders its defaults from constants until the person's own action saves one. App-wide named containers or participants the request requires — the requested pottery studio and its named apprentice — are written by the agent building the app with `put_doc` as the owner during the build, once the rules that admit them are live, and read back; that save is the owner's own action. Give each one a stable identity, write parents and their membership or role grants before what depends on them, and let the opening view show them when they are there and an inviting add flow until they are. `access.js` admits exactly those records, and the app's code holds the handlers a person uses to add more. Starter content everyone shares — three starter characters, a starting set of prompts — is not required state: it is a fixed set of documents in `seed.json`, and the app's code writes no copy of it for each visitor. Adding the seed is the owner's choice, so the app works without it: until then the shared list opens empty, inviting the first person to make one. `seed.json` stays the optional example-content import: a name present only there describes an offered import, not an existing record or grant.
423
424
 
424
- **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
425
+ **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. A page someone sets up for their own visitors — a booking page, a shop's menu, office hours — is a product its provider owns: the provider is usually the person who made the app and is never assumed to be, so any signed-in person who opens the app can set up a page of their own, its provider in a write-once `creatorHandle`, with what visitors make on it carrying the page's id and read by that visitor and the page's provider (the access skill's _A page a person sets up for their own visitors_); the seed's page is `"@owner"`'s own page in that shape. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
425
426
 
426
427
  **The seed dresses the opening screen.** The screenshot and the owner's first look both see one phone screen, so write the seed for that screen: the richest, most representative state of the app lives on the view it opens to, filled with just enough records to cover that one screen at the size the view draws them — a few rich cards, or a handful more compact rows — a lived-in week rather than a wall. Give that view one of each state the app knows how to show, so every feature is visible at first glance: a canoe club's trip board opens on a trip already paddled, one coming up next and one just posted with its put-in note; a sourdough starter log opens on a feed marked done, one due next, and a fresh loaf with its note. Every sample belongs to the owner, and its names, notes and numbers read like a real person's, so the first screen looks like someone has been happily using the app for a while.
427
428
 
package/system-prompt.md CHANGED
@@ -725,7 +725,7 @@ When the user's prompt hands you **concrete example data** — an image of items
725
725
 
726
726
  **Required starting state is saved by an action, never by the app on load.** A person's own action — a tap, a submit, a pick — is what starts a write; nothing writes on load or on mount. A person's own starting state needs no record: their records live on the channel named by their handle, and the view renders its defaults from constants until the person's own action saves one. App-wide named containers or participants the request requires — the requested pottery studio and its named apprentice — are written by the agent building the app with `put_doc` as the owner during the build, once the rules that admit them are live, and read back; that save is the owner's own action. Give each one a stable identity, write parents and their membership or role grants before what depends on them, and let the opening view show them when they are there and an inviting add flow until they are. `access.js` admits exactly those records, and the app's code holds the handlers a person uses to add more. Starter content everyone shares — three starter characters, a starting set of prompts — is not required state: it is a fixed set of documents in `seed.json`, and the app's code writes no copy of it for each visitor. Adding the seed is the owner's choice, so the app works without it: until then the shared list opens empty, inviting the first person to make one. `seed.json` stays the optional example-content import: a name present only there describes an offered import, not an existing record or grant.
727
727
 
728
- **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
728
+ **Content the owner authors for everyone else ships its examples in `seed.json`.** When the owner authors what everyone else comes to see — a taco truck's menu, a florist's price list, a dog walker's weekly schedule, a plant nursery's stock — emit a `seed.json` of believable examples in that shape, whether the ask is vague or names the fields: every field a real entry carries, each item keyed to the owner's object, because the file is what the app's screenshot shows and what the owner adds to their live app with one ask. A page someone sets up for their own visitors — a booking page, a shop's menu, office hours — is a product its provider owns: the provider is usually the person who made the app and is never assumed to be, so any signed-in person who opens the app can set up a page of their own, its provider in a write-once `creatorHandle`, with what visitors make on it carrying the page's id and read by that visitor and the page's provider (the access skill's _A page a person sets up for their own visitors_); the seed's page is `"@owner"`'s own page in that shape. The app's own code writes nothing invented and nothing on load — only what a person's own action enters. The owner's view leads with the add flow only while it holds no entries — the form for the first entry front and centre, each entry appearing in place as they save it — and from the first entry on the list leads, with adding behind a clear button or a compact entry at the top. That switch waits for the person to finish: an add flow that saves as they go — a draft that autosaves at each pause — stays open through its own first save, because which entry is open is state, set as the person starts the entry and before its first save, never a reading of the list. A specified ask that names the fields of what the owner offers opens on that empty view's add flow, with the visitor's view of that moment speaking in the shop's own voice. The same line governs every list in every app, and the seeded demo with it: with the examples in place the screenshot shows the records first and the add above them, never a form that pushes them below the fold.
729
729
 
730
730
  **The seed dresses the opening screen.** The screenshot and the owner's first look both see one phone screen, so write the seed for that screen: the richest, most representative state of the app lives on the view it opens to, filled with just enough records to cover that one screen at the size the view draws them — a few rich cards, or a handful more compact rows — a lived-in week rather than a wall. Give that view one of each state the app knows how to show, so every feature is visible at first glance: a canoe club's trip board opens on a trip already paddled, one coming up next and one just posted with its put-in note; a sourdough starter log opens on a feed marked done, one due next, and a fresh loaf with its note. Every sample belongs to the owner, and its names, notes and numbers read like a real person's, so the first screen looks like someone has been happily using the app for a while.
731
731