@metaobjectsdev/sdk 0.24.4 → 0.25.0

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.
Files changed (59) hide show
  1. package/README.md +1 -7
  2. package/agent-context/servers/csharp.meta.json +3 -1
  3. package/agent-context/servers/java.meta.json +3 -1
  4. package/agent-context/servers/kotlin.meta.json +3 -1
  5. package/agent-context/servers/python.meta.json +3 -1
  6. package/agent-context/servers/typescript.meta.json +3 -1
  7. package/agent-context/skills/metaobjects-audit/SKILL.md +15 -8
  8. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +6 -1
  9. package/agent-context/skills/metaobjects-audit/references/csharp.md +10 -1
  10. package/agent-context/skills/metaobjects-audit/references/java.md +10 -0
  11. package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -0
  12. package/agent-context/skills/metaobjects-audit/references/python.md +12 -2
  13. package/agent-context/skills/metaobjects-audit/references/typescript.md +7 -2
  14. package/agent-context/skills/metaobjects-authoring/SKILL.md +35 -6
  15. package/agent-context/skills/metaobjects-codegen/SKILL.md +249 -10
  16. package/agent-context/skills/metaobjects-codegen/references/csharp.md +1 -0
  17. package/agent-context/skills/metaobjects-codegen/references/java.md +1 -0
  18. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +1 -0
  19. package/agent-context/skills/metaobjects-codegen/references/python.md +1 -0
  20. package/agent-context/skills/metaobjects-codegen/references/typescript.md +190 -6
  21. package/agent-context/skills/metaobjects-prompts/references/typescript.md +10 -1
  22. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +22 -0
  23. package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +15 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/java.md +33 -0
  25. package/agent-context/skills/metaobjects-runtime-ui/references/kotlin.md +23 -0
  26. package/agent-context/skills/metaobjects-runtime-ui/references/python.md +25 -0
  27. package/agent-context/skills/metaobjects-runtime-ui/references/tanstack.md +34 -7
  28. package/agent-context/skills/metaobjects-runtime-ui/references/typescript.md +20 -0
  29. package/agent-context/skills/metaobjects-verify/SKILL.md +28 -0
  30. package/agent-context/templates/always-on.md.mustache +45 -4
  31. package/dist/agent-context/assemble.d.ts.map +1 -1
  32. package/dist/agent-context/assemble.js +52 -4
  33. package/dist/agent-context/assemble.js.map +1 -1
  34. package/dist/agent-context/scaffold.d.ts +16 -1
  35. package/dist/agent-context/scaffold.d.ts.map +1 -1
  36. package/dist/agent-context/scaffold.js +81 -4
  37. package/dist/agent-context/scaffold.js.map +1 -1
  38. package/dist/forge-types.d.ts.map +1 -1
  39. package/dist/forge-types.js +19 -1
  40. package/dist/forge-types.js.map +1 -1
  41. package/package.json +4 -9
  42. package/src/agent-context/assemble.ts +86 -6
  43. package/src/agent-context/scaffold.ts +91 -5
  44. package/src/forge-types.ts +21 -0
  45. package/dist/agent-docs/body.d.ts +0 -6
  46. package/dist/agent-docs/body.d.ts.map +0 -1
  47. package/dist/agent-docs/body.js +0 -593
  48. package/dist/agent-docs/body.js.map +0 -1
  49. package/dist/agent-docs/content-hash.d.ts +0 -8
  50. package/dist/agent-docs/content-hash.d.ts.map +0 -1
  51. package/dist/agent-docs/content-hash.js +0 -23
  52. package/dist/agent-docs/content-hash.js.map +0 -1
  53. package/dist/agent-docs/index.d.ts +0 -3
  54. package/dist/agent-docs/index.d.ts.map +0 -1
  55. package/dist/agent-docs/index.js +0 -4
  56. package/dist/agent-docs/index.js.map +0 -1
  57. package/src/agent-docs/body.ts +0 -592
  58. package/src/agent-docs/content-hash.ts +0 -25
  59. package/src/agent-docs/index.ts +0 -8
@@ -1,593 +0,0 @@
1
- // Agent reference docs body. Scaffolded into .metaobjects/AGENTS.md and CLAUDE.md by `meta init`.
2
- /**
3
- * @deprecated The single-blob agent doc is replaced by the assembled agent-context
4
- * (see `@metaobjectsdev/sdk/agent-context`). Kept only for back-compat; not scaffolded by `meta init`.
5
- */
6
- export const AGENT_DOCS_BODY = `# Meta Forge — agent reference
7
-
8
- This file is scaffolded by \`meta init\` and lives alongside your \`metaobjects/\` records. It teaches AI coding assistants (Claude Code, Codex, etc.) how to read and modify MetaObjects metadata correctly. Refresh after CLI updates with \`meta init --refresh-docs\`.
9
-
10
- ## Five working principles (read first)
11
-
12
- These shape every interaction with a metaobjects-driven project. Follow them when you author metadata, write hand-coded business logic, or review someone else's work.
13
-
14
- ### 1. If it's pattern-derivable from metadata, generate it. Never hand-write boilerplate.
15
-
16
- The metaobjects raison d'être is that anything the metadata fully describes — schemas, FK references, basic CRUD, query helpers, Zod validators, route handlers, RHF rules, form fields — should be produced by codegen, not hand-typed. If you find yourself hand-writing something the metadata already knows about, stop and use the generated artifact.
17
-
18
- The first version of the trainer website's database layer had hand-written Drizzle schemas, Zod schemas, and CRUD endpoints. Every one of those is now generated. The hand-written code that remains is real business logic (Stripe webhooks, Loops integration, custom auth flows) — things the generator genuinely cannot derive.
19
-
20
- When you're about to add a new field or entity: edit \`metaobjects/*.json\` and re-run \`meta gen\`. Don't reach for the generated file directly.
21
-
22
- ### 2. Use the generated constants. Never use magic strings that touch metadata.
23
-
24
- After \`meta gen\`, each entity file exports a rich metadata-constants block. Each non-dollar-prefixed key is a per-field object that carries everything a consumer might need (name, label, view, html input type, placeholder, RHF validation rules):
25
-
26
- \`\`\`ts
27
- export const Subscriber = {
28
- $entity: "Subscriber", // entity name string
29
- $table: "subscribers", // SQL table name
30
- $path: "/subscribers", // REST resource path
31
-
32
- email: {
33
- name: "email", // field name string (use for filters, register())
34
- label: "Email Address", // humanized fallback or @label override
35
- view: "text", // MetaView subtype
36
- htmlType: "email", // optional; maps view → HTML <input type=>
37
- placeholder: "you@example.com", // optional; only when @placeholder is set on the view
38
- helpText: "We never share this.", // optional; only when @helpText is set
39
- rules: { // optional; derived from validator children
40
- required: "Email is required",
41
- maxLength: { value: 255, message: "Too long" },
42
- pattern: { value: /.../, message: "Invalid email" },
43
- },
44
- },
45
- firstName: { name: "firstName", label: "First Name", view: "text", htmlType: "text", rules: { required: "First Name is required" } },
46
- // ...
47
- } as const;
48
- \`\`\`
49
-
50
- **Use them everywhere — in both generated AND hand-written code:**
51
-
52
- \`\`\`tsx
53
- // ✗ Don't:
54
- <input name="email" type="email" placeholder="Email" />
55
-
56
- // ✓ Do:
57
- <input
58
- type={Subscriber.email.htmlType}
59
- name={Subscriber.email.name}
60
- placeholder={Subscriber.email.placeholder}
61
- aria-label={Subscriber.email.label}
62
- />
63
- \`\`\`
64
-
65
- Rename a field in \`metaobjects/\` and re-gen — TypeScript catches every stale reference.
66
-
67
- **Special case — Drizzle column access:** when you're already inside Drizzle's typed builder, just use the column properties directly. Drizzle's table-const types are themselves derived from metadata, so \`weeks.programId\` is already TS-safe:
68
-
69
- \`\`\`ts
70
- // ✓ Use Drizzle's typed accessor directly — no constants needed here:
71
- db.select().from(weeks).where(eq(weeks.programId, X))
72
-
73
- // ✗ Don't do this — it's redundant indirection:
74
- db.select().from(weeks).where(eq(weeks[Week.programId.name], X))
75
- \`\`\`
76
-
77
- Use the constants when you need a STRING (filter object keys, registration arguments, REST paths, labels). Use Drizzle properties directly when the type system already does the work.
78
-
79
- ### 3. Forms: spread \`form.input.<field>\` from useEntityForm. One line per input.
80
-
81
- For React forms, use \`useEntityForm\` from \`@metaobjectsdev/react\`. It returns the standard React Hook Form surface plus a pre-bound \`.input\` accessor — one entry per field, ready to spread onto an \`<input>\`:
82
-
83
- \`\`\`tsx
84
- import { useEntityForm } from '@metaobjectsdev/react';
85
- import { Subscriber, SubscriberInsertSchema } from './generated/Subscriber';
86
-
87
- const form = useEntityForm(Subscriber, SubscriberInsertSchema);
88
-
89
- <label>{Subscriber.email.label}</label>
90
- <input {...form.input.email} /> // ← type, placeholder, name, rules, aria-label all spread automatically
91
- \`\`\`
92
-
93
- For non-\`<input>\` controls (textarea, select), the \`type\` attr is omitted from \`form.input.X\` — pick the right element yourself.
94
-
95
- The same Zod schema (\`SubscriberInsertSchema\`) validates on the server (in Fastify routes) and on the client (via the resolver). One schema, two surfaces, zero drift.
96
-
97
- ### 4. Routes: use the generated \`<Entity>.routes.ts\` for stock CRUD. Hand-write only what's custom.
98
-
99
- \`meta gen\` emits a per-entity routes file that mounts the 5 standard verbs via \`mountCrudRoutes\` from \`@metaobjectsdev/runtime-ts/drizzle-fastify\`. The runtime is plain Drizzle + Zod — no extra ORM.
100
-
101
- For custom flows (Stripe webhooks, side effects, auth-gated actions), hand-write the route — but import the entity constants + generated Zod schema. The boilerplate (CRUD, validation, 404 mapping, pagination) lives in the helper; your hand-written code is just the business logic.
102
-
103
- **Auth pattern:** install a plugin-level Fastify \`preHandler\` hook at the top of your route plugin. The hook applies to every route registered after it — both hand-written handlers AND metaobjects-generated routes via the \`routeOptions\` field. Beats sprinkling \`if (!auth(...)) return;\` at the top of every handler.
104
-
105
- ### 5. Hand-coded code is always available, but coexists with generated code.
106
-
107
- Generated code does the boilerplate. Hand-coded code does the business logic. They live in the same project, the same package, sometimes the same file. The hand-coded code consumes the generated constants and generated Zod schemas — it never duplicates schema, never hard-codes paths, never declares its own validators that metadata could declare.
108
-
109
- Concrete pattern from the trainer website:
110
- - Generated \`Subscriber.routes.ts\` registers GET / GET-by-id / POST / PATCH / DELETE on \`/api/subscribers\`.
111
- - Hand-written \`apps/api/src/routes/subscribers.ts\` keeps \`POST /subscribe\` — the custom endpoint with the Loops side-effect.
112
- - Both registered with \`fastify.register()\`. Both validate via \`SubscriberInsertSchema\`. Both use \`Subscriber.email.name\` / etc. Neither knows the other exists.
113
-
114
- ## Metaobjects metamodel — quick rules
115
-
116
- The format used by \`metaobjects/*.json\` is **metaobjects metadata**, a cross-language standard. Eight base types:
117
-
118
- | Type | Purpose |
119
- |---|---|
120
- | \`metadata\` | Root document wrapper |
121
- | \`object\` | An entity (table/record) |
122
- | \`field\` | A property on an object |
123
- | \`attr\` | Named scalar/array decoration on any parent |
124
- | \`validator\` | A validation rule |
125
- | \`view\` | A UI control kind |
126
- | \`identity\` | A primary/secondary key |
127
- | \`relationship\` | An association between objects |
128
-
129
- ### Two most-violated rules
130
-
131
- 1. **Attribute uniqueness.** Within a single parent metadata node, all attribute names must be unique. You cannot have two \`attr\` children both named \`alternative\`. For multi-value, use a single \`stringarray\` attr: \`"@alternatives": ["a", "b", "c"]\`.
132
-
133
- 2. **Inline \`@<name>\` and \`attr\` child are the same thing.** \`"@maxLength": 50\` is shorthand for \`{"attr": {"name": "maxLength", "subType": "int", "value": "50"}}\`. The parser converts inline form into attr children. Don't use both forms for the same attribute name on the same parent.
134
-
135
- ### Object subtypes (v0.3)
136
-
137
- - \`base\` — abstract template (no runtime semantics)
138
- - \`entity\` — persistent record; should have a primary identity
139
- - \`value\` — value-object; equality by content; must NOT have a primary identity
140
-
141
- Java-runtime strategies (pojo / map / proxy) belong on \`@javaRuntime\`, not in \`subType\`.
142
-
143
- ### Reserved structural keys (NOT attributes)
144
-
145
- \`name\`, \`subType\`, \`package\`, \`extends\`, \`isAbstract\`, \`children\`, \`merge\`, \`value\`.
146
-
147
- The v0.2 keys (\`super\`, \`overlay\`, \`override\`, \`isInterface\`, \`implements\`) are **gone**. The current parser will reject them. Use:
148
- - \`extends:\` instead of \`super:\` for the supertype reference
149
- - \`merge: true\` instead of \`overlay: true\` / \`override: true\` for in-place modification
150
- - \`@isAbstract: true\` instead of \`isInterface: true\` (multiple inheritance is not supported)
151
-
152
- ### Package paths and inheritance
153
-
154
- - Package segments separated by \`::\` — \`acme::common::id\`
155
- - Relative references in \`extends:\` — \`..::common::id\` means "go up to parent package, descend into \`common::id\`". Relative forms (\`..::\` parent-relative, leading \`::\` root-absolute) are a **YAML-authoring affordance only**; canonical JSON must be fully-qualified (a relative ref in JSON is rejected with \`ERR_RELATIVE_REF_IN_CANONICAL\`).
156
- - Cross-file resolution works as long as all files are passed to Loader (or live in the same \`metaobjects/\` directory)
157
-
158
- ### Two special intercepted attrs (parser-routed)
159
-
160
- - \`@isArray\` → marks a field as a collection
161
- - \`@isAbstract\` → marks a node as abstract (inheritable but not instantiable)
162
-
163
- ## Validators — two layers
164
-
165
- Validators can attach in two places, and they compose:
166
-
167
- **Field-level validators** describe what makes the *stored value* valid. They survive across UI, API, batch import, manual SQL — anywhere data enters the system. The generated Zod \`<Entity>InsertSchema\` encodes these.
168
-
169
- \`\`\`json
170
- {"field": {"name": "email", "subType": "string",
171
- "children": [
172
- {"validator": {"subType": "required"}},
173
- {"validator": {"subType": "regex", "@pattern": "^[^@]+@[^@]+\\\\.[^@]+$"}}
174
- ]
175
- }}
176
- \`\`\`
177
-
178
- **View-level validators** describe what makes user *input* valid in a specific UI surface — possibly stricter, possibly with different messages, possibly format-specific. They run client-side in generated forms. They do NOT necessarily reject the stored value if it's already in the DB.
179
-
180
- \`\`\`json
181
- {"field": {"name": "phone", "subType": "string",
182
- "children": [
183
- {"validator": {"subType": "regex", "@pattern": "^\\\\+?[0-9]+$"}},
184
- {"view": {"subType": "text-input", "@label": "Phone",
185
- "children": [
186
- {"validator": {"subType": "length", "@min": 7, "@max": 20,
187
- "@message": "Phone must be 7-20 digits"}}
188
- ]
189
- }}
190
- ]
191
- }}
192
- \`\`\`
193
-
194
- Rule of thumb: rules that protect data integrity → field. Rules that improve input UX → view.
195
-
196
- ## metaobjects.config.ts — generator wiring (project root)
197
-
198
- \`meta gen\` reads \`metaobjects.config.ts\` at the project root. This is where you declare which generators run and their options. It is TypeScript, type-checked, and imported via \`jiti\` at run time.
199
-
200
- \`\`\`ts
201
- import { defineConfig } from "@metaobjectsdev/cli";
202
- import {
203
- entityFile, queriesFile, routesFile, /* formFile, */ barrel,
204
- } from "@metaobjectsdev/codegen-ts/generators";
205
-
206
- export default defineConfig({
207
- outDir: "packages/database/src/generated",
208
- extStyle: "none",
209
- dbImport: "../index",
210
- dialect: "sqlite",
211
- generators: [
212
- entityFile(),
213
- queriesFile(),
214
- routesFile(),
215
- // formFile(), // opt-in: emits stock React forms per entity
216
- barrel(),
217
- ],
218
- });
219
- \`\`\`
220
-
221
- 3rd-party generator example: \`import { tanstackQuery } from "@metaobjectsdev/codegen-ts-tanstack"; // then add tanstackQuery({ ... }) to the generators array\`
222
-
223
- Filters live on the generator entry: \`routesFile({ filter: e => e.name !== "AuditLog" })\`
224
-
225
- \`.metaobjects/config.json\` holds static project state (schema_version, pending_in_git, confidence_thresholds) **and \`sources\` — the set of places your metadata comes from** (see "Where metadata comes from" below). Generator wiring belongs in \`metaobjects.config.ts\` so TypeScript can type-check the imports.
226
-
227
- ## Generated hooks + grids (TanStack)
228
-
229
- When \`tanstackQuery()\` is in your \`metaobjects.config.ts\`, every entity gets \`<Entity>.hooks.ts\` with a query-key factory + \`useEntity\`, \`useEntities\`, \`useCreate\`, \`useUpdate\`, \`useDelete\` hooks. When \`tanstackGrid()\` is in the config, entities with a \`layout[dataGrid]\` child also get \`<Entity>.columns.tsx\`.
230
-
231
- \`\`\`tsx
232
- import { usePrograms, useCreateProgram } from "@your-pkg/database/generated/Program.hooks";
233
- import { programDefaultColumns, programDefaultGrid } from "@your-pkg/database/generated/Program.columns";
234
- import { EntityGrid } from "@metaobjectsdev/tanstack";
235
-
236
- const { data, isLoading } = usePrograms();
237
- const create = useCreateProgram({ onSuccess: () => navigate("/programs") });
238
-
239
- <EntityGrid
240
- columns={programDefaultColumns}
241
- grid={programDefaultGrid}
242
- data={data ?? []}
243
- isLoading={isLoading}
244
- onRowClick={(row) => navigate(\`/admin/programs/\${row.id}\`)}
245
- />
246
- \`\`\`
247
-
248
- **Provider setup.** Wrap your app with \`<EntityFetcherProvider value={fetcher}>\` (supplies the HTTP fetcher to all generated hooks). For an admin subtree with different auth, wrap a second time inside: \`<EntityFetcherProvider value={adminFetch}>...</EntityFetcherProvider>\` overrides the outer one.
249
-
250
- **Metadata layer — grid definition:**
251
-
252
- \`\`\`jsonc
253
- { "layout": {
254
- "subType": "dataGrid",
255
- "name": "default",
256
- "@pageSize": 25,
257
- "@defaultSortField": "createdAt",
258
- "@defaultSortOrder": "desc",
259
- "@filterable": true,
260
- "@columns": ["email", "firstName", "subscribed", "createdAt"]
261
- }}
262
- \`\`\`
263
-
264
- The \`@columns\` attr is a flat string array listing fields to display. Per-column rendering comes from each field's own \`view\` subtype (the same one that drives forms); sortability comes from the field's \`@sortable\` attr; width belongs in app CSS. There are no nested per-column children — just \`@columns\`.
265
-
266
- **Cell renderers.** Field rendering inside grids comes from each field's own \`view\` subtype (the same one that drives forms). Override defaults app-wide with \`<CellRendererProvider value={{ currency: ({ getValue }) => <Money value={getValue()} /> }}>\`.
267
-
268
- **Per-entity opt-out.** \`@emitTanstack: false\` on an entity skips both hooks and columns.
269
-
270
- ## Filtering generated lists
271
-
272
- Mark filterable fields in metadata with \`@filterable: true\`:
273
-
274
- \`\`\`jsonc
275
- { "field": { "name": "email", "subType": "string", "@filterable": true } }
276
- \`\`\`
277
-
278
- The generated \`useSubscribers(filter)\` hook accepts a typed filter:
279
-
280
- \`\`\`tsx
281
- const { data } = useSubscribers({
282
- email: { like: "amy@%" },
283
- subscribed: true,
284
- sort: "createdAt:desc",
285
- limit: 25,
286
- });
287
- \`\`\`
288
-
289
- URL sent: \`/subscribers?filter[email][like]=amy@%25&filter[subscribed]=true&sort=createdAt:desc&limit=25\`
290
-
291
- **Leading wildcards are rejected by default.** The generated \`<Entity>FilterAllowlist\` ships \`leadingWildcard: false\` on every field, so a \`like\` pattern starting with \`%\` (e.g. \`"%@example.com"\`) is a 400 \`filter.leading_wildcard_disallowed\` — an unanchored LIKE defeats index usage, so it is fail-closed. To opt a field in, hand-edit that field's entry in the generated allowlist to \`leadingWildcard: true\` (hand edits inside generated files survive regeneration via the three-way merge). This gate is TypeScript-only; other ports' generated APIs do not enforce it.
292
-
293
- **Operators by field subtype:**
294
- - String: \`eq, ne, in, like, isNull\`
295
- - Number/date: \`eq, ne, gt, gte, lt, lte, in, isNull\`
296
- - Boolean: \`eq, isNull\`
297
-
298
- Illegal combinations like \`useSubscribers({ subscribed: { gte: true } })\` fail to compile (booleans don't support \`gte\`).
299
-
300
- **Per-grid preset filter** via layout \`@filter\`:
301
-
302
- \`\`\`jsonc
303
- { "layout": { "subType": "dataGrid", "name": "active",
304
- "@filter": { "subscribed": true },
305
- "@columns": ["email", "firstName", "subscribed"] }}
306
- \`\`\`
307
-
308
- Generates \`subscriberActiveFilter\` const consumable in pages. Compose with ad-hoc filters via object spread.
309
-
310
- ## Projections (read models with joined/aggregated columns)
311
-
312
- When a list needs computed columns (counts, sums, joined fields), create a **projection** — an entity that extends a base entity but reads from a SQL view:
313
-
314
- \`\`\`json
315
- // metaobjects/meta.commerce.json (inline with Program)
316
- {
317
- "object": {
318
- "name": "ProgramSummary",
319
- "subType": "entity",
320
- "extends": "Program",
321
- "children": [
322
- { "source": { "subType": "rdb", "@kind": "view", "@table": "v_program_summary" } },
323
- { "field": { "name": "weekCount", "subType": "int", "children": [
324
- { "origin": { "subType": "aggregate",
325
- "@agg": "count", "@of": "Week.id", "@via": "Program.weeks" }}
326
- ]}},
327
- { "identity": { "subType": "primary", "name": "id", "@fields": "id" } }
328
- ]
329
- }
330
- }
331
- \`\`\`
332
-
333
- \`meta gen\` produces a read-only \`useProgramSummaries(filter)\` hook, a SQL view DDL in the migration, and a read-only GET-only route.
334
-
335
- **Aggregate vocabulary** (\`origin.aggregate @agg\`): \`count\`, \`sum\`, \`avg\`, \`min\`, \`max\`; plus \`any\`/\`all\` (a boolean predicate quantifier over a required \`@filter\` — no \`@of\`; "did any/every related row match?"), and \`collect\` (an array rollup of \`@of\` — the field must be \`isArray: true\`; \`@distinct\` dedupes, \`@orderBy\` sets element order).
336
-
337
- **Other read-model origins**: \`origin.computed\` — a row-level value from the base row's own fields via a structured \`@expr\` tree (e.g. \`{ "op": "isNotNull", "arg": { "field": "payloadJson" } }\` → a boolean, to avoid shipping a heavy column); \`origin.first\` — the single related row picked by \`@orderBy\` along \`@via\`, projecting \`@of\` (e.g. "the latest child's status"; the field must not be \`@required\` — an empty related set yields null).
338
-
339
- **Multi-level via paths** are supported: \`@via: "Program.weeks.workouts"\` builds a 2-level JOIN tree.
340
-
341
- **For pages that need a full nested tree** (e.g., Program → Weeks → Workouts → Exercises), use 4 entity hooks with Project D's filter syntax for batched lookups (no projection needed — flat hooks + client-side stitching is enough):
342
-
343
- \`\`\`tsx
344
- const { data: weeks } = useWeeks({ programId, sort: "weekNumber:asc" });
345
- const weekIds = weeks?.map((w) => w.id) ?? [];
346
- const { data: workouts } = useWorkouts(
347
- weekIds.length ? { weekId: { in: weekIds } } : undefined,
348
- );
349
- \`\`\`
350
-
351
- ## Currency fields
352
-
353
- Declare a money field with \`subType: "currency"\`:
354
-
355
- \`\`\`json
356
- { "field": { "name": "priceCents", "subType": "currency", "@currency": "USD" } }
357
- \`\`\`
358
-
359
- Storage stays as integer minor units (cents for USD). The generated \`<Entity>\` constants block carries \`view\`, \`currency\`, \`locale\` so admin grids auto-format prices.
360
-
361
- **Imports — use sub-paths in browser code:**
362
-
363
- \`\`\`tsx
364
- import { formatCurrency } from "@metaobjectsdev/runtime-web";
365
- import { CurrencyInput } from "@metaobjectsdev/react";
366
- \`\`\`
367
-
368
- **Display:**
369
-
370
- \`\`\`tsx
371
- <span>{formatCurrency(program.priceCents)}</span> // $15.00
372
- <span>{formatCurrency(p.amountCents, "EUR", "de-DE")}</span> // 15,00 €
373
- \`\`\`
374
-
375
- **Form input:**
376
-
377
- \`\`\`tsx
378
- <CurrencyInput value={priceCents} onChange={setPriceCents} currency="USD" />
379
- \`\`\`
380
-
381
- User types \`15.99\` → component emits \`1599\` to \`onChange\` on blur. Wire format is always integer cents.
382
-
383
- **Locale override** via a \`view[currency]\` child:
384
-
385
- \`\`\`json
386
- { "field": { "name": "priceCents", "subType": "currency", "@currency": "EUR",
387
- "children": [{ "view": { "subType": "currency", "@locale": "de-DE" } }]
388
- }}
389
- \`\`\`
390
-
391
- Currency code lives on the field; locale lives on the view.
392
-
393
- ## Generated artifacts — what \`meta gen\` produces
394
-
395
- After \`meta gen\`, you get one barrel + per-entity files in your configured \`outDir\` (default \`packages/database/src/generated/\`):
396
-
397
- | File | What's in it | When to touch by hand |
398
- |---|---|---|
399
- | \`<Entity>.ts\` | Drizzle table, relations(), inferred types, Zod insert/update schemas, and the rich \`<Entity>\` constants block (per-field objects with name, label, view, htmlType, rules, etc.) | Never. Regenerate. |
400
- | \`<Entity>.queries.ts\` | Typed query helpers (\`findUserById\`, \`listUsers\`, \`createUser\`, ...) using prepared statements | Never. Regenerate. |
401
- | \`<Entity>.routes.ts\` | Fastify CRUD plugin delegating to \`mountCrudRoutes\` from \`@metaobjectsdev/runtime-ts/drizzle-fastify\` (5 verbs, Zod validation, 404/204 mapping, Drizzle-direct under the hood) | Never. Regenerate. |
402
- | \`<Entity>.form.tsx\` | React form using \`useEntityForm\` + the entity constants. **OPT-IN at project level:** add \`formFile()\` to \`generators\` in \`metaobjects.config.ts\`. Opt out per-entity via \`@emitForm: false\`. | Never. Regenerate. |
403
- | \`index.ts\` | Barrel re-exporting every entity file | Never. Regenerate. |
404
-
405
- For business logic the generator doesn't cover, create a SIBLING file: \`<Entity>.extra.ts\` for query/route helpers, or any file you like in your apps directory. Import the constants from the generated \`<Entity>.ts\`.
406
-
407
- ### Stock route mounting
408
-
409
- \`\`\`ts
410
- import { subscriberRoutes } from "@your-pkg/database/generated/Subscriber.routes";
411
- fastify.register(subscriberRoutes, { prefix: "/api" });
412
- \`\`\`
413
-
414
- That mounts: \`GET /api/subscribers\`, \`GET /api/subscribers/:id\`, \`POST /api/subscribers\`, \`PATCH /api/subscribers/:id\`, \`DELETE /api/subscribers/:id\`. Pagination via \`?limit=\` & \`?offset=\`. Validation via the generated Zod schemas. Drizzle calls under the hood.
415
-
416
- ### Mixing custom routes alongside generated
417
-
418
- \`\`\`ts
419
- import { db, subscribers } from "@your-pkg/database";
420
- import { Subscriber, SubscriberInsertSchema } from "@your-pkg/database/generated/Subscriber";
421
- import { eq } from "drizzle-orm";
422
-
423
- fastify.post("/subscribe", async (req, reply) => {
424
- // Generated Zod schema validates the body — same schema the API route uses.
425
- const parsed = SubscriberInsertSchema.safeParse(req.body);
426
- if (!parsed.success) return reply.code(400).send({ issues: parsed.error.issues });
427
-
428
- // Drizzle's typed accessors are already TS-safe; no need for indirection.
429
- const existing = await db.select().from(subscribers).where(eq(subscribers.email, parsed.data.email)).get();
430
- if (existing) return reply.code(409).send({ error: "Already subscribed" });
431
-
432
- const [row] = await db.insert(subscribers).values(parsed.data).returning();
433
- // ... your business logic (analytics, Loops/Mailchimp, navigation, etc.)
434
- return reply.code(201).send(row);
435
- });
436
- \`\`\`
437
-
438
- The fact that every metadata-derived value flows from \`Subscriber\` / \`SubscriberInsertSchema\` / the typed \`subscribers\` table is what makes rename-the-field-in-metadata-and-regen safe.
439
-
440
- ### Hand-written form using \`useEntityForm\`
441
-
442
- \`\`\`tsx
443
- import { useEntityForm } from '@metaobjectsdev/react';
444
- import { Subscriber, SubscriberInsertSchema, type Subscriber as Row } from './generated/Subscriber';
445
-
446
- export function SubscribeForm() {
447
- const form = useEntityForm(Subscriber, SubscriberInsertSchema);
448
- const { handleSubmit, formState: { errors } } = form;
449
-
450
- return (
451
- <form onSubmit={handleSubmit(/* your onSubmit */)} className="your-design-system">
452
- <label>{Subscriber.email.label}</label>
453
- <input {...form.input.email} />
454
- {errors.email && <span>{errors.email.message}</span>}
455
-
456
- <label>{Subscriber.firstName.label}</label>
457
- <input {...form.input.firstName} />
458
- {errors.firstName && <span>{errors.firstName.message}</span>}
459
-
460
- <button type="submit">Subscribe</button>
461
- </form>
462
- );
463
- }
464
- \`\`\`
465
-
466
- Spread \`form.input.<field>\` — it carries name, type, placeholder, rules, aria-label automatically. No magic strings.
467
-
468
- ## Meta Forge additions
469
-
470
- ### \`@forge*\` attribute namespace
471
-
472
- Provenance and confidence concerns expressed as inline attributes on any metadata child. Names use camelCase (no separator).
473
-
474
- Most common:
475
- - \`@forgeConfidence\` (double 0..1) — confidence the record is correct
476
- - \`@forgeSource\` (string) — \`human\` | \`claude\` | \`ts-ast\` | \`drizzle\` | ...
477
- - \`@forgePrimaryLocation\` (string) — file path for an entity
478
- - \`@forgeRationale\` (string, decision only) — why this decision
479
- - \`@forgeAlternatives\` (stringarray, decision only) — alternatives considered
480
-
481
- Full inventory in \`packages/sdk/FORGE-METADATA.md\`.
482
-
483
- ### New top-level types
484
-
485
- Registered by \`@metaobjectsdev/sdk\` into the TypeRegistry:
486
-
487
- | Type | Purpose |
488
- |---|---|
489
- | \`decision\` | Architectural or design decision |
490
- | \`principle\` | Design principle (advisory/enforced) |
491
- | \`convention\` | Coding/structural convention |
492
- | \`glossary\` | Domain-term definition |
493
- | \`failure\` | Recorded failure mode |
494
-
495
- These coexist with \`object\` children in the same package files. \`meta gen\` and \`meta migrate\` only consume \`object\`; the descriptive types are context for AI tooling and don't drive codegen.
496
-
497
- ## File layout
498
-
499
- \`\`\`
500
- metaobjects/
501
- ├── meta.common.json shared base fields/validators (optional)
502
- ├── meta.<domain>.json your entity packages(s)
503
- └── _pending/<pkg>.json proposed packages awaiting review
504
-
505
- .metaobjects/
506
- ├── config.json static project state + \`sources\`
507
- ├── migrations/ written by meta migrate
508
- └── .gen-state/ codegen merge base (gitignored)
509
-
510
- metaobjects.config.ts generator wiring (committed)
511
- \`\`\`
512
-
513
- ### Where metadata comes from — the \`sources\` set
514
-
515
- **\`metaobjects/\` is the DEFAULT VALUE of \`sources\`, not a requirement.** When \`sources\` is absent or empty in \`.metaobjects/config.json\`, it takes that default — the \`metaobjects/\` directory beside the \`.metaobjects/\` folder holding the config. \`meta init\` scaffolds \`"sources": []\`, so a project that does nothing takes the default.
516
-
517
- Do NOT assume that directory exists. A project may declare \`sources\` and put its metadata anywhere — a sibling module, a shared model repository, a single file — and need not have a directory of that name at all:
518
-
519
- \`\`\`json
520
- {
521
- "schema_version": 1,
522
- "sources": [
523
- { "path": "../model/src/main/resources/metadata" },
524
- { "path": "metaobjects" }
525
- ]
526
- }
527
- \`\`\`
528
-
529
- \`meta gen\`, \`meta migrate\`, \`meta verify\`, \`meta docs\` and \`meta export\` all read exactly that set, so pointing \`sources\` elsewhere moves every command together. A \`path\` is read **in place and never installed** or copied.
530
-
531
- \`sources\` is read by **all four CLI surfaces** — the Node \`meta\` CLI, \`dotnet meta\` (C#), \`metaobjects\` (Python) and \`metaobjects:generate\` (Java and Kotlin, via Maven). Each resolves the same files from the same declaration.
532
-
533
- **\`sources\` is a set, not an ordered list** — resolution is order-independent, so declaration order never changes what loads. \`config.json\` also rejects unknown top-level keys, so a misspelled key is an error rather than a silently ignored one.
534
-
535
- ## Worked example
536
-
537
- \`\`\`json
538
- {
539
- "metadata": {
540
- "package": "myapp",
541
- "children": [
542
- {
543
- "object": {
544
- "name": "User",
545
- "subType": "entity",
546
- "@forgeConfidence": 0.95,
547
- "@forgeSource": "human",
548
- "@forgePrimaryLocation": "src/db/users.schema.ts",
549
- "children": [
550
- {"field": {"name": "id", "extends": "common::id"}},
551
- {"field": {"name": "email", "subType": "string",
552
- "@column": "email_address",
553
- "children": [{"validator": {"subType": "required"}}]
554
- }},
555
- {"identity": {"name": "pk", "subType": "primary", "@fields": ["id"], "@generation": "increment"}}
556
- ]
557
- }
558
- },
559
- {
560
- "decision": {
561
- "name": "useTanstackQuery",
562
- "subType": "global",
563
- "@forgeConfidence": 0.9,
564
- "@forgeSource": "human",
565
- "@forgeRationale": "Real-time invalidation matters for live game state.",
566
- "@forgeAlternatives": ["swr", "redux-toolkit-query"]
567
- }
568
- }
569
- ]
570
- }
571
- }
572
- \`\`\`
573
-
574
- ## Authoring guidance
575
-
576
- | Situation | Action |
577
- |---|---|
578
- | Adding a field to an existing entity | Edit the \`object\`'s \`children\`; append a \`field\` node, then \`meta gen\` |
579
- | New entity in an existing domain | Append an \`object\` to the appropriate package file, then \`meta gen\` |
580
- | Renaming an entity or field | Edit the metadata, regenerate; TS will surface every stale consumer of the constants |
581
- | New REST resource | Already done — \`meta gen\` produced \`<Entity>.routes.ts\`. Just \`fastify.register(...)\` it |
582
- | Custom business logic (Stripe webhook, side-effects, auth flows) | Hand-write a route/handler that imports the generated constants + \`om()\` |
583
- | Architectural choice affecting how entities are built | Add a \`decision\` with \`@forgeRationale\` + \`@forgeAlternatives\` |
584
- | Coding convention | Add a \`convention\` with \`@forgePatternDescription\` + \`@forgeAppliesTo\` |
585
- | Domain term | Add a \`glossary\` entry with \`@forgeTerm\` + \`@forgeDefinition\` |
586
-
587
- ## Deeper references
588
-
589
- - \`packages/metadata/METAMODEL.md\` — full metamodel reference
590
- - \`packages/sdk/FORGE-METADATA.md\` — full \`@forge*\` inventory + MetaObjects layout details
591
- - \`docs/strategy/2026-05-12-v0.3-ai-first-metadata-loading.md\` — current strategy (v0.3 vocab, packages, AI-first loading)
592
- `;
593
- //# sourceMappingURL=body.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"body.js","sourceRoot":"","sources":["../../src/agent-docs/body.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0kB9B,CAAC"}
@@ -1,8 +0,0 @@
1
- export declare function computeContentHash(body: string): string;
2
- /** Returns the body with a content-hash HTML comment prepended. */
3
- export declare function withContentHash(body: string): string;
4
- /** Extract the embedded hash, or undefined if not present. */
5
- export declare function extractContentHash(fileBody: string): string | undefined;
6
- /** True iff the file body's hash matches its own content (i.e. unmodified). */
7
- export declare function isUnmodified(fileBody: string): boolean;
8
- //# sourceMappingURL=content-hash.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"content-hash.d.ts","sourceRoot":"","sources":["../../src/agent-docs/content-hash.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKtD"}
@@ -1,23 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- export function computeContentHash(body) {
3
- return createHash("sha256").update(body, "utf8").digest("hex");
4
- }
5
- /** Returns the body with a content-hash HTML comment prepended. */
6
- export function withContentHash(body) {
7
- const hash = computeContentHash(body);
8
- return `<!-- metaobjects-content-hash: ${hash} -->\n${body}`;
9
- }
10
- /** Extract the embedded hash, or undefined if not present. */
11
- export function extractContentHash(fileBody) {
12
- const match = /<!-- metaobjects-content-hash: ([a-f0-9]{64}) -->/.exec(fileBody);
13
- return match?.[1];
14
- }
15
- /** True iff the file body's hash matches its own content (i.e. unmodified). */
16
- export function isUnmodified(fileBody) {
17
- const embedded = extractContentHash(fileBody);
18
- if (embedded === undefined)
19
- return false;
20
- const withoutHash = fileBody.replace(/^<!-- metaobjects-content-hash: [a-f0-9]{64} -->\n/, "");
21
- return computeContentHash(withoutHash) === embedded;
22
- }
23
- //# sourceMappingURL=content-hash.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"content-hash.js","sourceRoot":"","sources":["../../src/agent-docs/content-hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,kCAAkC,IAAI,SAAS,IAAI,EAAE,CAAC;AAC/D,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,MAAM,KAAK,GAAG,mDAAmD,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACzC,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,oDAAoD,EAAE,EAAE,CAAC,CAAC;IAC/F,OAAO,kBAAkB,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC;AACtD,CAAC"}
@@ -1,3 +0,0 @@
1
- export { AGENT_DOCS_BODY } from "./body.js";
2
- export { computeContentHash, withContentHash, extractContentHash, isUnmodified, } from "./content-hash.js";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-docs/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC"}
@@ -1,4 +0,0 @@
1
- // Public surface for @metaobjectsdev/sdk/agent-docs.
2
- export { AGENT_DOCS_BODY } from "./body.js";
3
- export { computeContentHash, withContentHash, extractContentHash, isUnmodified, } from "./content-hash.js";
4
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/agent-docs/index.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC"}