@vibes.diy/prompts 14.1.23 → 14.1.25

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
@@ -1,8 +1,10 @@
1
1
  # Access Control (`access.js`) — permission design
2
2
 
3
- You are seeing this doc because the app's prompt is **permission-shaped** — it asks about privacy, sharing, teams, members, roles, DMs, approval, or who-can-see-what. Use it to design the app's `access.js`.
3
+ This doc is in view because the app is getting its `access.js`: either the prompt names sharing, privacy, teams, members, roles, approval or who-can-see-what, or the platform is running the second pass every app gets — a first build drafts the records with their keys already in place (`creatorHandle` on the object, the parent id on each child, `authorHandle` on every record) and leaves the rules and the invite screen to this pass. Every app gets record rules unless the person asked for none in their own words, and the group shape taught below is the resting default.
4
4
 
5
- **Honest default first:** all app data is shared and world-readable by default; the runtime, not your code, decides access. Add an `access.js` only when the app genuinely needs per-document write validation or channel-based read isolation. Never write UI copy that promises privacy the access model doesn't enforce — describe what the access rules actually do.
5
+ **On a second pass over an app that already exists, this is a whole-app upgrade, applied in full.** Read `App.jsx` for the object type the app is about, the field that names its creator and the parent id its children carry; name the channels from those records; write the complete `access.js`; then change `App.jsx` to match it — the invite screen, the `can` gates, the object id on every child — keeping everything the app already does.
6
+
7
+ **Honest copy:** until `access.js` says otherwise, all app data is shared and world-readable, and the runtime, not your UI, decides access — `access.js` is where per-document write validation and channel-based read isolation live. Write UI copy that describes what the rules actually do.
6
8
 
7
9
  `access.js` is a separate file alongside `App.jsx`; each **named export** gates the database of the same name (`export function chat(...)` gates `useFireproof("chat")`), and an `export default` acts as a catch-all. The `App.jsx` you write gates its write surfaces on `useVibe(dbName).can` — the same function this access.js enforces server-side.
8
10
 
@@ -10,8 +12,9 @@ Construct the document an action will save, check that same value with `can.crea
10
12
  `can.edit`, then write it to the same database. Include every field the access rule checks,
11
13
  including the intended next status. For example, cancelling a reservation constructs
12
14
  `const next = { ...reservation, status: "cancelled" }`, checks `can.edit(next).ok`, and
13
- saves `next`. A creation control checks the complete creation draft. Render the verdict's
14
- reason when denied, and keep loading and signed-out states distinct from denial.
15
+ saves `next`. A creation control checks the complete creation draft. On a denial render a
16
+ sentence in the app's own words — `reason` is a machine token that informs that sentence —
17
+ and keep loading and signed-out states distinct from denial.
15
18
 
16
19
  ## Reference
17
20
 
@@ -108,7 +111,7 @@ The platform predicate `ctx.isImgGenVersionAppend(doc, oldDoc)` accepts exactly
108
111
 
109
112
  ### `_id` strategy matters
110
113
 
111
- Documents that represent a unique named resource (channels, user profiles, config singletons, starter content) should use a deterministic `_id` with a short prefix — `"ch:" + name`, `"profile:" + handle`, `"config"`, `"starter:" + key`. This enforces uniqueness: two users creating "general" get the same doc, not two — and a first-load write that runs again (a fresh tab, another device) overwrites the same starter docs instead of duplicating them. Records a request says the app must already have are created this way, by the app's first render; see "Required starting records" in `fireproof.md`. Documents that represent events or content (messages, posts, survey responses) should let `_id` be auto-generated — each one is unique by nature. Use `doc._id` as the channel name for resource docs; use a `channelId` foreign key on content docs.
114
+ Documents that represent a unique named resource (channels, user profiles, config singletons, starter content) should use a deterministic `_id` with a short prefix — `"ch:" + name`, `"profile:" + handle`, `"config"`, `"starter:" + key`. This enforces uniqueness: two users creating "general" get the same doc, not two — and a first-load write that runs again (a fresh tab, another device) overwrites the same starter docs instead of duplicating them. Records a request says the app must already have are created this way, by the app's first render; see "Required starting records" in `fireproof.md`. Documents that represent events or content (messages, posts, survey responses) should let `_id` be auto-generated — each one is unique by nature. A resource doc's channel is computed from the record itself — its own prefixed `_id`, or `board:${doc._id}` — and a content doc carries that object's id as a foreign key and routes to the same computed name.
112
115
 
113
116
  ### Grants are additive
114
117
 
@@ -124,7 +127,7 @@ If `user` is `null` and the function returns without throwing, the runtime check
124
127
 
125
128
  A channel is a _reusable_ unit of read access: grant a user into a channel once and they can read every document routed there. Reach for the smallest number of channels the sharing actually requires.
126
129
 
127
- - **A reusable group reads many docs** (a team, a board, a project): route to one channel the _collaboration_ owns — `return { channels: [doc.channelId] }` — and grant membership once via a meta or invite doc. Many documents share the one channel.
130
+ - **A reusable group reads many docs** (a team, a board, a project): route to one channel the _collaboration_ owns — `return { channels: [doc.channelId] }` — and grant membership once via a meta or invite doc. Many documents share the one channel. That channel's name is computed from the record the collaboration lives on — `board:${doc._id}` on the object itself and `board:${doc.boardId}` on its children — and the branch that creates the object grants its creator that channel in the same `return`, so the person who makes a thing is already inside it and their very first write lands. An invite doc for that collaboration grants the picked handle that same computed channel, so being let into one object is being let into that one object, and the next group to open the app gets a room of its own.
128
131
  - **Only the author reads it** (private notes, a user's own uploads): route to one channel the _user_ owns. `const mine = \`user:${user.userHandle}\`; return { channels: [mine], grant: { users: { [user.userHandle]: [mine] } } };` — all of that user's private documents live in this single channel.
129
132
  - **A document goes to a one-off set with no reusable group:** route to several channels at once — `return { channels: [\`user:${aHandle}\`, \`user:${bHandle}\`] }`. Mint a per-document channel (`channels: [doc._id]`) only when each document genuinely has its own disjoint audience.
130
133
  - **Refusing a write:** `throw { forbidden: "reason" }`. Every document you store is routed to at least one channel so it can be read back.
@@ -370,7 +373,7 @@ export function habits(doc, oldDoc, user, ctx) {
370
373
 
371
374
  The leaderboard is just the access model: read the public `track:` channels and sum them; each viewer additionally sees their own streaks and any buddy who granted them in. A habit tracker whose owner **asked to keep it to themselves** is instead the per-visitor shape shown above: every visitor's habits and check-ins on their single `user:${user.userHandle}` channel, self-granted and private to them.
372
375
 
373
- **Per-object collaboration is the resting shape of almost every app** (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 (`board:<id>`/`list:<id>`); 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`.
376
+ **Per-object collaboration is the resting shape of almost every app** (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`.
374
377
 
375
378
  **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`.
376
379
 
@@ -1,6 +1,6 @@
1
1
  # CallAI Helper Function
2
2
 
3
- The `callAI` function asks an AI model and returns a string. With a `schema` the string is structured JSON you `JSON.parse()`; without one it is ordinary text — a chat reply, a caption, a paragraph — used as is. Reach for a schema when the app needs fields, and leave it out when it needs prose.
3
+ The `callAI` function asks an AI model and returns a string. Every call carries a `schema`, and the string that comes back is JSON you `JSON.parse()`. When the app needs prose — a chat reply, a caption, a paragraph — the schema is a single string field and that field holds the prose: `schema: { properties: { reply: { type: "string" } } }`, read back as `JSON.parse(response).reply`.
4
4
 
5
5
  ## Basic Usage
6
6
 
package/llms/callai.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CallAI Helper Function
2
2
 
3
- The `callAI` function returns structured JSON from an AI model. It always requires a schema and returns a string that you `JSON.parse()`.
3
+ The `callAI` function asks an AI model and returns a string. Every call carries a `schema`, and the string that comes back is JSON you `JSON.parse()`. When the app needs prose — a chat reply, a caption, a paragraph — the schema is a single string field and that field holds the prose: `schema: { properties: { reply: { type: "string" } } }`, read back as `JSON.parse(response).reply`.
4
4
 
5
5
  ## Basic Usage
6
6
 
@@ -90,8 +90,10 @@ App.jsx
90
90
  <button type="submit">Post</button>
91
91
  </form>
92
92
  ) : (
93
- // Your own words, informed by v.reason — never the token itself.
94
- <p>Sign in to add a comment.</p>
93
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
94
+ // outside this thread's membership, so the sentence says that; the sign-in
95
+ // ask is for the anonymous viewer.
96
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
95
97
  );
96
98
  })()}
97
99
  >>>>>>> REPLACE
@@ -143,8 +145,10 @@ import { useViewer, useVibe } from "use-vibes";
143
145
  <button type="submit">Post</button>
144
146
  </form>
145
147
  ) : (
146
- // Your own words, informed by v.reason — never the token itself.
147
- <p>Sign in to add a comment.</p>
148
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
149
+ // outside this thread's membership, so the sentence says that; the sign-in
150
+ // ask is for the anonymous viewer.
151
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
148
152
  );
149
153
  })()}
150
154
  =======
@@ -167,8 +171,10 @@ import { useViewer, useVibe } from "use-vibes";
167
171
  <button type="submit">Post</button>
168
172
  </form>
169
173
  ) : (
170
- // Your own words, informed by v.reason — never the token itself.
171
- <p>Sign in to add a comment.</p>
174
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
175
+ // outside this thread's membership, so the sentence says that; the sign-in
176
+ // ask is for the anonymous viewer.
177
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
172
178
  );
173
179
  })()}
174
180
  >>>>>>> REPLACE
@@ -49,7 +49,7 @@ export default function App() {
49
49
 
50
50
  ## Gating UI
51
51
 
52
- Gate the comment form on `useVibe("comments").can` — not on `viewer`. The current viewer never needs a pill here (that's in the Vibes Switch); just render the gated form and its `reason` when denied:
52
+ Gate the comment form on `useVibe("comments").can` — not on `viewer`. The current viewer never needs a pill here (that's in the Vibes Switch); just render the gated form, and on a denial a sentence in your app's own words — `reason` is a machine token that informs that sentence and is never printed:
53
53
 
54
54
  App.jsx
55
55
 
@@ -91,7 +91,10 @@ App.jsx
91
91
  <button type="submit">Post</button>
92
92
  </form>
93
93
  ) : (
94
- <p>{v.reason}</p>
94
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
95
+ // outside this thread's membership, so the sentence says that; the sign-in
96
+ // ask is for the anonymous viewer.
97
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
95
98
  );
96
99
  })()}
97
100
  >>>>>>> REPLACE
@@ -137,7 +140,10 @@ import { useViewer, useVibe } from "use-vibes";
137
140
  <button type="submit">Post</button>
138
141
  </form>
139
142
  ) : (
140
- <p>{v.reason}</p>
143
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
144
+ // outside this thread's membership, so the sentence says that; the sign-in
145
+ // ask is for the anonymous viewer.
146
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
141
147
  );
142
148
  })()}
143
149
  =======
@@ -160,7 +166,10 @@ import { useViewer, useVibe } from "use-vibes";
160
166
  <button type="submit">Post</button>
161
167
  </form>
162
168
  ) : (
163
- <p>{v.reason}</p>
169
+ // Your own words, informed by v.reason. A signed-in viewer who is denied is
170
+ // outside this thread's membership, so the sentence says that; the sign-in
171
+ // ask is for the anonymous viewer.
172
+ <p>{me?.userHandle ? "Comments here are for members of this thread." : "Sign in to add a comment."}</p>
164
173
  );
165
174
  })()}
166
175
  >>>>>>> REPLACE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.1.23",
3
+ "version": "14.1.25",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -34,9 +34,9 @@
34
34
  "license": "Apache-2.0",
35
35
  "dependencies": {
36
36
  "@adviser/cement": "~0.5.34",
37
- "@vibes.diy/call-ai-v2": "14.1.23",
38
- "@vibes.diy/identity": "14.1.23",
39
- "@vibes.diy/use-vibes-types": "14.1.23",
37
+ "@vibes.diy/call-ai-v2": "14.1.25",
38
+ "@vibes.diy/identity": "14.1.25",
39
+ "@vibes.diy/use-vibes-types": "14.1.25",
40
40
  "arktype": "~2.2.3",
41
41
  "json-schema-faker": "~0.6.3"
42
42
  },
package/system-prompt.md CHANGED
@@ -91,7 +91,7 @@ The sandbox serves raw ES modules, so `App.jsx` can import local `.js`/`.jsx` fi
91
91
  - **Load Google Fonts with `&display=swap` (or `&display=optional`), never `&display=block`.** Append it to the Fonts URL so text paints immediately in a fallback instead of staying invisible for seconds on slow connections (flash of invisible text) — e.g. `https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap`.
92
92
  - **The bottom-right corner belongs to the platform — never pin your own control there.** The Vibes Switch (the logo) floats over every app in that corner, so a `fixed` element anchored to both `bottom` and `right` lands underneath it: no floating add/compose button, no chat bubble, no scroll-to-top disc in that spot. Anchor a floating action bottom-left or bottom-center instead, or fold it into the layout — a header button, or a full-width sticky bar (the platform already reserves scroll clearance below your app for one).
93
93
 
94
- **If the app needs an `access.js`** — privacy, sharing, teams, roles, or approval — the access skill doc (included on permission-shaped turns) carries the emit format, placement, and worked examples; emit `access.js` before any `App.jsx` edit that writes a doc type it gates. Gate every write surface on `useVibe(dbName).can` regardless of whether the app has an `access.js`.
94
+ **Every app gets an `access.js`** unless the person asked for none in their own words, and the access skill doc carries the emit format, placement, and worked examples whenever a turn writes one. When the instruction is the platform's second pass over an app drafted without one, that is a whole-app upgrade applied in full: read `App.jsx` for the records it already writes, emit the complete `access.js` first, then the `App.jsx` edits it needs — the invite screen, the `can` gates, the object id on every child. On any turn, emit `access.js` before any `App.jsx` edit that writes a doc type it gates. Gate every write surface on `useVibe(dbName).can` regardless of whether the app has an `access.js` yet.
95
95
 
96
96
  **Keep `access.js` in step with the data model.** The app's current `access.js` and `seed.json` are always in view (the `APP_STATE` block). When an edit adds a new written doc `type` — or a field the rules key on — update `access.js` in the same reply so the new writes are allowed; an existing terminal branch that rejects unknown types will reject them at runtime.
97
97
 
@@ -302,9 +302,9 @@ Below is a tiny worked example showing the format end-to-end. Description → sc
302
302
 
303
303
  Note how each edit is preceded by exactly one prose line, the visible structure (input + button) lands before the data wiring (`useDocument` / state), and each SEARCH block is the smallest unique snippet that targets the change.
304
304
 
305
- ### access.js output format (when needed)
305
+ ### access.js output format
306
306
 
307
- When the app needs channel-based read isolation or per-document write validation, the **access skill doc** (included on permission-shaped turns) carries the full teaching: emit format and placement (one prose line, `access.js`, then one complete fenced block — for a fresh app right after the shell, on a follow-up turn **before** any `App.jsx` edit that writes a doc type it gates), the new-doc-type-first ordering rule, and the worked examples. Never put access function code inside an `App.jsx` block — the filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
307
+ When a turn writes `access.js`, the **access skill doc** carries the full teaching: emit format and placement (one prose line, `access.js`, then one complete fenced block — for a fresh app right after the shell, on a follow-up turn **before** any `App.jsx` edit that writes a doc type it gates), the new-doc-type-first ordering rule, and the worked examples. Never put access function code inside an `App.jsx` block — the filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
308
308
 
309
309
  ## Your starter scaffold
310
310
 
@@ -364,7 +364,7 @@ export default function App() {
364
364
 
365
365
  Don't put a current-user `<ViewerTag />` or login button in the header — the logo's Vibes Switch already shows who's signed in and offers sign-in. Reach for `useViewer` (`const { ViewerTag } = useViewer();`) only where you render **other** users (`<ViewerTag userHandle={...} />`) — either in that feature component, or hoisted to `App` and passed down as a prop. Add `useVibe(dbName)` in the components that gate writes — `can`/`ready` are read where the write surface lives, not hoisted to `App`.
366
366
 
367
- **If the app needs an `access.js`** — privacy, sharing, teams, roles, or approval — the access skill doc (included on permission-shaped turns) carries the permission-model design, worked examples, and the emit format. Emit it right after the scaffold, before any feature edits, and emit the follow-up `access.js` block with any new doc type's branch **before** the edits that write it. Gate every write surface on `useVibe(dbName).can` — the liveness gate is universal even when the app has no `access.js`.
367
+ **When this turn writes `access.js`**, the access skill doc carries the permission-model design, worked examples, and the emit format. Emit it right after the scaffold, before any feature edits, and emit the follow-up `access.js` block with any new doc type's branch **before** the edits that write it. Gate every write surface on `useVibe(dbName).can` — the liveness gate is universal even when the app has no `access.js`.
368
368
  ## Social: followers see your stuff (platform graph)
369
369
 
370
370
  The follow graph lives in the PLATFORM (Settings → Social) — never store friend/follow