Skip to content

Commit 835d6be

Browse files
dmealingclaude
andauthored
feat(codegen-ts): fix projection/driver-type codegen and add advisory verify pass (#80)
* fix(codegen-ts,cli): projection + driver-type codegen fixes, verify-as-teacher, model-first guidance Codegen fixes (the friction that forced agents to abandon MetaObjects and hand-roll): - queries-file: projections now emit READ-ONLY queries (findById + list from the view) — no `<Name>InsertSchema` import / create/update. Fixes TS2724 that made a declared projection fail to compile, forcing a revert to hand-rolled aggregates. - queries-file: SQLite `Db` accepts BOTH sync (better-sqlite3) and async (libsql/Turso/D1) drivers (`BaseSQLiteDatabase<"sync" | "async", unknown>`) — the pinned `<"async">` rejected better-sqlite3, the most common driver. - queries-file + callable-file: Postgres `Db` is the base `PgDatabase<PgQueryResultHKT, …>` every PG driver extends (node-postgres, postgres.js, Neon, Vercel, pglite), not just `NodePgDatabase`. All three verified by compiling generated query shapes against the real drivers. verify-as-teacher (cli): `meta verify` + `meta gen` run an advisory pass that flags hand-rolled aggregates / money-as-float / CHECK-IN enums and names the construct that models them. Warnings only — never fails the build. `--no-antipatterns` opts out. Agent-context skills: - authoring: a model-first / generate-first operating principle (declare metadata, generate persistence/DAO/API/UI; hand-write only what metadata can't express), with `meta types` vocabulary search folded in. - codegen: write-your-own-generators is now a first-class section (real apps rarely use OOTB codegen as-is) with the accurate `Generator` / `perEntity` API. - verify: run `meta verify` before calling a build done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky * no-mistakes(review): honor both anti-pattern opt-outs on meta gen and verify * no-mistakes(document): sync docs for driver-type widening and verify-as-teacher pass --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8352079 commit 835d6be

60 files changed

Lines changed: 1273 additions & 244 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: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,51 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a
1515
`package` on its root node. Files in the same `package` with the same object
1616
`name` are merged by the loader.
1717

18-
## Find the construct first — model it, don't hand-write it
18+
## The operating principle: model-first, generate-first
19+
20+
You are not hand-writing an application — you are **declaring the model it is
21+
generated from.** Persistence, data access, validation, APIs, and UI scaffolding are
22+
**derived from metadata, never authored by hand.** Model-first is the default for
23+
*every* capability; hand-writing one of these layers is an exception you must
24+
**justify**, not a convenience you reach for.
25+
26+
**This requires thinking differently.** Imperative code asks *"how do I implement
27+
this endpoint?"* Model-first asks *"what is this resource, and what is true about
28+
it?"* — and lets codegen own the *how*. **Describe WHAT, not HOW.** The metadata is
29+
the source of truth; generated code is a disposable, regenerable artifact — delete
30+
it and `meta gen` restores it identically.
31+
32+
**Why model-first wins even when hand-writing is cheaper this once — and it often
33+
is, this once:**
34+
- **Hand-writing a layer the metadata could own creates a second source of truth for
35+
one fact.** A field's type, validation, column, route, and form then change in N
36+
places and must stay consistent forever — not drift *risk*, but two sources of
37+
truth for one fact, broken by construction.
38+
- **The hand-roll saving is paid once; the consistency tax is paid on every future
39+
change.** Assume the system will grow — it always does. The metadata amortizes
40+
toward zero as the model is reused across layers and time; the hand-rolled
41+
liability compounds with every field, refactor, and language port.
42+
- **One metadata change regenerates persistence + DAO + API + UI consistently**
43+
and inherits every future generator improvement. Hand-writing opts out of all of
44+
it, permanently.
45+
46+
**Before you hand-write anything data-shaped, STOP and find the model.** The moment
47+
you reach for a hand-written query, route, validator, form, relationship, or
48+
aggregate — that is almost always **metadata you have not declared yet.** In order:
49+
1. **Search the vocabulary**`meta types <term>`, or `meta types --all
50+
<what-it-does>` to search by behavior. There are field subtypes, relationships,
51+
projections, origins, identities, sources, and attributes you may not know exist.
52+
Find the construct that models it.
53+
2. **Declare it and generate** — then *consume* the generated query/type/route;
54+
never reimplement it alongside.
55+
3. **Only if no construct can express it** — and you have actually looked —
56+
hand-write it, wired to generated types. Business algorithms, external
57+
integrations, and bespoke interactions are legitimately hand-written; CRUD,
58+
validation, finders, relationships, and derived/aggregate data are not.
59+
60+
Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
61+
call** — even when a one-off hand-write would be faster today.
1962

20-
You do **not** know the full vocabulary from memory, and it is larger and more
21-
powerful than the basics below. Before authoring anything non-trivial, and the
22-
moment you reach for hand-written data logic, **search the live metamodel**:
23-
24-
```
25-
meta types relationship # find by name
26-
meta types --all aggregate # find by WHAT IT DOES (searches descriptions)
27-
meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs
28-
```
29-
30-
**The rule:** before you hand-write any data logic — a join, a foreign key, a
31-
derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field
32-
rule, a relationship between entities, a derived read model — run `meta types` and
33-
check for a construct that **declares** it. If one exists, declare it instead. That
34-
is the entire point of MetaObjects: declared metadata is generated, typed, and
35-
regenerates on change; hand-written logic drifts and is the thing this tool exists
36-
to eliminate. When you catch yourself writing a query, a validator, or an FK by
37-
hand, stop and search the types first.
38-
39-
Two on-disk formats, one shape:
40-
41-
- **Canonical JSON** — the on-disk interchange. Every node is a single-key map
42-
whose key fuses the type and subtype.
43-
- **YAML** — the sigil-free authoring front-end. Lowered to canonical JSON at load
44-
time, so it shares the entire downstream pipeline.
45-
46-
Author in whichever fits the project. Prefer YAML for new hand-authored metadata
47-
(it's less noisy); JSON is the format conformance fixtures and tooling pin.
4863

4964
## The fused-key encoding (non-negotiable)
5065

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,63 @@ the data access too.
8686
`meta migrate` its DB view), and you **call that generated query from your
8787
route**. Declaring the projection is only half the win — *consuming* its
8888
generated query is the other half.
89-
- **Codegen is yours to extend.** A generated file carries the `@generated` header
90-
and is a normal source file: copy it and customize the copy (three-way merge
91-
preserves your edits on regen), or write your own `Generator` (the plugin
92-
interface) and add it to the `generators` array for an artifact the built-ins
93-
don't cover.
9489

9590
`meta gen --list` prints every generator by stable name; the `generators` array in
9691
`metaobjects.config.ts` is where you opt each one in or out.
9792

93+
## Write your own generators — the built-ins rarely fit an app exactly
94+
95+
The built-in generators (entity, queries, routes, form, grid, barrel) cover the
96+
common shape, but **real apps routinely need output the built-ins don't emit as-is**
97+
— a bespoke REST contract, custom DTO/response shapes, an app-specific service or
98+
repository layer, a UI the defaults don't produce. When that happens the model-first
99+
move is **not** to abandon metadata and hand-write the layer. Write a **custom
100+
generator** that reads the same metadata and emits *your* app's shape.
101+
102+
Treat this as a first-class, expected activity — not an escape hatch. A custom
103+
generator is still model-first: it derives from the metadata spine, so it
104+
regenerates on change and stays consistent across every entity — the leverage you'd
105+
forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
106+
generating *your own shape from* metadata is the point.
107+
108+
The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
109+
`{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
110+
(`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:
111+
112+
```ts
113+
import { perEntity } from "@metaobjectsdev/codegen-ts";
114+
import type { Generator } from "@metaobjectsdev/codegen-ts";
115+
116+
// One file per entity, in YOUR shape — reads the loaded metadata, emits your code.
117+
export function serviceFile(): Generator {
118+
return {
119+
name: "service-file", // kebab-case; shows in `meta gen --list`
120+
filter: (e) => e.isEntity, // which nodes it applies to
121+
generate: perEntity((entity, ctx) => ({
122+
path: `${entity.name}.service.ts`,
123+
content: renderYourService(entity.fields(), ctx), // walk the typed metadata
124+
})),
125+
};
126+
}
127+
```
128+
129+
`ctx` gives you `entities`, the `loadedRoot`, and `config`; `oncePerRun((entities,
130+
ctx) => …)` is the one-shot variant (a barrel, an app-config). Add your generator to
131+
the `generators` array in `metaobjects.config.ts` next to the built-ins — it runs in
132+
the same pass, writes under the same target rules, and carries the `@generated`
133+
header so it round-trips like any other.
134+
135+
**Close but not exact?** You don't always need a new generator — a generated file is
136+
a normal source file. Copy it and customize the copy (three-way merge preserves your
137+
edits on regen), or customize the template a built-in renders from. Reach for a
138+
custom generator when you want the change applied **consistently across every
139+
entity** (the scale win); a one-off edit when it's genuinely one file.
140+
141+
**The decision ladder:** a built-in fits → use it · close → customize the
142+
output/template · doesn't fit → write a generator that emits your shape *from the
143+
metadata* · only the genuinely un-modelable (business algorithms, external calls) is
144+
hand-written outside codegen — and it still imports the generated types.
145+
98146
## Dialects
99147

100148
Generated DB schema/DDL targets a SQL **dialect**:

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ Two more are caught structurally rather than by a command: **generated-edited**
3030
**migration-vs-metadata** (migrations are emitted *from* metadata diffs, so they
3131
can't drift by construction).
3232

33+
## Run `meta verify` before you call a build done
34+
35+
Make a bare `meta verify` the last step before you consider any MetaObjects work
36+
finished — not only in CI. Besides the drift checks below, a bare `verify` (and
37+
every `meta gen`) runs an **advisory anti-pattern pass**: it scans your authored
38+
source and flags where you hand-rolled something the metadata could model, naming
39+
the construct that replaces it — a hand-written aggregate (`AVG`/`reduce`-sum →
40+
`origin.aggregate` on an `object.projection`), money as a float (`* 100`/`toFixed`
41+
`field.currency`), a `CHECK (... IN ...)` value set (→ `field.enum`). It is
42+
advisory (never fails the build), but each line is the fix: when you see one, model
43+
it and call the generated query/field instead of keeping the hand-rolled version.
44+
This is the most common way a build ends up *declaring* a projection yet still
45+
hand-aggregating in a route — verify catches exactly that.
46+
3347
## The `verify` subverbs
3448

3549
`verify` has three drift checks. Run them in CI.

docs/features/cli.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ at `-Dmeta.verify.templateRoot`). The one goal covers BOTH Java (`codegen-spring
8484
migrate engine, ADR-0015"); an unknown mode fails listing the valid ones. (Schema `--db` remains
8585
Node-only by the ADR-0015 design — see below.)
8686

87+
## `meta gen` / `meta verify` run an advisory anti-pattern pass (Node `meta`)
88+
89+
Both `meta verify` and a real `meta gen` write run (not `--dry-run`) end with a
90+
**"verify-as-teacher"** advisory scan over your authored source. It flags a few
91+
high-precision constructs you hand-rolled that the metadata could model and names
92+
the construct that replaces it:
93+
94+
| Hand-rolled pattern | Suggested construct |
95+
|---|---|
96+
| an aggregate computed by hand (SQL `AVG`/`SUM`, a summing `.reduce(...)`) | `origin.aggregate` (on an `object.projection`) |
97+
| money as a float / hand-rolled minor units (a money-named field with `* 100`, `/ 100`, `.toFixed(2)`, `parseFloat`) | `field.currency` |
98+
| a fixed value set enforced by a SQL `CHECK (... IN (...))` | `field.enum` |
99+
100+
It is **warnings only** — it never changes the exit code (bias to under-flagging;
101+
a >15% false-positive rate is a project kill criterion). Suppress it with
102+
`--no-antipatterns` on either command, or `META_NO_ANTIPATTERNS=1` in the
103+
environment. This pass is **Node-`meta`-specific**; the C#/Java/Kotlin/Python
104+
codegen surfaces do not run it.
105+
87106
## Schema is Node-only — by design
88107

89108
No port other than the Node `meta` exposes `migrate` or `verify --db`. The C#,

docs/recipes/precommit-meta-verify.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ npx --no-install meta verify --prompts data/templates
2424
Exit 0 = clean. Non-zero = drift. The drift report goes to stdout — same shape
2525
as the CI action's PR comment.
2626

27+
`meta verify` also prints an **advisory** anti-pattern pass (hand-rolled
28+
aggregates / money-as-float / `CHECK (... IN (...))` enums, with the construct
29+
that models them). It is warnings only — it **never** changes the exit code, so it
30+
won't block a commit. Silence it with `--no-antipatterns` or `META_NO_ANTIPATTERNS=1`.
31+
2732
## Choose your hook manager
2833

2934
The MetaObjects framework is hook-manager-agnostic. Three common setups:

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

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,51 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a
1515
`package` on its root node. Files in the same `package` with the same object
1616
`name` are merged by the loader.
1717

18-
## Find the construct first — model it, don't hand-write it
18+
## The operating principle: model-first, generate-first
19+
20+
You are not hand-writing an application — you are **declaring the model it is
21+
generated from.** Persistence, data access, validation, APIs, and UI scaffolding are
22+
**derived from metadata, never authored by hand.** Model-first is the default for
23+
*every* capability; hand-writing one of these layers is an exception you must
24+
**justify**, not a convenience you reach for.
25+
26+
**This requires thinking differently.** Imperative code asks *"how do I implement
27+
this endpoint?"* Model-first asks *"what is this resource, and what is true about
28+
it?"* — and lets codegen own the *how*. **Describe WHAT, not HOW.** The metadata is
29+
the source of truth; generated code is a disposable, regenerable artifact — delete
30+
it and `meta gen` restores it identically.
31+
32+
**Why model-first wins even when hand-writing is cheaper this once — and it often
33+
is, this once:**
34+
- **Hand-writing a layer the metadata could own creates a second source of truth for
35+
one fact.** A field's type, validation, column, route, and form then change in N
36+
places and must stay consistent forever — not drift *risk*, but two sources of
37+
truth for one fact, broken by construction.
38+
- **The hand-roll saving is paid once; the consistency tax is paid on every future
39+
change.** Assume the system will grow — it always does. The metadata amortizes
40+
toward zero as the model is reused across layers and time; the hand-rolled
41+
liability compounds with every field, refactor, and language port.
42+
- **One metadata change regenerates persistence + DAO + API + UI consistently**
43+
and inherits every future generator improvement. Hand-writing opts out of all of
44+
it, permanently.
45+
46+
**Before you hand-write anything data-shaped, STOP and find the model.** The moment
47+
you reach for a hand-written query, route, validator, form, relationship, or
48+
aggregate — that is almost always **metadata you have not declared yet.** In order:
49+
1. **Search the vocabulary**`meta types <term>`, or `meta types --all
50+
<what-it-does>` to search by behavior. There are field subtypes, relationships,
51+
projections, origins, identities, sources, and attributes you may not know exist.
52+
Find the construct that models it.
53+
2. **Declare it and generate** — then *consume* the generated query/type/route;
54+
never reimplement it alongside.
55+
3. **Only if no construct can express it** — and you have actually looked —
56+
hand-write it, wired to generated types. Business algorithms, external
57+
integrations, and bespoke interactions are legitimately hand-written; CRUD,
58+
validation, finders, relationships, and derived/aggregate data are not.
59+
60+
Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
61+
call** — even when a one-off hand-write would be faster today.
1962

20-
You do **not** know the full vocabulary from memory, and it is larger and more
21-
powerful than the basics below. Before authoring anything non-trivial, and the
22-
moment you reach for hand-written data logic, **search the live metamodel**:
23-
24-
```
25-
meta types relationship # find by name
26-
meta types --all aggregate # find by WHAT IT DOES (searches descriptions)
27-
meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs
28-
```
29-
30-
**The rule:** before you hand-write any data logic — a join, a foreign key, a
31-
derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field
32-
rule, a relationship between entities, a derived read model — run `meta types` and
33-
check for a construct that **declares** it. If one exists, declare it instead. That
34-
is the entire point of MetaObjects: declared metadata is generated, typed, and
35-
regenerates on change; hand-written logic drifts and is the thing this tool exists
36-
to eliminate. When you catch yourself writing a query, a validator, or an FK by
37-
hand, stop and search the types first.
38-
39-
Two on-disk formats, one shape:
40-
41-
- **Canonical JSON** — the on-disk interchange. Every node is a single-key map
42-
whose key fuses the type and subtype.
43-
- **YAML** — the sigil-free authoring front-end. Lowered to canonical JSON at load
44-
time, so it shares the entire downstream pipeline.
45-
46-
Author in whichever fits the project. Prefer YAML for new hand-authored metadata
47-
(it's less noisy); JSON is the format conformance fixtures and tooling pin.
4863

4964
## The fused-key encoding (non-negotiable)
5065

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,63 @@ the data access too.
8686
`meta migrate` its DB view), and you **call that generated query from your
8787
route**. Declaring the projection is only half the win — *consuming* its
8888
generated query is the other half.
89-
- **Codegen is yours to extend.** A generated file carries the `@generated` header
90-
and is a normal source file: copy it and customize the copy (three-way merge
91-
preserves your edits on regen), or write your own `Generator` (the plugin
92-
interface) and add it to the `generators` array for an artifact the built-ins
93-
don't cover.
9489

9590
`meta gen --list` prints every generator by stable name; the `generators` array in
9691
`metaobjects.config.ts` is where you opt each one in or out.
9792

93+
## Write your own generators — the built-ins rarely fit an app exactly
94+
95+
The built-in generators (entity, queries, routes, form, grid, barrel) cover the
96+
common shape, but **real apps routinely need output the built-ins don't emit as-is**
97+
— a bespoke REST contract, custom DTO/response shapes, an app-specific service or
98+
repository layer, a UI the defaults don't produce. When that happens the model-first
99+
move is **not** to abandon metadata and hand-write the layer. Write a **custom
100+
generator** that reads the same metadata and emits *your* app's shape.
101+
102+
Treat this as a first-class, expected activity — not an escape hatch. A custom
103+
generator is still model-first: it derives from the metadata spine, so it
104+
regenerates on change and stays consistent across every entity — the leverage you'd
105+
forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
106+
generating *your own shape from* metadata is the point.
107+
108+
The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
109+
`{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
110+
(`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:
111+
112+
```ts
113+
import { perEntity } from "@metaobjectsdev/codegen-ts";
114+
import type { Generator } from "@metaobjectsdev/codegen-ts";
115+
116+
// One file per entity, in YOUR shape — reads the loaded metadata, emits your code.
117+
export function serviceFile(): Generator {
118+
return {
119+
name: "service-file", // kebab-case; shows in `meta gen --list`
120+
filter: (e) => e.isEntity, // which nodes it applies to
121+
generate: perEntity((entity, ctx) => ({
122+
path: `${entity.name}.service.ts`,
123+
content: renderYourService(entity.fields(), ctx), // walk the typed metadata
124+
})),
125+
};
126+
}
127+
```
128+
129+
`ctx` gives you `entities`, the `loadedRoot`, and `config`; `oncePerRun((entities,
130+
ctx) => …)` is the one-shot variant (a barrel, an app-config). Add your generator to
131+
the `generators` array in `metaobjects.config.ts` next to the built-ins — it runs in
132+
the same pass, writes under the same target rules, and carries the `@generated`
133+
header so it round-trips like any other.
134+
135+
**Close but not exact?** You don't always need a new generator — a generated file is
136+
a normal source file. Copy it and customize the copy (three-way merge preserves your
137+
edits on regen), or customize the template a built-in renders from. Reach for a
138+
custom generator when you want the change applied **consistently across every
139+
entity** (the scale win); a one-off edit when it's genuinely one file.
140+
141+
**The decision ladder:** a built-in fits → use it · close → customize the
142+
output/template · doesn't fit → write a generator that emits your shape *from the
143+
metadata* · only the genuinely un-modelable (business algorithms, external calls) is
144+
hand-written outside codegen — and it still imports the generated types.
145+
98146
## Dialects
99147

100148
Generated DB schema/DDL targets a SQL **dialect**:

0 commit comments

Comments
 (0)