Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions agent-context/skills/metaobjects-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,51 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a
`package` on its root node. Files in the same `package` with the same object
`name` are merged by the loader.

## Find the construct first — model it, don't hand-write it
## The operating principle: model-first, generate-first

You are not hand-writing an application — you are **declaring the model it is
generated from.** Persistence, data access, validation, APIs, and UI scaffolding are
**derived from metadata, never authored by hand.** Model-first is the default for
*every* capability; hand-writing one of these layers is an exception you must
**justify**, not a convenience you reach for.

**This requires thinking differently.** Imperative code asks *"how do I implement
this endpoint?"* Model-first asks *"what is this resource, and what is true about
it?"* — and lets codegen own the *how*. **Describe WHAT, not HOW.** The metadata is
the source of truth; generated code is a disposable, regenerable artifact — delete
it and `meta gen` restores it identically.

**Why model-first wins even when hand-writing is cheaper this once — and it often
is, this once:**
- **Hand-writing a layer the metadata could own creates a second source of truth for
one fact.** A field's type, validation, column, route, and form then change in N
places and must stay consistent forever — not drift *risk*, but two sources of
truth for one fact, broken by construction.
- **The hand-roll saving is paid once; the consistency tax is paid on every future
change.** Assume the system will grow — it always does. The metadata amortizes
toward zero as the model is reused across layers and time; the hand-rolled
liability compounds with every field, refactor, and language port.
- **One metadata change regenerates persistence + DAO + API + UI consistently** —
and inherits every future generator improvement. Hand-writing opts out of all of
it, permanently.

**Before you hand-write anything data-shaped, STOP and find the model.** The moment
you reach for a hand-written query, route, validator, form, relationship, or
aggregate — that is almost always **metadata you have not declared yet.** In order:
1. **Search the vocabulary** — `meta types <term>`, or `meta types --all
<what-it-does>` to search by behavior. There are field subtypes, relationships,
projections, origins, identities, sources, and attributes you may not know exist.
Find the construct that models it.
2. **Declare it and generate** — then *consume* the generated query/type/route;
never reimplement it alongside.
3. **Only if no construct can express it** — and you have actually looked —
hand-write it, wired to generated types. Business algorithms, external
integrations, and bespoke interactions are legitimately hand-written; CRUD,
validation, finders, relationships, and derived/aggregate data are not.

Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
call** — even when a one-off hand-write would be faster today.

You do **not** know the full vocabulary from memory, and it is larger and more
powerful than the basics below. Before authoring anything non-trivial, and the
moment you reach for hand-written data logic, **search the live metamodel**:

```
meta types relationship # find by name
meta types --all aggregate # find by WHAT IT DOES (searches descriptions)
meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs
```

**The rule:** before you hand-write any data logic — a join, a foreign key, a
derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field
rule, a relationship between entities, a derived read model — run `meta types` and
check for a construct that **declares** it. If one exists, declare it instead. That
is the entire point of MetaObjects: declared metadata is generated, typed, and
regenerates on change; hand-written logic drifts and is the thing this tool exists
to eliminate. When you catch yourself writing a query, a validator, or an FK by
hand, stop and search the types first.

Two on-disk formats, one shape:

- **Canonical JSON** — the on-disk interchange. Every node is a single-key map
whose key fuses the type and subtype.
- **YAML** — the sigil-free authoring front-end. Lowered to canonical JSON at load
time, so it shares the entire downstream pipeline.

Author in whichever fits the project. Prefer YAML for new hand-authored metadata
(it's less noisy); JSON is the format conformance fixtures and tooling pin.

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

Expand Down
58 changes: 53 additions & 5 deletions agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,63 @@ the data access too.
`meta migrate` its DB view), and you **call that generated query from your
route**. Declaring the projection is only half the win — *consuming* its
generated query is the other half.
- **Codegen is yours to extend.** A generated file carries the `@generated` header
and is a normal source file: copy it and customize the copy (three-way merge
preserves your edits on regen), or write your own `Generator` (the plugin
interface) and add it to the `generators` array for an artifact the built-ins
don't cover.

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

## Write your own generators — the built-ins rarely fit an app exactly

The built-in generators (entity, queries, routes, form, grid, barrel) cover the
common shape, but **real apps routinely need output the built-ins don't emit as-is**
— a bespoke REST contract, custom DTO/response shapes, an app-specific service or
repository layer, a UI the defaults don't produce. When that happens the model-first
move is **not** to abandon metadata and hand-write the layer. Write a **custom
generator** that reads the same metadata and emits *your* app's shape.

Treat this as a first-class, expected activity — not an escape hatch. A custom
generator is still model-first: it derives from the metadata spine, so it
regenerates on change and stays consistent across every entity — the leverage you'd
forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
generating *your own shape from* metadata is the point.

The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
`{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
(`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:

```ts
import { perEntity } from "@metaobjectsdev/codegen-ts";
import type { Generator } from "@metaobjectsdev/codegen-ts";

// One file per entity, in YOUR shape — reads the loaded metadata, emits your code.
export function serviceFile(): Generator {
return {
name: "service-file", // kebab-case; shows in `meta gen --list`
filter: (e) => e.isEntity, // which nodes it applies to
generate: perEntity((entity, ctx) => ({
path: `${entity.name}.service.ts`,
content: renderYourService(entity.fields(), ctx), // walk the typed metadata
})),
};
}
```

`ctx` gives you `entities`, the `loadedRoot`, and `config`; `oncePerRun((entities,
ctx) => …)` is the one-shot variant (a barrel, an app-config). Add your generator to
the `generators` array in `metaobjects.config.ts` next to the built-ins — it runs in
the same pass, writes under the same target rules, and carries the `@generated`
header so it round-trips like any other.

**Close but not exact?** You don't always need a new generator — a generated file is
a normal source file. Copy it and customize the copy (three-way merge preserves your
edits on regen), or customize the template a built-in renders from. Reach for a
custom generator when you want the change applied **consistently across every
entity** (the scale win); a one-off edit when it's genuinely one file.

**The decision ladder:** a built-in fits → use it · close → customize the
output/template · doesn't fit → write a generator that emits your shape *from the
metadata* · only the genuinely un-modelable (business algorithms, external calls) is
hand-written outside codegen — and it still imports the generated types.

## Dialects

Generated DB schema/DDL targets a SQL **dialect**:
Expand Down
14 changes: 14 additions & 0 deletions agent-context/skills/metaobjects-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ Two more are caught structurally rather than by a command: **generated-edited**
**migration-vs-metadata** (migrations are emitted *from* metadata diffs, so they
can't drift by construction).

## Run `meta verify` before you call a build done

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

## The `verify` subverbs

`verify` has three drift checks. Run them in CI.
Expand Down
19 changes: 19 additions & 0 deletions docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ at `-Dmeta.verify.templateRoot`). The one goal covers BOTH Java (`codegen-spring
migrate engine, ADR-0015"); an unknown mode fails listing the valid ones. (Schema `--db` remains
Node-only by the ADR-0015 design — see below.)

## `meta gen` / `meta verify` run an advisory anti-pattern pass (Node `meta`)

Both `meta verify` and a real `meta gen` write run (not `--dry-run`) end with a
**"verify-as-teacher"** advisory scan over your authored source. It flags a few
high-precision constructs you hand-rolled that the metadata could model and names
the construct that replaces it:

| Hand-rolled pattern | Suggested construct |
|---|---|
| an aggregate computed by hand (SQL `AVG`/`SUM`, a summing `.reduce(...)`) | `origin.aggregate` (on an `object.projection`) |
| money as a float / hand-rolled minor units (a money-named field with `* 100`, `/ 100`, `.toFixed(2)`, `parseFloat`) | `field.currency` |
| a fixed value set enforced by a SQL `CHECK (... IN (...))` | `field.enum` |

It is **warnings only** — it never changes the exit code (bias to under-flagging;
a >15% false-positive rate is a project kill criterion). Suppress it with
`--no-antipatterns` on either command, or `META_NO_ANTIPATTERNS=1` in the
environment. This pass is **Node-`meta`-specific**; the C#/Java/Kotlin/Python
codegen surfaces do not run it.

## Schema is Node-only — by design

No port other than the Node `meta` exposes `migrate` or `verify --db`. The C#,
Expand Down
5 changes: 5 additions & 0 deletions docs/recipes/precommit-meta-verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ npx --no-install meta verify --prompts data/templates
Exit 0 = clean. Non-zero = drift. The drift report goes to stdout — same shape
as the CI action's PR comment.

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

## Choose your hook manager

The MetaObjects framework is hook-manager-agnostic. Three common setups:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,51 @@ concept (`meta.commerce.json`, `meta.users.yaml`, …). Each file declares a
`package` on its root node. Files in the same `package` with the same object
`name` are merged by the loader.

## Find the construct first — model it, don't hand-write it
## The operating principle: model-first, generate-first

You are not hand-writing an application — you are **declaring the model it is
generated from.** Persistence, data access, validation, APIs, and UI scaffolding are
**derived from metadata, never authored by hand.** Model-first is the default for
*every* capability; hand-writing one of these layers is an exception you must
**justify**, not a convenience you reach for.

**This requires thinking differently.** Imperative code asks *"how do I implement
this endpoint?"* Model-first asks *"what is this resource, and what is true about
it?"* — and lets codegen own the *how*. **Describe WHAT, not HOW.** The metadata is
the source of truth; generated code is a disposable, regenerable artifact — delete
it and `meta gen` restores it identically.

**Why model-first wins even when hand-writing is cheaper this once — and it often
is, this once:**
- **Hand-writing a layer the metadata could own creates a second source of truth for
one fact.** A field's type, validation, column, route, and form then change in N
places and must stay consistent forever — not drift *risk*, but two sources of
truth for one fact, broken by construction.
- **The hand-roll saving is paid once; the consistency tax is paid on every future
change.** Assume the system will grow — it always does. The metadata amortizes
toward zero as the model is reused across layers and time; the hand-rolled
liability compounds with every field, refactor, and language port.
- **One metadata change regenerates persistence + DAO + API + UI consistently** —
and inherits every future generator improvement. Hand-writing opts out of all of
it, permanently.

**Before you hand-write anything data-shaped, STOP and find the model.** The moment
you reach for a hand-written query, route, validator, form, relationship, or
aggregate — that is almost always **metadata you have not declared yet.** In order:
1. **Search the vocabulary** — `meta types <term>`, or `meta types --all
<what-it-does>` to search by behavior. There are field subtypes, relationships,
projections, origins, identities, sources, and attributes you may not know exist.
Find the construct that models it.
2. **Declare it and generate** — then *consume* the generated query/type/route;
never reimplement it alongside.
3. **Only if no construct can express it** — and you have actually looked —
hand-write it, wired to generated types. Business algorithms, external
integrations, and bespoke interactions are legitimately hand-written; CRUD,
validation, finders, relationships, and derived/aggregate data are not.

Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
call** — even when a one-off hand-write would be faster today.

You do **not** know the full vocabulary from memory, and it is larger and more
powerful than the basics below. Before authoring anything non-trivial, and the
moment you reach for hand-written data logic, **search the live metamodel**:

```
meta types relationship # find by name
meta types --all aggregate # find by WHAT IT DOES (searches descriptions)
meta types origin.aggregate --detail # one construct: description, when to use it, valid @attrs
```

**The rule:** before you hand-write any data logic — a join, a foreign key, a
derived/aggregate value (count/sum/avg), a uniqueness / format / range / cross-field
rule, a relationship between entities, a derived read model — run `meta types` and
check for a construct that **declares** it. If one exists, declare it instead. That
is the entire point of MetaObjects: declared metadata is generated, typed, and
regenerates on change; hand-written logic drifts and is the thing this tool exists
to eliminate. When you catch yourself writing a query, a validator, or an FK by
hand, stop and search the types first.

Two on-disk formats, one shape:

- **Canonical JSON** — the on-disk interchange. Every node is a single-key map
whose key fuses the type and subtype.
- **YAML** — the sigil-free authoring front-end. Lowered to canonical JSON at load
time, so it shares the entire downstream pipeline.

Author in whichever fits the project. Prefer YAML for new hand-authored metadata
(it's less noisy); JSON is the format conformance fixtures and tooling pin.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,63 @@ the data access too.
`meta migrate` its DB view), and you **call that generated query from your
route**. Declaring the projection is only half the win — *consuming* its
generated query is the other half.
- **Codegen is yours to extend.** A generated file carries the `@generated` header
and is a normal source file: copy it and customize the copy (three-way merge
preserves your edits on regen), or write your own `Generator` (the plugin
interface) and add it to the `generators` array for an artifact the built-ins
don't cover.

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

## Write your own generators — the built-ins rarely fit an app exactly

The built-in generators (entity, queries, routes, form, grid, barrel) cover the
common shape, but **real apps routinely need output the built-ins don't emit as-is**
— a bespoke REST contract, custom DTO/response shapes, an app-specific service or
repository layer, a UI the defaults don't produce. When that happens the model-first
move is **not** to abandon metadata and hand-write the layer. Write a **custom
generator** that reads the same metadata and emits *your* app's shape.

Treat this as a first-class, expected activity — not an escape hatch. A custom
generator is still model-first: it derives from the metadata spine, so it
regenerates on change and stays consistent across every entity — the leverage you'd
forfeit by hand-writing. Hand-rolling *away from* metadata is the anti-pattern;
generating *your own shape from* metadata is the point.

The plugin interface is small (`@metaobjectsdev/codegen-ts`): a `Generator` is
`{ name, filter?, generate }`, where `generate(ctx)` returns `EmittedFile[]`
(`{ path, content }`). `perEntity` / `oncePerRun` wrap the common cases:

```ts
import { perEntity } from "@metaobjectsdev/codegen-ts";
import type { Generator } from "@metaobjectsdev/codegen-ts";

// One file per entity, in YOUR shape — reads the loaded metadata, emits your code.
export function serviceFile(): Generator {
return {
name: "service-file", // kebab-case; shows in `meta gen --list`
filter: (e) => e.isEntity, // which nodes it applies to
generate: perEntity((entity, ctx) => ({
path: `${entity.name}.service.ts`,
content: renderYourService(entity.fields(), ctx), // walk the typed metadata
})),
};
}
```

`ctx` gives you `entities`, the `loadedRoot`, and `config`; `oncePerRun((entities,
ctx) => …)` is the one-shot variant (a barrel, an app-config). Add your generator to
the `generators` array in `metaobjects.config.ts` next to the built-ins — it runs in
the same pass, writes under the same target rules, and carries the `@generated`
header so it round-trips like any other.

**Close but not exact?** You don't always need a new generator — a generated file is
a normal source file. Copy it and customize the copy (three-way merge preserves your
edits on regen), or customize the template a built-in renders from. Reach for a
custom generator when you want the change applied **consistently across every
entity** (the scale win); a one-off edit when it's genuinely one file.

**The decision ladder:** a built-in fits → use it · close → customize the
output/template · doesn't fit → write a generator that emits your shape *from the
metadata* · only the genuinely un-modelable (business algorithms, external calls) is
hand-written outside codegen — and it still imports the generated types.

## Dialects

Generated DB schema/DDL targets a SQL **dialect**:
Expand Down
Loading
Loading