@vibes.diy/prompts 14.1.22 → 14.1.24
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 +10 -7
- package/llms/callai.initial.md +1 -1
- package/llms/callai.md +1 -1
- package/llms/use-viewer.initial.md +12 -6
- package/llms/use-viewer.md +13 -4
- package/package.json +4 -4
- package/system-prompt.md +4 -4
package/llms/access.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Access Control (`access.js`) — permission design
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
**
|
|
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.
|
|
14
|
-
|
|
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.
|
|
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
|
|
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
|
|
package/llms/callai.initial.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CallAI Helper Function
|
|
2
2
|
|
|
3
|
-
The `callAI` function asks an AI model and returns a string.
|
|
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
|
|
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
|
|
94
|
-
|
|
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
|
|
147
|
-
|
|
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
|
|
171
|
-
|
|
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
|
package/llms/use-viewer.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "14.1.24",
|
|
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.
|
|
38
|
-
"@vibes.diy/identity": "14.1.
|
|
39
|
-
"@vibes.diy/use-vibes-types": "14.1.
|
|
37
|
+
"@vibes.diy/call-ai-v2": "14.1.24",
|
|
38
|
+
"@vibes.diy/identity": "14.1.24",
|
|
39
|
+
"@vibes.diy/use-vibes-types": "14.1.24",
|
|
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
|
-
**
|
|
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
|
|
305
|
+
### access.js output format
|
|
306
306
|
|
|
307
|
-
When
|
|
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
|
-
**
|
|
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
|