@metaobjectsdev/sdk 0.10.0 → 0.11.0-rc.1
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/agent-context/skills/metaobjects-authoring/SKILL.md +37 -0
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +30 -0
- package/agent-context/skills/metaobjects-prompts/SKILL.md +41 -0
- package/agent-context/skills/metaobjects-verify/references/migration.md +34 -1
- package/package.json +2 -2
|
@@ -165,6 +165,26 @@ Reuse a constraint set across entities with an abstract `field.enum` + `extends`
|
|
|
165
165
|
{ "field.object": { "name": "address", "@objectRef": "Address", "@storage": "flattened" } }
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
**Arrays of value objects** — set `isArray: true` with `@storage: jsonb`. The whole
|
|
169
|
+
array lives in **one** jsonb column (a JSON array), never a native `jsonb[]`. The
|
|
170
|
+
generated Postgres column is typed `.$type<VO[]>()` and the Zod schema is
|
|
171
|
+
`z.array(<VO>InsertSchema)`:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{ "field.object": { "name": "triples", "@objectRef": "Triple",
|
|
175
|
+
"@storage": "jsonb", "isArray": true } }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Opaque jsonb (no value object)** — when the payload has no fixed shape (freeform
|
|
179
|
+
config, passthrough metadata, an open-keyed map), do NOT use `field.object` (it
|
|
180
|
+
requires `@objectRef`, and a partial VO would let the generated Zod strip unknown
|
|
181
|
+
keys → data loss). Model it as a `field.string` with the physical-type override
|
|
182
|
+
`@dbColumnType: jsonb` — the logical type stays string-bound, the column is jsonb:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "field.string": { "name": "metadata", "@dbColumnType": "jsonb" } }
|
|
186
|
+
```
|
|
187
|
+
|
|
168
188
|
## YAML sigil-free authoring + the coercion footgun
|
|
169
189
|
|
|
170
190
|
In YAML, write the fused `type.subType` key with a **map body**, bare reserved
|
|
@@ -205,6 +225,12 @@ reference for navigation/typing/codegen only. Referential actions
|
|
|
205
225
|
(`@onDelete`/`@onUpdate`) are NOT on `identity.reference` — they live on the
|
|
206
226
|
`relationship.*` node (see Relationships below).
|
|
207
227
|
|
|
228
|
+
`@references` resolves cross-package by **fully-qualified name**
|
|
229
|
+
(`@references: "shared::billing::Account"`), the same rule as `extends`; a bare
|
|
230
|
+
name resolves within the current package. The FK target must be an entity with a
|
|
231
|
+
single-column primary key (the FK points at that PK); a target with a composite
|
|
232
|
+
PK needs the explicit dotted form `@references: "pkg::Target.fieldA,fieldB"`.
|
|
233
|
+
|
|
208
234
|
```json
|
|
209
235
|
{ "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }
|
|
210
236
|
{ "identity.secondary": { "name": "byEmail", "@fields": ["email"] } }
|
|
@@ -229,6 +255,17 @@ the two halves of one FK.
|
|
|
229
255
|
"@cardinality": "many", "@onDelete": "cascade" } }
|
|
230
256
|
```
|
|
231
257
|
|
|
258
|
+
**Adoption footgun — pin BOTH actions.** `@onDelete` and `@onUpdate` each default to
|
|
259
|
+
`cascade` when omitted, but a plain SQL foreign key is `NO ACTION` on both. If you're
|
|
260
|
+
adopting an existing database (matching metadata to a live schema), omitting these
|
|
261
|
+
makes the metadata declare `CASCADE` where the DB has `NO ACTION` — a perpetual
|
|
262
|
+
`verify --db` drift. Pin **both** explicitly to the DB's real behavior:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{ "relationship.composition": { "name": "author", "@objectRef": "User",
|
|
266
|
+
"@cardinality": "one", "@onDelete": "no-action", "@onUpdate": "no-action" } }
|
|
267
|
+
```
|
|
268
|
+
|
|
232
269
|
## Sources — `source.rdb` + `@kind`
|
|
233
270
|
|
|
234
271
|
`source.rdb` declares where an entity's data lives. Read-only-ness derives from
|
|
@@ -41,6 +41,11 @@ export default defineConfig({
|
|
|
41
41
|
dialect: "postgres", // "postgres" | "sqlite" | "d1" (D1 is TS-only)
|
|
42
42
|
apiPrefix: "/api", // flows to routes AND client fetch URLs
|
|
43
43
|
columnNamingStrategy: "snake_case", // "snake_case" (default) | "literal" | "kebab-case"
|
|
44
|
+
timestampMode: "string", // "string" (default, ISO-8601 wire contract) | "date" (Drizzle native Date)
|
|
45
|
+
pluralizeCollections: true, // default; table VARS auto-pluralize (AgentConfig → agentConfigs)
|
|
46
|
+
collectionNameOverrides: { // per-entity escape hatch for names the rule gets wrong
|
|
47
|
+
AuditLog: "auditLog", LlmTierConfig: "llmTierConfig",
|
|
48
|
+
},
|
|
44
49
|
generators: [
|
|
45
50
|
entityFile(), queriesFile(), routesFile(), barrel(),
|
|
46
51
|
formFile(), tanstackQuery(), tanstackGrid(),
|
|
@@ -48,6 +53,13 @@ export default defineConfig({
|
|
|
48
53
|
});
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
Naming + timestamp knobs are **codegen config**, not metadata attributes — a
|
|
57
|
+
collection variable name and a Drizzle column mode are per-port rendering choices
|
|
58
|
+
with no meaning to the other language ports, so they carry no cross-port
|
|
59
|
+
conformance cost. `collectionNameOverrides` wins over `pluralizeCollections` and is
|
|
60
|
+
applied consistently to the table declaration, every FK reference, the `relations()`
|
|
61
|
+
block, and the inferred types.
|
|
62
|
+
|
|
51
63
|
A second file, `.metaobjects/config.json`, holds static project state parseable by
|
|
52
64
|
non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
|
|
53
65
|
|
|
@@ -133,3 +145,21 @@ Deterministic per dialect: `field.string` + `@maxLength` → `varchar(N)`,
|
|
|
133
145
|
(Postgres) + `gen_random_uuid()`, `field.enum` → `varchar` + `CHECK`. Override a
|
|
134
146
|
field's physical column name with `@column` on the field; the DB schema name lives
|
|
135
147
|
on `source.rdb` via `@schema`.
|
|
148
|
+
|
|
149
|
+
### Value-object jsonb columns
|
|
150
|
+
|
|
151
|
+
A `field.object` with `@storage: jsonb` (or the default `subdocument`) becomes a
|
|
152
|
+
single typed jsonb column — the referenced value-object's TS type is carried onto
|
|
153
|
+
the Drizzle column via `.$type<>()`, and its Zod schema is the VO's `InsertSchema`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// field.object @objectRef=LlmConfig @storage=jsonb
|
|
157
|
+
llmConfigJson: jsonb("llm_config_json").$type<LlmConfig>(),
|
|
158
|
+
// field.object @objectRef=Triple @storage=jsonb isArray=true
|
|
159
|
+
triples: jsonb("triples").$type<Triple[]>(), // one jsonb column, NOT a native jsonb[]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The VO type, its Zod `InsertSchema`, and this `.$type<>()` all import the VO from
|
|
163
|
+
the same module (layout/package/`extStyle`-aware resolution). An opaque jsonb column
|
|
164
|
+
(`field.string @dbColumnType: jsonb`) gets no `.$type<>()` — it stays `unknown`,
|
|
165
|
+
which is the correct shape for freeform payloads with no fixed VO.
|
|
@@ -123,6 +123,47 @@ For the `xml`-format example above with payload `{ displayName: "Ada", postCount
|
|
|
123
123
|
bytes. You render the prompt, call your LLM client (provider-agnostic — codegen
|
|
124
124
|
emits no provider-side schema), then parse the response.
|
|
125
125
|
|
|
126
|
+
## Conditional content: data and flags, never branched prose
|
|
127
|
+
|
|
128
|
+
When a prompt's wording varies along some dimension — audience, tier, mode,
|
|
129
|
+
locale, entitlement, a domain variant — do NOT branch the prose in code and
|
|
130
|
+
concatenate strings. Branching prompt text in a service is the anti-pattern this
|
|
131
|
+
pillar exists to remove: it scatters the same distinction across call sites, each
|
|
132
|
+
re-encoded and free to drift, and none of it snapshot-tested. The variation
|
|
133
|
+
belongs in exactly two places, with a third for the rare genuine divergence:
|
|
134
|
+
|
|
135
|
+
- **Vocabulary as payload data.** The words and values that differ become typed
|
|
136
|
+
payload fields, pre-computed once from the varying dimension — a noun, a label,
|
|
137
|
+
a set of verbs (a list), an example. The template stays single and references
|
|
138
|
+
`{{term}}` / `{{#items}}…{{/items}}`. The prose *structure* is identical across
|
|
139
|
+
variants; only the data differs, so there is nothing to branch.
|
|
140
|
+
- **Presence as boolean flags.** When a whole block exists-or-not for a variant,
|
|
141
|
+
gate it with a section flag the payload sets: `{{#showBlock}}…{{/showBlock}}`.
|
|
142
|
+
Reserve flags for entire blocks — never mid-sentence word swaps, which are
|
|
143
|
+
vocabulary.
|
|
144
|
+
- **Variant text only when prose truly diverges.** If a section's wording — not
|
|
145
|
+
just its vocabulary — genuinely differs, select a per-variant text through the
|
|
146
|
+
provider seam (a `@textRef` variant, or an included partial) so the shared
|
|
147
|
+
prose still lives in one place. Expect to need this rarely.
|
|
148
|
+
|
|
149
|
+
A single resolver maps the varying dimension to that payload (the flags + the
|
|
150
|
+
vocabulary), so the distinction is defined ONCE and every template that depends
|
|
151
|
+
on it stays consistent.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
// WRONG — prose branched and concatenated in a service:
|
|
155
|
+
if (tier.isPremium()) sb.append("Your plan includes priority support.");
|
|
156
|
+
else sb.append("Upgrade any time for priority support.");
|
|
157
|
+
```
|
|
158
|
+
```mustache
|
|
159
|
+
{{! RIGHT — text in the template; the variant is data + a flag }}
|
|
160
|
+
{{supportLine}}
|
|
161
|
+
{{#isPremium}}(Priority queue enabled.){{/isPremium}}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This stays deterministic and golden-testable per variant: render the template
|
|
165
|
+
against each value of the dimension and snapshot every variant.
|
|
166
|
+
|
|
126
167
|
## `verify` fails the build on prompt-drift
|
|
127
168
|
|
|
128
169
|
For every template, the verify step resolves the text, parses each `{{...}}`
|
|
@@ -66,7 +66,40 @@ A clean run is silent; a failure names the drifted table/column. Bias toward
|
|
|
66
66
|
trusting the tool — a drift failure almost always means the metadata changed and the
|
|
67
67
|
DB didn't follow.
|
|
68
68
|
|
|
69
|
+
## Index modeling (Postgres)
|
|
70
|
+
|
|
71
|
+
Secondary indexes carry physical-shape attributes contributed by the db provider
|
|
72
|
+
(they live on `identity.secondary`, not core):
|
|
73
|
+
|
|
74
|
+
- `@orders` — per-key sort direction, positional to `@fields` (`["asc", "desc"]`).
|
|
75
|
+
Omit for all-ascending; drives `DESC`-ordered index keys (e.g. a recency index).
|
|
76
|
+
- `@where` — a partial-index predicate (raw SQL, e.g. `"delivered_at IS NULL"`),
|
|
77
|
+
emitted as `WHERE (<pred>)`. The index then covers only matching rows.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "identity.secondary": { "@fields": ["userId", "createdAt"],
|
|
81
|
+
"@orders": ["asc", "desc"], "@where": "archived_at IS NULL" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Adopting an existing database (non-destructive)
|
|
85
|
+
|
|
86
|
+
`meta verify --db` / `meta migrate` can reach **zero drift** against a hand-built
|
|
87
|
+
schema without a rewrite:
|
|
88
|
+
|
|
89
|
+
- **`meta migrate --from-db`** reverse-engineers a baseline from the live DB so the
|
|
90
|
+
first diff is empty.
|
|
91
|
+
- **Auto schema-scope** — the diff manages only the schemas the metadata *declares*
|
|
92
|
+
(via `source.rdb @schema`); tables in undeclared schemas belong to another owner
|
|
93
|
+
and are left untouched. This is what lets several apps share one database, each
|
|
94
|
+
owning its own schema, with a clean per-owner `verify --db` and no manual ignore
|
|
95
|
+
lists. A downstream app that extends the toolkit's DB declares its own `@schema`,
|
|
96
|
+
models only its tables, and runs its own migrate/verify against that scope.
|
|
97
|
+
- **`identity.reference @constraintName`** pins a foreign-key constraint name so the
|
|
98
|
+
metadata can match an existing DB's naming convention without a destructive
|
|
99
|
+
rename.
|
|
100
|
+
|
|
69
101
|
## Not yet shipped
|
|
70
102
|
|
|
71
|
-
Triggers, generated columns,
|
|
103
|
+
Triggers, generated columns, exclusion + CHECK constraints, MySQL, and data
|
|
72
104
|
migrations (column-type changes needing data transformation error out with a hint).
|
|
105
|
+
(Partial + descending **indexes** *are* supported — see Index modeling above.)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-rc.1",
|
|
4
4
|
"description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@metaobjectsdev/metadata": "0.
|
|
59
|
+
"@metaobjectsdev/metadata": "0.11.0-rc.1",
|
|
60
60
|
"zod": "^3.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|