@vibes.diy/prompts 14.3.80 → 14.3.81

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
@@ -34,7 +34,7 @@ writes.
34
34
 
35
35
  Express every refusal about **who may act** through the supplied validators — `ctx.requireAccess`, `ctx.requireRole`, `ctx.requireBackend` — so that admin mode passes them and the function still runs, applying your own channels and grants to the repaired document; a refusal about the document's own **shape**, like an unknown type or a missing field, stays a plain `throw` and is right to, while a write-once check sits in between and is worth naming in a comment, since a schema repair has to get past it.
36
36
 
37
- A component-owned image database (`media:<hostDbName>`) uses platform-authored rules by default: each generated image is readable by whoever can read the record it was generated for, and by the person who generated it. An explicit string-named export such as `export { posterMedia as "media:posters" }` overrides that one media database. Neither `export default` nor a function merely named `media` overrides it. Within those, platform-driven writes onto the app's own docs — like an `<ImgGen>` version append (the `ctx.isImgGenVersionAppend` shape) — flow through your function like any other write.
37
+ A component-owned image database (`media:<hostDbName>`) uses platform-authored rules by default: each generated image is readable by whoever can read the record it was generated for, and by the person who generated it. An explicit string-named export such as `export { posterMedia as "media:posters" }` overrides that one media database. Neither `export default` nor a function merely named `media` overrides it. Your function governs your app's own records, and every picture `<ImgGen>` makes is saved in that media database as its creator's own record, so pictures need no branch of their own in yours.
38
38
 
39
39
  ### AccessDescriptor return type
40
40
 
@@ -175,9 +175,9 @@ would copy draft prompts and prior versions. Unpublishing removes that record's
175
175
  public grant. Explicit media bindings fail closed on older access outputs until
176
176
  the publish-time backfill evaluates them under the new rule.
177
177
 
178
- ### The ImgGen version-append nuance
178
+ ### Pictures on shared records
179
179
 
180
- The platform predicate `ctx.isImgGenVersionAppend(doc, oldDoc)` accepts exactly one legitimate ImgGen version append by a non-author — prior versions/files and all other fields must be unchanged, so a field you didn't anticipate stays author-protected by default; deletes stay author-only. Because `versions`/`currentVersion` are among the version fields it allows, a non-author can also advance the DISPLAYED version (the shared-generation feature); guard `currentVersion` specifically for author-only display control. Write-once docs can simply `if (oldDoc === null) {} else throw`.
180
+ Anyone who can read a record can make and keep pictures for it: each generation is saved in the media database as that person's own record, so a shared record stays author-edited while everyone's pictures land. Write-once docs can simply `if (oldDoc === null) {} else throw`.
181
181
 
182
182
  ### `_id` strategy matters
183
183
 
@@ -316,14 +316,12 @@ export function chat(doc, oldDoc, user, ctx) {
316
316
  if (!user) throw { forbidden: "authentication required" };
317
317
 
318
318
  if (doc.type === "message") {
319
- // Author fixed at create; ownership immutable. On update a non-author may
320
- // only append one legitimate ImgGen version — the platform predicate
321
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
319
+ // Author fixed at create and kept for good; the author edits their own.
322
320
  if (oldDoc === null) {
323
321
  if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
324
322
  } else if (doc.userHandle !== oldDoc.userHandle) {
325
323
  throw { forbidden: "cannot change author" };
326
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.userHandle !== user.userHandle) {
324
+ } else if (oldDoc.userHandle !== user.userHandle) {
327
325
  throw { forbidden: "not author" };
328
326
  }
329
327
  ctx.requireAccess(doc.channelId);
@@ -334,7 +332,7 @@ export function chat(doc, oldDoc, user, ctx) {
334
332
  }
335
333
  ```
336
334
 
337
- `ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates of shared-visible docs, keep ownership immutable (`if (oldDoc && doc.<authorField> !== oldDoc.<authorField>) throw`, where `<authorField>` is your doc's author field — `authorHandle`/`userHandle`/`senderHandle`) rather than requiring the updater to BE the author — image version appends run as the VIEWING user, and an author-only update gate turns every other viewer's generation into a billed deny-retry. Reserve author-only updates for genuinely private per-user docs; on shared docs, let a non-author through only when `ctx.isImgGenVersionAppend(doc, oldDoc)` accepts the write (as `chat.message` above shows) — it permits exactly one legitimate ImgGen version append and requires every other field unchanged.
335
+ `ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates, keep ownership immutable (`if (oldDoc && doc.<authorField> !== oldDoc.<authorField>) throw`, where `<authorField>` is your doc's author field — `authorHandle`/`userHandle`/`senderHandle`) and let the author edit their own (as `chat.message` above shows). Pictures other viewers make for a shared doc are their own records in the media database, so an author-only update keeps every picture flowing.
338
336
 
339
337
  **A follow-up edit that adds a NEW doc type updates `access.js` FIRST.** The app runs live while your edits stream in, and writes are enforced against the access.js that was in force before this turn until the whole turn completes — so a `db.put` of a doc type the old function rejects fails immediately (`unknown document type`), even though your access.js update lands later in the same reply. Emit the access.js edit adding the new type's branch before the App.jsx edits that write it. And never fire-and-forget background writes of a newly added type: gate seed/auto-write effects on `useVibe(dbName)` — `if (!ready || !can.create(sampleDoc).ok) return;` with `ready`/`can` in the effect deps, checking a representative sample of **each** doc type the effect writes — so a not-yet-allowed write is skipped quietly instead of surfacing rejection errors the user didn't cause.
340
338
 
@@ -377,14 +375,12 @@ export function wall(doc, oldDoc, user, ctx) {
377
375
  }
378
376
 
379
377
  if (doc.type === "post") {
380
- // Author fixed at create; ownership immutable. On update a non-author may
381
- // only append one legitimate ImgGen version — the platform predicate
382
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
378
+ // Author fixed at create and kept for good; the author edits their own.
383
379
  if (oldDoc === null) {
384
380
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
385
381
  } else if (doc.authorHandle !== oldDoc.authorHandle) {
386
382
  throw { forbidden: "cannot change author" };
387
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
383
+ } else if (oldDoc.authorHandle !== user.userHandle) {
388
384
  throw { forbidden: "not author" };
389
385
  }
390
386
  return { channels: [doc.channelId] };
@@ -420,7 +416,7 @@ export function board(doc, oldDoc, user, ctx) {
420
416
  }
421
417
  ```
422
418
 
423
- `ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open feed anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/share/request doc must also return `channels` — a channel-less result is rejected. On updates of shared-visible docs, keep ownership immutable (`doc.authorHandle !== oldDoc.authorHandle` → throw) rather than requiring the updater to BE the author — image version appends run as the viewing user, and an author-only update gate turns every other viewer's generation into a billed deny-retry. Reserve author-only updates for genuinely private per-user docs; on shared docs, let a non-author through only when `ctx.isImgGenVersionAppend(doc, oldDoc)` accepts the write (as `wall.post` above shows) — it permits exactly one legitimate ImgGen version append and requires every other field unchanged.
419
+ `ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open feed anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/share/request doc must also return `channels` — a channel-less result is rejected. On updates, keep ownership immutable (`doc.authorHandle !== oldDoc.authorHandle` → throw) and let the author edit their own (as `wall.post` above shows). Pictures other viewers make for a shared doc are their own records in the media database, so an author-only update keeps every picture flowing.
424
420
 
425
421
  **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own entries, post a note, drop a pin, join a shared canvas. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
426
422
 
@@ -616,14 +612,12 @@ export function announcements(doc, oldDoc, user, ctx) {
616
612
  }
617
613
 
618
614
  if (doc.type === "post") {
619
- // Author fixed at create; ownership immutable. On update a non-author may
620
- // only append one legitimate ImgGen version — the platform predicate
621
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
615
+ // Author fixed at create and kept for good; the author edits their own.
622
616
  if (oldDoc === null) {
623
617
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
624
618
  } else if (doc.authorHandle !== oldDoc.authorHandle) {
625
619
  throw { forbidden: "cannot change author" };
626
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
620
+ } else if (oldDoc.authorHandle !== user.userHandle) {
627
621
  throw { forbidden: "not author" };
628
622
  }
629
623
  ctx.requireAccess(doc.channel);
@@ -759,14 +753,12 @@ export function chat(doc, oldDoc, user, ctx) {
759
753
  }
760
754
 
761
755
  if (doc.type === "post") {
762
- // Author fixed at create; ownership immutable. On update a non-author may
763
- // only append one legitimate ImgGen version — the platform predicate
764
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
756
+ // Author fixed at create and kept for good; the author edits their own.
765
757
  if (oldDoc === null) {
766
758
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
767
759
  } else if (doc.authorHandle !== oldDoc.authorHandle) {
768
760
  throw { forbidden: "cannot change author" };
769
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
761
+ } else if (oldDoc.authorHandle !== user.userHandle) {
770
762
  throw { forbidden: "not author" };
771
763
  }
772
764
  // Any signed-in author may post to this open channel. Do NOT call
@@ -1100,14 +1092,12 @@ export function chat(doc, oldDoc, user, ctx) {
1100
1092
  }
1101
1093
 
1102
1094
  if (doc.type === "message") {
1103
- // Author fixed at create; ownership immutable. On update a non-author may
1104
- // only append one legitimate ImgGen version — the platform predicate
1105
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
1095
+ // Author fixed at create and kept for good; the author edits their own.
1106
1096
  if (oldDoc === null) {
1107
1097
  if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
1108
1098
  } else if (doc.userHandle !== oldDoc.userHandle) {
1109
1099
  throw { forbidden: "cannot change author" };
1110
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.userHandle !== user.userHandle) {
1100
+ } else if (oldDoc.userHandle !== user.userHandle) {
1111
1101
  throw { forbidden: "not author" };
1112
1102
  }
1113
1103
  ctx.requireAccess(doc.channelId);
package/llms/calendar.js CHANGED
@@ -1,7 +1,24 @@
1
1
  export const calendarConfig = {
2
2
  name: "calendar",
3
3
  label: "calendar-ics",
4
- description: "live calendar subscriptions (webcal/.ics): serve each user's dated items — favorites, RSVPs, picks, bookings — as a phone-calendar feed that updates automatically as they add more; includes the backend.js aggregation recipe and the iOS-proof ICS format rules",
5
- cues: ["calendar", "webcal", "ics", "calendar feed", "calendar subscription", "rsvp", "ical"],
4
+ description: "dates and calendars: any app that keeps a date — due dates, deadlines, streaks and check-ins, schedules and shifts, reminders, timelines, 'tomorrow', 'Friday', 'every Monday' — resolves relative words against today, saves at once and shows the resolved date, and dates its sample records around the day they are written so the demo opens alive; plus live calendar subscriptions (webcal/.ics): each user's dated items as a phone-calendar feed that updates as they add more, with the backend.js aggregation recipe and the iOS-proof ICS format rules",
5
+ cues: [
6
+ "calendar",
7
+ "date",
8
+ "due date",
9
+ "deadline",
10
+ "streak",
11
+ "schedule",
12
+ "reminder",
13
+ "timeline",
14
+ "tomorrow",
15
+ "every monday",
16
+ "webcal",
17
+ "ics",
18
+ "calendar feed",
19
+ "calendar subscription",
20
+ "rsvp",
21
+ "ical",
22
+ ],
6
23
  };
7
24
  //# sourceMappingURL=calendar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../jsr/llms/calendar.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,cAAc;IACrB,WAAW,EACT,oQAAoQ;IACtQ,IAAI,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,CAAC;CAC9F,CAAC"}
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../jsr/llms/calendar.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,cAAc,GAAc;IACvC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,cAAc;IACrB,WAAW,EACT,oiBAAoiB;IAItiB,IAAI,EAAE;QACJ,UAAU;QACV,MAAM;QACN,UAAU;QACV,UAAU;QACV,QAAQ;QACR,UAAU;QACV,UAAU;QACV,UAAU;QACV,UAAU;QACV,cAAc;QACd,QAAQ;QACR,KAAK;QACL,eAAe;QACf,uBAAuB;QACvB,MAAM;QACN,MAAM;KACP;CACF,CAAC"}
package/llms/calendar.md CHANGED
@@ -1,4 +1,122 @@
1
- # Calendar subscriptions — a live .ics feed of each user's items
1
+ # Dates and calendars — the app knows today; subscriptions as a live .ics feed
2
+
3
+ ## Dates — the app knows today
4
+
5
+ An app that keeps a date — a due date, a deadline, a run of daily check-ins, a
6
+ service schedule, a reminder, a timeline — is working against a clock that is
7
+ always on. `new Date()` is today, in the person's own time zone, at the moment
8
+ they type. The build context states today's date too (the `<today>` line), so a
9
+ record you write and a sample you author both start from a known day.
10
+
11
+ **Relative words resolve against now, save at once, and show the resolved date.**
12
+ "Tomorrow", "Friday", "next week", "the 15th" each name exactly one day from
13
+ where the person stands; resolve it with `new Date()` when the record is saved,
14
+ save the ISO day, and show that day beside the record so one tap adjusts it.
15
+ The saved card is the confirmation and the date on it is the question answered.
16
+ A sentence-intake schema call reads dates the same way: the person's local day
17
+ goes into its prompt (`Today: ${keyOf(new Date())}`, with `keyOf` below), as the
18
+ intake samples in the fireproof and callai docs do with their own `Today:`. Days
19
+ are kept as local `YYYY-MM-DD` strings (`keyOf`), so a date stays the day the
20
+ person meant across time zones.
21
+
22
+ ### `App.jsx`
23
+ ```jsx app=practice-log
24
+ import React, { useState } from "react";
25
+ import { useFireproof } from "use-fireproof";
26
+ import { Button, Field, Input, Row, Stack, Text } from "@vibes.diy/pouf";
27
+
28
+ const pad = (n) => String(n).padStart(2, "0");
29
+ const keyOf = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
30
+ const DAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
31
+
32
+ const daysInMonth = (year, month) => new Date(year, month + 1, 0).getDate();
33
+ // "the 15th" → the next month, this one included, that has a 15th and has not passed it.
34
+ function nextDayOfMonth(n, now) {
35
+ if (n < 1 || n > 31) return null;
36
+ for (let i = 0; i < 12; i += 1) {
37
+ const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
38
+ if (n > daysInMonth(d.getFullYear(), d.getMonth())) continue;
39
+ d.setDate(n);
40
+ if (d >= now || keyOf(d) === keyOf(now)) return d;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ // "tomorrow", "friday", "next week", "the 15th" → the one day they mean from today.
46
+ function resolveDay(words, now = new Date()) {
47
+ const text = words.toLowerCase();
48
+ const day = new Date(now);
49
+ const weekday = DAYS.findIndex((name) => text.includes(name));
50
+ if (text.includes("tomorrow")) day.setDate(day.getDate() + 1);
51
+ else if (text.includes("next week")) day.setDate(day.getDate() + 7);
52
+ else if (weekday >= 0) day.setDate(day.getDate() + ((weekday - day.getDay() + 7) % 7 || 7));
53
+ else {
54
+ const nth = text.match(/\bthe (\d{1,2})(?:st|nd|rd|th)?\b/);
55
+ if (nth) return nextDayOfMonth(Number(nth[1]), now) && keyOf(nextDayOfMonth(Number(nth[1]), now));
56
+ }
57
+ return keyOf(day);
58
+ }
59
+
60
+ export default function App() {
61
+ const { database, useLiveQuery } = useFireproof("practice-log");
62
+ const { docs: sessions } = useLiveQuery("type", { key: "session" });
63
+ const [words, setWords] = useState("");
64
+ const [day, setDay] = useState(keyOf(new Date()));
65
+ const today = keyOf(new Date());
66
+ const plan = (text) => {
67
+ setWords(text);
68
+ // A day the words name lands in the date field; "the 31st" in a short month leaves it for the person to pick.
69
+ setDay(resolveDay(text) ?? "");
70
+ };
71
+ const save = async () => {
72
+ if (!words.trim() || !day) return;
73
+ await database.put({ type: "session", piece: words.trim(), day, createdAt: Date.now() });
74
+ setWords("");
75
+ setDay(today);
76
+ };
77
+ return (
78
+ <Stack gap={3}>
79
+ <Field label="Plan a practice">{(id, d) => <Input id={id} describedBy={d} value={words} onChange={plan} placeholder="Scales on friday" />}</Field>
80
+ <Row gap={2}>
81
+ <Field label="On">{(id, d) => <Input id={id} describedBy={d} type="date" value={day} onChange={setDay} />}</Field>
82
+ <Button onClick={save}>Save</Button>
83
+ </Row>
84
+ {sessions.map((s) => (
85
+ <Row key={s._id} gap={2}>
86
+ <Text>{s.piece}</Text>
87
+ <Text size="sm" muted>{s.day === today ? "today" : s.day}</Text>
88
+ </Row>
89
+ ))}
90
+ </Stack>
91
+ );
92
+ }
93
+ ```
94
+
95
+ **Sample records are dated around the day they are written.** A demo opens
96
+ alive when its examples sit where the app's own views look: recent activity in
97
+ the last few days, an unbroken run of daily entries reaching today, upcoming
98
+ items this week and next, a month grid with this month's entries on it. Write
99
+ each seed date from the `<today>` line as an offset — yesterday, three days ago,
100
+ this coming Saturday — so a reader of the seed can see the run reaching the day
101
+ it was written. The example below was written on a `<today>` of `2026-10-06`:
102
+ `day` runs three days ago, two days ago, yesterday, today, and the plan lands
103
+ on the coming Saturday (`2026-10-10`); on another day, the same offsets give
104
+ other dates.
105
+
106
+ ### `seed.json`
107
+ ```json
108
+ {
109
+ "practice-log": [
110
+ { "key": "scales-1", "type": "session", "piece": "G major scales", "day": "2026-10-03" },
111
+ { "key": "etude-1", "type": "session", "piece": "Kreutzer etude 2", "day": "2026-10-04" },
112
+ { "key": "scales-2", "type": "session", "piece": "G major scales, faster", "day": "2026-10-05" },
113
+ { "key": "sonata", "type": "session", "piece": "Sonata first movement", "day": "2026-10-06" },
114
+ { "key": "recital", "type": "session", "piece": "Run the recital program", "day": "2026-10-10" }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ ## Calendar subscriptions — a live .ics feed of each user's items
2
120
 
3
121
  When an app has per-user dated items — favorited events, RSVPs, picks,
4
122
  bookings, shifts — offer a **calendar subscription**: a `webcal://` link that
package/llms/image-gen.md CHANGED
@@ -212,30 +212,9 @@ public facts. Public viewers render its `_files` URL with `<img>`; keep generati
212
212
  and revision controls on the creator's draft. Share the same `useImgGen` hook
213
213
  result between preview, download and publication so they select the same version.
214
214
 
215
- ## Access Functions and `doc.type`
215
+ ## Access Functions and pictures
216
216
 
217
- If your app defines an access function that keys on `doc.type` (the common pattern — e.g. it throws `{ forbidden: "unknown document type" }` for any unenumerated type), be aware of how ImgGen sets `type`:
218
-
219
- - **Image attached to a host doc** (`<ImgGen _id={hostDoc._id} database={db} />`): the image inherits the host doc's existing `type` (`story`, `hat`, whatever) and its access/channels. The write passes a type-keyed access function with no extra work.
220
- - **Standalone image** (no `_id`, or a brand-new doc): the doc is written with `type: "image"`. A type-keyed access function will **reject** this unless it explicitly permits `"image"`. Add an `"image"` branch (with the grant/channel you want image docs to live on) so the write is accepted — otherwise image generation fails silently with `Failed to put document:`.
221
-
222
- Prefer attaching images to the host doc whose access you already grant; reach for standalone image docs only when you've added an `"image"` branch to the access function.
223
-
224
- **ImgGen does not stamp your app's author field.** The doc ImgGen writes (a standalone `type: "image"` doc, or the version it appends to a host doc) carries no `authorHandle`/`userHandle`/`senderHandle` of yours — that field is `null`/`undefined`. So an access-fn branch that author-gates the type ImgGen writes (`if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }`) **rejects the write**: `null !== user.userHandle`, the signed-in maker included, and the image silently fails to persist as the doc you meant (`Failed to put document:`). Two fixes: (1) **attach the image to a host doc you created with the author field already stamped** — `<ImgGen _id={hostDoc._id} database={db} />` inherits the host's `type` and access, so the author check already passed when you wrote the host; this is the preferred shape. (2) For a genuinely **standalone** image doc, do NOT author-gate the `"image"` branch on strict author equality — route it to the channel you want and skip the author check (the platform's ImgGen write is server-authoritative, so there's no spoof to guard against). Never author-gate a type whose docs ImgGen originates without stamping the author yourself first.
225
-
226
- **A version append (regenerate, or an img2img edit on an existing `_id`) arrives as the VIEWING user, not the original author.** So even when you DID stamp the author on the host doc, an `if (oldDoc && doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }` on that type denies any _other_ signed-in viewer's generation on a shared/public-read doc — after the platform already billed it, arming an unbounded billed-retry loop (#3784/#3832). Author-equality belongs on **create and ownership change**, not on every update. Fix the author at create, forbid re-authoring, and for a non-author update accept only a legitimate ImgGen version append — the platform predicate `ctx.isImgGenVersionAppend(doc, oldDoc)` decides that (`oldDoc` is `null` on create):
227
-
228
- ```js
229
- if (oldDoc === null) {
230
- if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
231
- } else if (doc.authorHandle !== oldDoc.authorHandle) {
232
- throw { forbidden: "cannot change author" };
233
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
234
- throw { forbidden: "not author" };
235
- }
236
- ```
237
-
238
- The platform predicate `ctx.isImgGenVersionAppend` accepts exactly one legitimate ImgGen version append by a non-author — prior versions/files and all other fields must be unchanged, so a field you didn't anticipate stays author-protected by default; deletes stay author-only. Because `versions`/`currentVersion` are among the version fields it allows, a non-author can also advance the DISPLAYED version (the shared-generation feature); guard `currentVersion` specifically if you need author-only display control.
217
+ Your `access.js` governs your app's own records — the story, the hat, the roast — and ImgGen keeps its pictures beside them in `media:<database>`, under the platform's media rules: every generation, a regenerate or an img2img edit included, is a new `version` record authored by whoever asked for it, and each person keeps their own `pointer` record for the picture they are viewing. So a type-keyed access function with one branch per record type already serves an image app, author checks and all; give each record the author and channel its own sharing calls for. When pictures should be more open or more closed than their record, write the access skill's media export, which checks that `doc.authorHandle` is the caller and admits both `version` and `pointer` records.
239
218
 
240
219
  ## Choosing a Model
241
220
 
package/llms/use-vibe.md CHANGED
@@ -18,6 +18,21 @@ While `pending` is set, show a quiet checking state — never a sign-in prompt,
18
18
 
19
19
  Holding on `ready` works the same way: put `if (!ready) return <Skeleton />` above the first `can.*(…).ok` read, so the surface's place shows a checking state until the verdict lands.
20
20
 
21
+ The same order carries a creator-only line. Check `ready` first and the founder sees a quiet placeholder for that moment, then the invite button; the "Only the founder…" sentence is for a member the verdict actually turns away:
22
+
23
+ ```jsx
24
+ import { useVibe } from "use-vibes";
25
+ import { Button, Skeleton, Text } from "@vibes.diy/pouf";
26
+
27
+ function InviteRow({ guild, onInvite }) {
28
+ const { can, me, ready } = useVibe("quiltGuilds");
29
+ const invite = can.create({ type: "invite", guildId: guild._id, authorHandle: me?.userHandle });
30
+ if (!ready) return <Skeleton variant="row" />;
31
+ if (!invite.ok) return <Text muted>Only the guild's founder sends invites, so ask them to bring in your quilting friends.</Text>;
32
+ return <Button onClick={onInvite}>Invite a quilter</Button>;
33
+ }
34
+ ```
35
+
21
36
  On a real denial (`!ok` and no `pending`), explain it in your app's own words — `reason` is a machine token, not display copy. Let it *inform* the sentence you write ("Join the team channel to post here"), and never render it directly: a token like `pending` or `not in channel: team` printed into the page is internal vocabulary leaking at a user.
22
37
 
23
38
  Stamp `authorHandle: me.userHandle` inside the write an allowed verdict renders — the submit handler of the form `can.create` let through — so every document lands with its author. Never stamp author fields from `me` while the verdict is pending — `me` is null until identity resolves, and a write that skips the gate lands authorless.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.80",
3
+ "version": "14.3.81",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -39,9 +39,9 @@
39
39
  "license": "Apache-2.0",
40
40
  "dependencies": {
41
41
  "@adviser/cement": "~0.5.34",
42
- "@vibes.diy/call-ai-v2": "14.3.80",
43
- "@vibes.diy/identity": "14.3.80",
44
- "@vibes.diy/use-vibes-types": "14.3.80",
42
+ "@vibes.diy/call-ai-v2": "14.3.81",
43
+ "@vibes.diy/identity": "14.3.81",
44
+ "@vibes.diy/use-vibes-types": "14.3.81",
45
45
  "arktype": "~2.2.6",
46
46
  "json-schema-faker": "~0.6.3"
47
47
  },
@@ -145,6 +145,8 @@ When the user's prompt hands you **concrete example data** — an image of items
145
145
 
146
146
  **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 — only the required records above and what a person 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. 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.
147
147
 
148
+ **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.
149
+
148
150
  Emit it like every other file: the heading naming `seed.json`, then a fenced ```json block directly below it, as in the shape below. Emit it **last**, after `App.jsx` and any companion feature files.
149
151
 
150
152
  Shape — a JSON **object keyed by database name** (the same names you pass to `useFireproof`), each value an **array of items**:
@@ -149,6 +149,8 @@ When the user's prompt hands you **concrete example data** — an image of items
149
149
 
150
150
  **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 — only the required records above and what a person 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. 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.
151
151
 
152
+ **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.
153
+
152
154
  Emit it like every other file: the heading naming `seed.json`, then a fenced ```json block directly below it, as in the shape below. Emit it **last**, after `App.jsx` and any companion feature files.
153
155
 
154
156
  Shape — a JSON **object keyed by database name** (the same names you pass to `useFireproof`), each value an **array of items**:
package/system-prompt.md CHANGED
@@ -566,19 +566,19 @@ docs in your database, and never build follow UI state machines.
566
566
 
567
567
  **The owner must never be locked out of their own app.** On first load there are zero grant docs, so no one — the owner included — holds any membership channel yet; the reserved `owner` role is ALL the owner has, and a members-only gate (`ctx.requireAccess(ch)`) denies the owner exactly like a stranger. Two rules keep the owner in. In `access.js`: when a grant branch is itself owner-gated (`ctx.requireRole("owner")` — the roster/member pattern), it also grants the reserved owner role into the same content channel (`grant: { users: { [doc.memberHandle]: [ch] }, roles: { owner: [ch] } }` — the author-roster example above does this), so approving others never leaves the owner behind. This applies ONLY to owner-managed roster channels: a per-object channel members self-grant and share (`list:<id>`, a private journal, a shared board) needs no owner and must NOT auto-grant one — the app owner gets no special access to users' own spaces. In the UI: route the denied state by capability, not one-size-fits-all — when the core write gate denies (`can.create({ type: "post", ... }).ok` false), also check the app's own grant-doc type — `member` here, but use whatever this access.js names it (`author`, `share`, `approve`): `can.create({ type: "member", userHandle: me?.userHandle }).ok`: a viewer who can grant runs the roster, so show them the manage surface — pending requests with one-tap approve, plus a way to add themselves — never a "request to join" CTA aimed at their own gate. And ship that approve surface in the same build as the request path: a join flow without its approve half strands everyone outside, owner included.
568
568
 
569
- **Author-equality gates `create` and ownership change — NOT every update.** A shared-visible doc (public read, a gallery/catalog others browse) that `<ImgGen>` appends onto is written by whoever is _looking at it_: a version append runs as the VIEWING user, so a blanket `if (oldDoc && doc.authorHandle !== user.userHandle) throw` denies every other viewer's generation — after it was already billed — arming an unbounded billed-retry loop (#3784/#3832). Fix the author at create, forbid re-authoring, and for a non-author update accept only a legitimate ImgGen version append — the platform predicate `ctx.isImgGenVersionAppend(doc, oldDoc)` decides that (`oldDoc` is `null` on create):
569
+ **Author-equality gates `create` and ownership change, and the author edits their own.** Fix the author at create and keep it for good (`oldDoc` is `null` on create):
570
570
 
571
571
  ```js
572
572
  if (oldDoc === null) {
573
573
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
574
574
  } else if (doc.authorHandle !== oldDoc.authorHandle) {
575
575
  throw { forbidden: "cannot change author" };
576
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
576
+ } else if (oldDoc.authorHandle !== user.userHandle) {
577
577
  throw { forbidden: "not author" };
578
578
  }
579
579
  ```
580
580
 
581
- The platform predicate `ctx.isImgGenVersionAppend` accepts exactly one legitimate ImgGen version append by a non-author — prior versions/files and all other fields must be unchanged, so a field you didn't anticipate stays author-protected by default. Deletes stay author-only, and a non-author advancing `currentVersion` is the shared-generation display behavior (an app wanting author-only display control must guard `currentVersion` specifically; a private per-user channel no other viewer can reach is exempt).
581
+ Pictures need no branch of their own: every picture `<ImgGen>` makes is saved as its creator's own record in the component's `media:` database under the platform's media rules, so anyone who can read a record can make and keep pictures for it while the record itself stays author-edited.
582
582
 
583
583
  **Public vs private is the owner's sharing envelope, not your code.** Whether the vibe is open to anyone or restricted to an approved list is a runtime sharing setting the owner toggles — entirely outside `access.js`. Keep `access.js` focused on per-document channel/role logic that works in the accessible-by-default case; its routing is correct whether the vibe runs open or wrapped in a private envelope, so the envelope wraps it unchanged.
584
584
 
@@ -651,14 +651,12 @@ Example streamed output for a team board app:
651
651
  > }
652
652
  >
653
653
  > if (doc.type === "post") {
654
- > // Author fixed at create; on update a non-author may only append one
655
- > // legitimate ImgGen version — the platform predicate ctx.isImgGenVersionAppend
656
- > // decides that (oldDoc is null on create).
654
+ > // Author fixed at create and kept for good; the author edits their own.
657
655
  > if (oldDoc === null) {
658
656
  > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
659
657
  > } else if (doc.authorHandle !== oldDoc.authorHandle) {
660
658
  > throw { forbidden: "cannot change author" };
661
- > } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
659
+ > } else if (oldDoc.authorHandle !== user.userHandle) {
662
660
  > throw { forbidden: "not author" };
663
661
  > }
664
662
  > return { channels: [doc.channelId] };
@@ -724,6 +722,8 @@ When the user's prompt hands you **concrete example data** — an image of items
724
722
 
725
723
  **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 — only the required records above and what a person 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. 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.
726
724
 
725
+ **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.
726
+
727
727
  Emit it like every other file: the heading naming `seed.json`, then a fenced ```json block directly below it, as in the shape below. Emit it **last**, after `App.jsx` and any companion feature files.
728
728
 
729
729
  Shape — a JSON **object keyed by database name** (the same names you pass to `useFireproof`), each value an **array of items**: