Skip to content

Commit 302db52

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/metadata-validation
# Conflicts: # server/csharp/MetaObjects/CoreTypes.cs # server/csharp/MetaObjects/Registry.cs # server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java # server/java/metadata/src/main/java/com/metaobjects/registry/MetaDataRegistry.java # server/java/metadata/src/main/java/com/metaobjects/registry/TypeDefinition.java # server/java/metadata/src/main/java/com/metaobjects/registry/TypeDefinitionBuilder.java # server/python/src/metaobjects/core_types.py # server/python/src/metaobjects/registry.py # server/typescript/packages/metadata/src/registry.ts
2 parents e19937c + 1a025c0 commit 302db52

93 files changed

Lines changed: 3299 additions & 221 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent-context/skills/metaobjects-authoring/SKILL.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,26 @@ Reuse a constraint set across entities with an abstract `field.enum` + `extends`
165165
{ "field.object": { "name": "address", "@objectRef": "Address", "@storage": "flattened" } }
166166
```
167167

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+
168188
## YAML sigil-free authoring + the coercion footgun
169189

170190
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
205225
(`@onDelete`/`@onUpdate`) are NOT on `identity.reference` — they live on the
206226
`relationship.*` node (see Relationships below).
207227

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+
208234
```json
209235
{ "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }
210236
{ "identity.secondary": { "name": "byEmail", "@fields": ["email"] } }
@@ -229,6 +255,17 @@ the two halves of one FK.
229255
"@cardinality": "many", "@onDelete": "cascade" } }
230256
```
231257

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+
232269
## Sources — `source.rdb` + `@kind`
233270

234271
`source.rdb` declares where an entity's data lives. Read-only-ness derives from

agent-context/skills/metaobjects-codegen/references/typescript.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,25 @@ export default defineConfig({
4141
dialect: "postgres", // "postgres" | "sqlite" | "d1" (D1 is TS-only)
4242
apiPrefix: "/api", // flows to routes AND client fetch URLs
4343
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+
},
4449
generators: [
4550
entityFile(), queriesFile(), routesFile(), barrel(),
4651
formFile(), tanstackQuery(), tanstackGrid(),
4752
],
4853
});
4954
```
5055

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+
5163
A second file, `.metaobjects/config.json`, holds static project state parseable by
5264
non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
5365

@@ -133,3 +145,21 @@ Deterministic per dialect: `field.string` + `@maxLength` → `varchar(N)`,
133145
(Postgres) + `gen_random_uuid()`, `field.enum``varchar` + `CHECK`. Override a
134146
field's physical column name with `@column` on the field; the DB schema name lives
135147
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.

agent-context/skills/metaobjects-verify/references/migration.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,40 @@ A clean run is silent; a failure names the drifted table/column. Bias toward
6666
trusting the tool — a drift failure almost always means the metadata changed and the
6767
DB didn't follow.
6868

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+
69101
## Not yet shipped
70102

71-
Triggers, generated columns, partial/exclusion/check constraints, MySQL, and data
103+
Triggers, generated columns, exclusion + CHECK constraints, MySQL, and data
72104
migrations (column-type changes needing data transformation error out with a hint).
105+
(Partial + descending **indexes** *are* supported — see Index modeling above.)

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,26 @@ Reuse a constraint set across entities with an abstract `field.enum` + `extends`
165165
{ "field.object": { "name": "address", "@objectRef": "Address", "@storage": "flattened" } }
166166
```
167167

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+
168188
## YAML sigil-free authoring + the coercion footgun
169189

170190
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
205225
(`@onDelete`/`@onUpdate`) are NOT on `identity.reference` — they live on the
206226
`relationship.*` node (see Relationships below).
207227

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+
208234
```json
209235
{ "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }
210236
{ "identity.secondary": { "name": "byEmail", "@fields": ["email"] } }
@@ -229,6 +255,17 @@ the two halves of one FK.
229255
"@cardinality": "many", "@onDelete": "cascade" } }
230256
```
231257

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+
232269
## Sources — `source.rdb` + `@kind`
233270

234271
`source.rdb` declares where an entity's data lives. Read-only-ness derives from

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,40 @@ A clean run is silent; a failure names the drifted table/column. Bias toward
6666
trusting the tool — a drift failure almost always means the metadata changed and the
6767
DB didn't follow.
6868

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+
69101
## Not yet shipped
70102

71-
Triggers, generated columns, partial/exclusion/check constraints, MySQL, and data
103+
Triggers, generated columns, exclusion + CHECK constraints, MySQL, and data
72104
migrations (column-type changes needing data transformation error out with a hint).
105+
(Partial + descending **indexes** *are* supported — see Index modeling above.)

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,26 @@ Reuse a constraint set across entities with an abstract `field.enum` + `extends`
165165
{ "field.object": { "name": "address", "@objectRef": "Address", "@storage": "flattened" } }
166166
```
167167

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+
168188
## YAML sigil-free authoring + the coercion footgun
169189

170190
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
205225
(`@onDelete`/`@onUpdate`) are NOT on `identity.reference` — they live on the
206226
`relationship.*` node (see Relationships below).
207227

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+
208234
```json
209235
{ "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }
210236
{ "identity.secondary": { "name": "byEmail", "@fields": ["email"] } }
@@ -229,6 +255,17 @@ the two halves of one FK.
229255
"@cardinality": "many", "@onDelete": "cascade" } }
230256
```
231257

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+
232269
## Sources — `source.rdb` + `@kind`
233270

234271
`source.rdb` declares where an entity's data lives. Read-only-ness derives from

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,40 @@ A clean run is silent; a failure names the drifted table/column. Bias toward
6666
trusting the tool — a drift failure almost always means the metadata changed and the
6767
DB didn't follow.
6868

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+
69101
## Not yet shipped
70102

71-
Triggers, generated columns, partial/exclusion/check constraints, MySQL, and data
103+
Triggers, generated columns, exclusion + CHECK constraints, MySQL, and data
72104
migrations (column-type changes needing data transformation error out with a hint).
105+
(Partial + descending **indexes** *are* supported — see Index modeling above.)

0 commit comments

Comments
 (0)