@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 +15 -25
- package/llms/calendar.js +19 -2
- package/llms/calendar.js.map +1 -1
- package/llms/calendar.md +119 -1
- package/llms/image-gen.md +2 -23
- package/llms/use-vibe.md +15 -0
- package/package.json +4 -4
- package/system-prompt-initial-oneshot.md +2 -0
- package/system-prompt-initial.md +2 -0
- package/system-prompt.md +7 -7
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.
|
|
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
|
-
###
|
|
178
|
+
### Pictures on shared records
|
|
179
179
|
|
|
180
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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):
|
|
5
|
-
cues: [
|
|
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
|
package/llms/calendar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../jsr/llms/calendar.ts"],"names":[],"mappings":"
|
|
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
|
-
#
|
|
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
|
|
215
|
+
## Access Functions and pictures
|
|
216
216
|
|
|
217
|
-
|
|
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.
|
|
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.
|
|
43
|
-
"@vibes.diy/identity": "14.3.
|
|
44
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
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**:
|
package/system-prompt-initial.md
CHANGED
|
@@ -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
|
|
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 (
|
|
576
|
+
} else if (oldDoc.authorHandle !== user.userHandle) {
|
|
577
577
|
throw { forbidden: "not author" };
|
|
578
578
|
}
|
|
579
579
|
```
|
|
580
580
|
|
|
581
|
-
|
|
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
|
|
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 (
|
|
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**:
|