howone 0.2.2 → 0.2.5
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/package.json +1 -1
- package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +36 -7
- package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
- package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +37 -19
- package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +14 -5
- package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
- package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
- package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
- package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
- package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
- package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
- package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
- package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
- package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
- package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
- package/templates/vite/.howone/skills/howone/SKILL.md +110 -4
- package/templates/vite/.howone/skills/howone/references/audio-generation.md +30 -0
- package/templates/vite/.howone/skills/howone/references/audio-recognition.md +30 -0
- package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
- package/templates/vite/.howone/skills/howone/references/finance.md +28 -0
- package/templates/vite/.howone/skills/howone/references/image-editing.md +30 -0
- package/templates/vite/.howone/skills/howone/references/image-generation.md +30 -0
- package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
- package/templates/vite/.howone/skills/howone/references/video-generation.md +35 -0
- package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/src/App.tsx +9 -5
- package/templates/vite/src/lib/sdk.ts +7 -5
- package/templates/vite/bun.lock +0 -1478
|
@@ -1,509 +1,194 @@
|
|
|
1
1
|
# Manifest Codegen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate `src/lib/sdk.ts` from the two synced manifests. Do not infer names, fields, access rules,
|
|
4
|
+
schemas, or workflow IDs from a prompt, old source file, or dependency cache.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Source and evidence order
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
1. Read `.howone/database/manifest.json` and `.howone/ai/manifest.json` after the latest sync.
|
|
9
|
+
2. Read `package.json` and installed SDK declarations as described by
|
|
10
|
+
`../../references/version-evidence.md`.
|
|
11
|
+
3. Load only the operation recipe needed by the app.
|
|
12
|
+
4. Generate bindings, run the project verifier, then typecheck/build.
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
If an AI workflow update may rotate `workflowId`, re-read the AI manifest immediately before writing
|
|
15
|
+
the binding. Never preserve a stale UUID merely to avoid a diff.
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
## Database manifest mapping
|
|
15
18
|
|
|
16
|
-
For
|
|
17
|
-
the synced manifest. Do not duplicate AI schemas in app code beyond generated zod/type bindings.
|
|
18
|
-
For workflow edits, `external-ai-capability` may rotate the manifest `workflowId`; always re-read
|
|
19
|
-
`.howone/ai/manifest.json` after the tool returns before updating `src/lib/sdk.ts`.
|
|
19
|
+
For each entity, generate:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
- `Record` type extending `EntityRecord` with only declared business fields;
|
|
22
|
+
- explicit `Create` type (never `Omit<Record, ...>`);
|
|
23
|
+
- `Update = Partial<Create>` unless the product needs a narrower update contract;
|
|
24
|
+
- a `defineEntityDefinition(...)` constant that preserves `properties`, `required`, and the entire
|
|
25
|
+
`access` block;
|
|
26
|
+
- an authenticated binding wrapped with `withEntityContract`;
|
|
27
|
+
- a public binding only when `access.public` exposes at least one operation, wrapped with
|
|
28
|
+
`withPublicEntityContract`.
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
### Type mapping
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"version": "1",
|
|
30
|
-
"entities": [
|
|
31
|
-
{
|
|
32
|
-
"name": "Story",
|
|
33
|
-
"fields": [
|
|
34
|
-
{ "name": "title", "type": "string", "required": true },
|
|
35
|
-
{ "name": "content", "type": "text", "required": true },
|
|
36
|
-
{ "name": "authorId", "type": "string", "required": true },
|
|
37
|
-
{ "name": "status", "type": "string", "required": true, "enum": ["draft", "published", "archived"] },
|
|
38
|
-
{ "name": "wordCount", "type": "integer", "required": true },
|
|
39
|
-
{ "name": "tags", "type": "array", "items": "string", "required": false },
|
|
40
|
-
{ "name": "coverUrl", "type": "string", "required": false }
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"name": "Comment",
|
|
45
|
-
"fields": [
|
|
46
|
-
{ "name": "storyId", "type": "string", "required": true },
|
|
47
|
-
{ "name": "authorId", "type": "string", "required": true },
|
|
48
|
-
{ "name": "body", "type": "text", "required": true },
|
|
49
|
-
{ "name": "likes", "type": "integer", "required": false }
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Field type → TypeScript type mapping
|
|
57
|
-
|
|
58
|
-
| Manifest type | TypeScript type |
|
|
32
|
+
| Manifest field type | TypeScript/Zod |
|
|
59
33
|
|---|---|
|
|
60
|
-
| `string` | `string` |
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `object` | `Record<string, unknown>` |
|
|
69
|
-
| `enum` | `'value1' \| 'value2' \| ...` |
|
|
70
|
-
| `["string", "null"]` | `string \| null` |
|
|
71
|
-
|
|
72
|
-
- Fields in `required: true` are non-optional in `Record` types.
|
|
73
|
-
- In `Create` types, required fields are required only when they do not have `default` or `autoGenerate`.
|
|
74
|
-
- Fields with `default`, `defaultValue`, or `autoGenerate` are optional in `Create`.
|
|
75
|
-
- Nullable fields include `null`.
|
|
76
|
-
- System response fields are never generated into `Create` or `Update`.
|
|
77
|
-
|
|
78
|
-
### Access-aware generated helpers
|
|
79
|
-
|
|
80
|
-
Use the manifest `access` block to decide which namespace UI code should call:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
export type ArticlePublicQuery = {
|
|
84
|
-
published?: boolean
|
|
85
|
-
category?: string
|
|
86
|
-
slug?: string
|
|
87
|
-
page?: { number?: number; size?: number }
|
|
88
|
-
orderBy?: { publishedAt?: 'asc' | 'desc'; updatedDate?: 'asc' | 'desc' }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const publicArticles = await howone.public.entities.Article.query({
|
|
92
|
-
published: true,
|
|
93
|
-
orderBy: { publishedAt: 'desc' },
|
|
94
|
-
})
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Rules:
|
|
98
|
-
|
|
99
|
-
- `access.authenticated.*` drives `howone.entities.*`.
|
|
100
|
-
- `access.public.read = "list"` allows `howone.public.entities.Entity.query`.
|
|
101
|
-
- `access.public.read = "scoped"` requires `queryScoped` / `query.scoped` and all `requiredScopes`.
|
|
102
|
-
- Public query types must include only `allowedFilters`, `allowedSorts`, `page`, `limit`, `search`, `include`, and `exactCount`.
|
|
103
|
-
- Public create/update types should only be emitted when `access.public.create/update` is not `"none"`.
|
|
104
|
-
- Public create must include `created_by_user_id` when the schema requires public owner assignment.
|
|
105
|
-
|
|
106
|
-
### Generated TypeScript from the example manifest
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
import { type EntityRecord } from '@howone/sdk'
|
|
110
|
-
|
|
111
|
-
// ── Story ─────────────────────────────────────────────────────
|
|
112
|
-
export type StoryRecord = EntityRecord & {
|
|
113
|
-
title: string
|
|
114
|
-
content: string
|
|
115
|
-
authorId: string
|
|
116
|
-
status: 'draft' | 'published' | 'archived'
|
|
117
|
-
wordCount: number
|
|
118
|
-
tags?: string[]
|
|
119
|
-
coverUrl?: string
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export type StoryCreate = {
|
|
123
|
-
title: string
|
|
124
|
-
content: string
|
|
125
|
-
authorId: string
|
|
126
|
-
status: 'draft' | 'published' | 'archived'
|
|
127
|
-
wordCount: number
|
|
128
|
-
tags?: string[]
|
|
129
|
-
coverUrl?: string
|
|
130
|
-
}
|
|
34
|
+
| `string`, `text` | `string` / `z.string()` |
|
|
35
|
+
| `integer`, `number`, `float` | `number` / `z.number()` (`.int()` for integer) |
|
|
36
|
+
| `boolean` | `boolean` / `z.boolean()` |
|
|
37
|
+
| `date`, `datetime` | ISO `string` / `z.string()` (add `.datetime()` only when the contract guarantees it) |
|
|
38
|
+
| `array` | `T[]` / `z.array(...)` |
|
|
39
|
+
| `object` | explicit object type / `z.object(...)` |
|
|
40
|
+
| enum | literal union / `z.enum([...])` |
|
|
41
|
+
| nullable union | `T \| null` / `z.nullable(...)` |
|
|
131
42
|
|
|
132
|
-
|
|
43
|
+
Fields with `default`, `defaultValue`, or `autoGenerate` can be optional in `Create` even when
|
|
44
|
+
listed as required by the response schema. Never include system fields (`id`, timestamps, schema
|
|
45
|
+
version) or ownership fields in Create/Update types.
|
|
133
46
|
|
|
134
|
-
|
|
135
|
-
export type CommentRecord = EntityRecord & {
|
|
136
|
-
storyId: string
|
|
137
|
-
authorId: string
|
|
138
|
-
body: string
|
|
139
|
-
likes?: number
|
|
140
|
-
}
|
|
47
|
+
## AI manifest mapping
|
|
141
48
|
|
|
142
|
-
|
|
143
|
-
storyId: string
|
|
144
|
-
authorId: string
|
|
145
|
-
body: string
|
|
146
|
-
likes?: number
|
|
147
|
-
}
|
|
49
|
+
For each action:
|
|
148
50
|
|
|
149
|
-
|
|
150
|
-
|
|
51
|
+
- preserve the manifest action `id` exactly (case-sensitive);
|
|
52
|
+
- convert `inputSchema` and `outputSchema` to Zod;
|
|
53
|
+
- preserve required arrays and closed enums;
|
|
54
|
+
- include the exact UUID `workflowId` in `defineAiAction`;
|
|
55
|
+
- never add `.passthrough()` to hide an envelope mismatch;
|
|
56
|
+
- never make required output fields optional to make validation pass.
|
|
151
57
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Reading `.howone/ai/manifest.json`
|
|
155
|
-
|
|
156
|
-
### Example manifest
|
|
157
|
-
|
|
158
|
-
```json
|
|
159
|
-
{
|
|
160
|
-
"version": "1",
|
|
161
|
-
"actions": [
|
|
162
|
-
{
|
|
163
|
-
"id": "generateStory",
|
|
164
|
-
"name": "Generate Story",
|
|
165
|
-
"workflowId": "d69ab648-2c00-4d94-928e-01bd7b2a5bb2",
|
|
166
|
-
"inputSchema": {
|
|
167
|
-
"type": "object",
|
|
168
|
-
"properties": {
|
|
169
|
-
"topic": { "type": "string" },
|
|
170
|
-
"ageRange": { "type": "string", "enum": ["3-5", "6-8", "9-12"] },
|
|
171
|
-
"language": { "type": "string" }
|
|
172
|
-
},
|
|
173
|
-
"required": ["topic", "ageRange"]
|
|
174
|
-
},
|
|
175
|
-
"outputSchema": {
|
|
176
|
-
"type": "object",
|
|
177
|
-
"properties": {
|
|
178
|
-
"title": { "type": "string" },
|
|
179
|
-
"content": { "type": "string" },
|
|
180
|
-
"summary": { "type": "string" }
|
|
181
|
-
},
|
|
182
|
-
"required": ["title", "content"]
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
"id": "translateText",
|
|
187
|
-
"name": "Translate Text",
|
|
188
|
-
"workflowId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
189
|
-
"inputSchema": {
|
|
190
|
-
"type": "object",
|
|
191
|
-
"properties": {
|
|
192
|
-
"text": { "type": "string" },
|
|
193
|
-
"targetLang": { "type": "string" },
|
|
194
|
-
"formality": { "type": "string", "enum": ["formal", "informal"] }
|
|
195
|
-
},
|
|
196
|
-
"required": ["text", "targetLang"]
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### JSON Schema → zod mapping
|
|
58
|
+
`defineAiAction` validates workflow IDs during module initialization. Missing or invalid values are
|
|
59
|
+
configuration failures, not a runtime fallback case.
|
|
204
60
|
|
|
205
|
-
|
|
206
|
-
|---|---|
|
|
207
|
-
| `{ "type": "string" }` | `z.string()` |
|
|
208
|
-
| `{ "type": "number" }` | `z.number()` |
|
|
209
|
-
| `{ "type": "integer" }` | `z.number().int()` |
|
|
210
|
-
| `{ "type": "boolean" }` | `z.boolean()` |
|
|
211
|
-
| `{ "type": "string", "enum": ["a","b"] }` | `z.enum(['a', 'b'])` |
|
|
212
|
-
| `{ "type": "array", "items": { "type": "string" } }` | `z.array(z.string())` |
|
|
213
|
-
| `{ "type": "object", "properties": { ... } }` | `z.object({ ... })` |
|
|
214
|
-
| Field NOT in `required[]` | append `.optional()` |
|
|
61
|
+
## Generated shape
|
|
215
62
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
import { z } from 'zod'
|
|
220
|
-
|
|
221
|
-
// ── generateStory ─────────────────────────────────────────────
|
|
222
|
-
export const generateStoryInputSchema = z.object({
|
|
223
|
-
topic: z.string(),
|
|
224
|
-
ageRange: z.enum(['3-5', '6-8', '9-12']),
|
|
225
|
-
language: z.string().optional(),
|
|
226
|
-
})
|
|
227
|
-
export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
|
|
228
|
-
|
|
229
|
-
export const generateStoryOutputSchema = z.object({
|
|
230
|
-
title: z.string(),
|
|
231
|
-
content: z.string(),
|
|
232
|
-
summary: z.string().optional(),
|
|
233
|
-
})
|
|
234
|
-
export type GenerateStoryOutput = z.infer<typeof generateStoryOutputSchema>
|
|
235
|
-
|
|
236
|
-
// ── translateText ─────────────────────────────────────────────
|
|
237
|
-
export const translateTextInputSchema = z.object({
|
|
238
|
-
text: z.string(),
|
|
239
|
-
targetLang: z.string(),
|
|
240
|
-
formality: z.enum(['formal', 'informal']).optional(),
|
|
241
|
-
})
|
|
242
|
-
export type TranslateTextInput = z.infer<typeof translateTextInputSchema>
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Do not make required output fields optional to silence validation failures. Do not add
|
|
246
|
-
`.passthrough()` as a workaround for EAX execution envelopes. `defineAiAction` validates the
|
|
247
|
-
workflow `finalResult` payload when an `outputSchema` is configured.
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Full Generated `src/lib/sdk.ts`
|
|
252
|
-
|
|
253
|
-
Combining both manifests from the examples above:
|
|
63
|
+
This is the architecture pattern; replace every placeholder with manifest-derived code:
|
|
254
64
|
|
|
255
65
|
```ts
|
|
256
66
|
// src/lib/sdk.ts
|
|
257
|
-
// Generated from .howone/database/manifest.json and .howone/ai/manifest.json
|
|
258
67
|
import {
|
|
259
68
|
createClient,
|
|
260
69
|
defineAiAction,
|
|
261
70
|
defineAiActions,
|
|
71
|
+
defineEntityDefinition,
|
|
262
72
|
defineEntities,
|
|
263
|
-
|
|
73
|
+
definePublicEntities,
|
|
264
74
|
type EntityDefinition,
|
|
265
75
|
type EntityRecord,
|
|
266
76
|
withAiActions,
|
|
267
77
|
withEntities,
|
|
78
|
+
withEntityContract,
|
|
79
|
+
withPublicEntities,
|
|
80
|
+
withPublicEntityContract,
|
|
268
81
|
} from '@howone/sdk'
|
|
269
82
|
import { z } from 'zod'
|
|
270
83
|
|
|
271
|
-
|
|
272
|
-
// ENTITY TYPES
|
|
273
|
-
// ═══════════════════════════════════════════════════════════════
|
|
274
|
-
|
|
275
|
-
export type StoryRecord = EntityRecord & {
|
|
276
|
-
title: string
|
|
277
|
-
content: string
|
|
278
|
-
authorId: string
|
|
279
|
-
status: 'draft' | 'published' | 'archived'
|
|
280
|
-
wordCount: number
|
|
281
|
-
tags?: string[]
|
|
282
|
-
coverUrl?: string
|
|
283
|
-
}
|
|
284
|
-
export type StoryCreate = {
|
|
84
|
+
export type ArticleRecord = EntityRecord & {
|
|
285
85
|
title: string
|
|
286
|
-
|
|
287
|
-
authorId: string
|
|
288
|
-
status: 'draft' | 'published' | 'archived'
|
|
289
|
-
wordCount: number
|
|
290
|
-
tags?: string[]
|
|
291
|
-
coverUrl?: string
|
|
86
|
+
status: 'draft' | 'published'
|
|
292
87
|
}
|
|
293
|
-
export type
|
|
88
|
+
export type ArticleCreate = { title: string; status: 'draft' | 'published' }
|
|
89
|
+
export type ArticleUpdate = Partial<ArticleCreate>
|
|
294
90
|
|
|
295
|
-
export
|
|
296
|
-
|
|
297
|
-
authorId: string
|
|
298
|
-
body: string
|
|
299
|
-
likes?: number
|
|
300
|
-
}
|
|
301
|
-
export type CommentCreate = {
|
|
302
|
-
storyId: string
|
|
303
|
-
authorId: string
|
|
304
|
-
body: string
|
|
305
|
-
likes?: number
|
|
306
|
-
}
|
|
307
|
-
export type CommentUpdate = Partial<CommentCreate>
|
|
308
|
-
|
|
309
|
-
export const storyEntityDefinition = {
|
|
310
|
-
name: 'Story',
|
|
91
|
+
export const articleEntityDefinition = defineEntityDefinition({
|
|
92
|
+
name: 'Article',
|
|
311
93
|
type: 'object',
|
|
312
94
|
properties: {
|
|
313
95
|
title: { type: 'string' },
|
|
314
|
-
|
|
315
|
-
authorId: { type: 'string' },
|
|
316
|
-
status: { type: 'string', enum: ['draft', 'published', 'archived'] },
|
|
317
|
-
wordCount: { type: 'integer' },
|
|
318
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
319
|
-
coverUrl: { type: 'string' },
|
|
320
|
-
},
|
|
321
|
-
required: ['title', 'content', 'authorId', 'status', 'wordCount'],
|
|
322
|
-
access: {
|
|
323
|
-
authenticated: { read: 'own', create: 'own', update: 'own', delete: 'own' },
|
|
324
|
-
public: { read: 'none', create: 'none', update: 'none', delete: 'none' },
|
|
325
|
-
},
|
|
326
|
-
} satisfies EntityDefinition
|
|
327
|
-
|
|
328
|
-
export const commentEntityDefinition = {
|
|
329
|
-
name: 'Comment',
|
|
330
|
-
type: 'object',
|
|
331
|
-
properties: {
|
|
332
|
-
storyId: { type: 'string' },
|
|
333
|
-
authorId: { type: 'string' },
|
|
334
|
-
body: { type: 'string' },
|
|
335
|
-
likes: { type: 'integer' },
|
|
96
|
+
status: { type: 'string', enum: ['draft', 'published'] },
|
|
336
97
|
},
|
|
337
|
-
required: ['
|
|
98
|
+
required: ['title', 'status'],
|
|
338
99
|
access: {
|
|
339
|
-
authenticated: { read: '
|
|
340
|
-
public: {
|
|
100
|
+
authenticated: { read: 'all', create: 'all', update: 'all', delete: 'all' },
|
|
101
|
+
public: {
|
|
102
|
+
read: 'list',
|
|
103
|
+
create: 'none',
|
|
104
|
+
update: 'none',
|
|
105
|
+
allowedFilters: ['status'],
|
|
106
|
+
allowedSorts: ['createdDate'],
|
|
107
|
+
},
|
|
341
108
|
},
|
|
342
|
-
} satisfies EntityDefinition
|
|
343
|
-
|
|
344
|
-
// ═══════════════════════════════════════════════════════════════
|
|
345
|
-
// AI SCHEMAS & TYPES
|
|
346
|
-
// ═══════════════════════════════════════════════════════════════
|
|
347
|
-
|
|
348
|
-
export const generateStoryInputSchema = z.object({
|
|
349
|
-
topic: z.string(),
|
|
350
|
-
ageRange: z.enum(['3-5', '6-8', '9-12']),
|
|
351
|
-
language: z.string().optional(),
|
|
352
|
-
})
|
|
353
|
-
export type GenerateStoryInput = z.infer<typeof generateStoryInputSchema>
|
|
354
|
-
export const generateStoryOutputSchema = z.object({
|
|
355
|
-
title: z.string(),
|
|
356
|
-
content: z.string(),
|
|
357
|
-
summary: z.string().optional(),
|
|
358
|
-
})
|
|
359
|
-
export type GenerateStoryOutput = z.infer<typeof generateStoryOutputSchema>
|
|
360
|
-
|
|
361
|
-
export const translateTextInputSchema = z.object({
|
|
362
|
-
text: z.string(),
|
|
363
|
-
targetLang: z.string(),
|
|
364
|
-
formality: z.enum(['formal', 'informal']).optional(),
|
|
365
|
-
})
|
|
366
|
-
export type TranslateTextInput = z.infer<typeof translateTextInputSchema>
|
|
367
|
-
|
|
368
|
-
// ═══════════════════════════════════════════════════════════════
|
|
369
|
-
// CLIENT
|
|
370
|
-
// ═══════════════════════════════════════════════════════════════
|
|
109
|
+
} satisfies EntityDefinition)
|
|
371
110
|
|
|
372
111
|
const client = createClient({
|
|
373
112
|
projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
|
|
374
113
|
env: import.meta.env.VITE_HOWONE_ENV,
|
|
375
114
|
})
|
|
376
115
|
|
|
377
|
-
// ═══════════════════════════════════════════════════════════════
|
|
378
|
-
// ENTITY BINDINGS
|
|
379
|
-
// ═══════════════════════════════════════════════════════════════
|
|
380
|
-
|
|
381
116
|
export const entities = defineEntities({
|
|
382
|
-
|
|
383
|
-
|
|
117
|
+
Article: withEntityContract(
|
|
118
|
+
client.entity<ArticleRecord, ArticleCreate, ArticleUpdate>('Article'),
|
|
119
|
+
articleEntityDefinition,
|
|
120
|
+
),
|
|
384
121
|
})
|
|
385
122
|
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
123
|
+
// Include only manifest-approved public entities/capabilities.
|
|
124
|
+
export const publicEntities = definePublicEntities({
|
|
125
|
+
Article: withPublicEntityContract(
|
|
126
|
+
client.public.entity<ArticleRecord, ArticleCreate, ArticleUpdate>('Article'),
|
|
127
|
+
articleEntityDefinition,
|
|
128
|
+
),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const summarizeInputSchema = z.object({ text: z.string() })
|
|
132
|
+
const summarizeOutputSchema = z.object({ summary: z.string() })
|
|
133
|
+
export type SummarizeInput = z.infer<typeof summarizeInputSchema>
|
|
134
|
+
export type SummarizeOutput = z.infer<typeof summarizeOutputSchema>
|
|
389
135
|
|
|
390
136
|
export const ai = defineAiActions({
|
|
391
|
-
|
|
137
|
+
summarize: defineAiAction('summarize', {
|
|
392
138
|
workflowId: 'd69ab648-2c00-4d94-928e-01bd7b2a5bb2',
|
|
393
|
-
inputSchema:
|
|
394
|
-
outputSchema:
|
|
395
|
-
}),
|
|
396
|
-
translateText: defineAiAction('translateText', {
|
|
397
|
-
workflowId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
398
|
-
inputSchema: translateTextInputSchema,
|
|
139
|
+
inputSchema: summarizeInputSchema,
|
|
140
|
+
outputSchema: summarizeOutputSchema,
|
|
399
141
|
}),
|
|
400
142
|
})
|
|
401
143
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const howone = withAiActions(withEntities(client, entities), ai)
|
|
144
|
+
const howoneWithEntities = withEntities(client, entities)
|
|
145
|
+
const howoneWithPublicEntities = withPublicEntities(howoneWithEntities, publicEntities)
|
|
146
|
+
const howone = withAiActions(howoneWithPublicEntities, ai)
|
|
407
147
|
export default howone
|
|
408
148
|
```
|
|
409
149
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
Before finalising generated code, verify:
|
|
150
|
+
If the manifest has no public capability, omit `publicEntities` and `withPublicEntities`. If an
|
|
151
|
+
entity has `access.authenticated.create: 'none'`, the authenticated wrapper has no `create` or
|
|
152
|
+
`bulkCreate` property at both runtime and type level. Do not cast it back.
|
|
415
153
|
|
|
416
|
-
|
|
417
|
-
- [ ] Entity definitions are exported as `*EntityDefinition` when app code needs payload/query guards
|
|
418
|
-
- [ ] `Create` types are defined **explicitly** (not via `Omit`)
|
|
419
|
-
- [ ] Create optionality accounts for `required`, `default`, `defaultValue`, `autoGenerate`, and nullable types
|
|
420
|
-
- [ ] System fields are not present in create/update input types
|
|
421
|
-
- [ ] Public query/write types are generated only from `access.public.allowedFilters`, `allowedSorts`, `requiredScopes`, and write permissions
|
|
422
|
-
- [ ] Every AI action from `.howone/ai/manifest.json` has an `inputSchema` zod object
|
|
423
|
-
- [ ] Every AI action with manifest `outputSchema` has a matching zod `outputSchema`
|
|
424
|
-
- [ ] Every AI action binding includes the exact manifest `workflowId`
|
|
425
|
-
- [ ] Required input fields are not `.optional()` in zod
|
|
426
|
-
- [ ] Required output fields are not `.optional()` in zod
|
|
427
|
-
- [ ] AI output schemas do not use `.passthrough()` to hide execution-envelope mismatches
|
|
428
|
-
- [ ] AI action names match the manifest `id` exactly (case-sensitive)
|
|
429
|
-
- [ ] `createClient` uses `import.meta.env.*` only
|
|
430
|
-
- [ ] `withEntities` is applied before `withAiActions` in the composition chain
|
|
431
|
-
- [ ] No generated source files are placed under `.howone/`
|
|
432
|
-
- [ ] Exported types and schemas are importable from `@/lib/sdk`
|
|
154
|
+
## Query and access generation
|
|
433
155
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
When new entities or actions are added to the manifests, update `src/lib/sdk.ts` by:
|
|
439
|
-
|
|
440
|
-
1. Reading the current `src/lib/sdk.ts` to preserve existing bindings and import style.
|
|
441
|
-
2. Appending new types, schemas, entity bindings, and action bindings.
|
|
442
|
-
3. Not removing existing bindings unless the manifest explicitly removed them.
|
|
443
|
-
4. Preserving export names for backward compatibility with existing UI code.
|
|
156
|
+
Authenticated code uses `howone.entities.Entity`; public code uses `howone.public.entities.Entity`.
|
|
157
|
+
Generate `QueryOptions` with `where`, `page`, `orderBy`, `include`, and `exactCount` only. Public
|
|
158
|
+
filters and sorts must be limited to manifest `allowedFilters`/`allowedSorts`; scoped reads must
|
|
159
|
+
provide every `requiredScopes` field under `where`.
|
|
444
160
|
|
|
445
161
|
```ts
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// After (added Comment entity from new manifest)
|
|
452
|
-
export const entities = defineEntities({
|
|
453
|
-
Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
|
|
454
|
-
Comment: client.entity<CommentRecord, CommentCreate, CommentUpdate>('Comment'),
|
|
162
|
+
await howone.public.entities.Article.query({
|
|
163
|
+
where: { status: 'published' },
|
|
164
|
+
orderBy: { createdDate: 'desc' },
|
|
165
|
+
page: { number: 1, size: 20 },
|
|
455
166
|
})
|
|
456
167
|
```
|
|
457
168
|
|
|
458
|
-
|
|
169
|
+
Never put `created_by_id`, `ownerId`, `puid`, or system fields into authenticated or public payloads.
|
|
170
|
+
Ownership comes from auth or the public scope contract.
|
|
459
171
|
|
|
460
|
-
##
|
|
172
|
+
## Incremental changes
|
|
461
173
|
|
|
462
|
-
When
|
|
174
|
+
When a manifest changes:
|
|
463
175
|
|
|
464
|
-
1.
|
|
465
|
-
2.
|
|
466
|
-
3.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
`outputSchema`.
|
|
470
|
-
5. Generate entity types and AI action bindings from synced manifests.
|
|
471
|
-
6. Use `runAiActionAndPersist()` for history-style products.
|
|
176
|
+
1. re-read the complete current manifest;
|
|
177
|
+
2. preserve app exports that are still present;
|
|
178
|
+
3. add/remove bindings to match the manifest exactly;
|
|
179
|
+
4. regenerate affected Zod and entity definitions;
|
|
180
|
+
5. run the verifier and typecheck.
|
|
472
181
|
|
|
473
|
-
|
|
474
|
-
// AI generates: { title: string, content: string, summary: string }
|
|
475
|
-
// Save it to Story entity which adds: authorId, status, wordCount
|
|
476
|
-
|
|
477
|
-
async function generateAndSave(input: GenerateStoryInput, authorId: string) {
|
|
478
|
-
const output = await howone.ai.generateStory.run(input)
|
|
479
|
-
|
|
480
|
-
return howone.entities.Story.create({
|
|
481
|
-
title: output.title,
|
|
482
|
-
content: output.content,
|
|
483
|
-
authorId,
|
|
484
|
-
status: 'draft',
|
|
485
|
-
wordCount: output.content.split(' ').length,
|
|
486
|
-
})
|
|
487
|
-
}
|
|
488
|
-
```
|
|
182
|
+
Do not hand-edit `.howone/*` metadata. Do not place generated source files under `.howone/`.
|
|
489
183
|
|
|
490
|
-
|
|
184
|
+
## Codegen checklist
|
|
491
185
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
run: (input) => howone.ai.generateStory.run(input),
|
|
502
|
-
mapCompleted: ({ output }) => ({
|
|
503
|
-
status: 'completed',
|
|
504
|
-
title: output.title,
|
|
505
|
-
content: output.content,
|
|
506
|
-
completedAt: new Date().toISOString(),
|
|
507
|
-
}),
|
|
508
|
-
})
|
|
509
|
-
```
|
|
186
|
+
- [ ] Every current entity has Record/Create/Update types and a definition constant.
|
|
187
|
+
- [ ] System/ownership fields are absent from write types.
|
|
188
|
+
- [ ] Access wrappers match authenticated/public operations.
|
|
189
|
+
- [ ] Public bindings include only manifest-approved entities.
|
|
190
|
+
- [ ] Query fields are typed from the generated Record, not `Record<string, unknown>`.
|
|
191
|
+
- [ ] Every AI action has a real Zod input schema and exact manifest UUID.
|
|
192
|
+
- [ ] Output requiredness and enums match the manifest.
|
|
193
|
+
- [ ] Provider consumes the composed `howone` client.
|
|
194
|
+
- [ ] `node .howone/skills/howone/scripts/verify-project.mjs` passes.
|