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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,41 @@ loaded before stops loading. This vocabulary ships in 1.3.0 with `metamodelVersi
one over measures as a `number` member (one reading a dimension is refused). Loader rule M11,
report rule R13 and a conformance fixture per refusal, in every loader port.

- **`meta init --skills-dir <dir>` and `meta agent-docs --skills-dir <dir>`: install the skills
for any coding assistant.** The `metaobjects-*` skills went only to `.claude/skills/`, which
Claude Code reads. `--skills-dir .agents/skills` (or any project-relative directory) puts them
where another assistant looks; the choice is recorded in `.metaobjects/.agent-context.json`,
so a later `--refresh-docs` keeps it, and moving the directory prunes the untouched copies at
the old one. The always-on file names the directory, so an assistant that does not load
skills opens `<dir>/<skill>/SKILL.md` by path. A directory outside the project is refused.
- **[What differs by language](docs/features/language-differences.md).** One page lists what
the same model does differently per port: that schema migrations and database checks need
the Node `meta` tool in every language, the list default and cap, the validation error body,
filter extras, field names in code, generated data access and hand-edit handling. Every port
page now says up front that migrations and `verify --db` run in Node, and the README's
getting-started path says it for Java, Kotlin, C# and Python teams.

### Changed

- **The always-on agent file works with any coding assistant, carries the REST conventions,
and is written once.** `.metaobjects/CLAUDE.md`, an identical copy of `.metaobjects/AGENTS.md`
that nothing imported, is no longer written; a refresh removes an untouched one and names a
hand-edited one. The text no longer tells the reader to use a root `CLAUDE.md` only, and
`--wire-root` on a project with neither root file now creates `AGENTS.md` as well as
`CLAUDE.md`, each with a plain pointer line beside the `@` import. A first init on a project
that has only a root `CLAUDE.md` creates `AGENTS.md` too (assistants other than Claude Code
read no `@` import out of `CLAUDE.md`); a refresh creates no root file and instead prints one
advisory line naming the missing `AGENTS.md`. A new "Calling the
generated REST API" block gives the filter form (`?filter[status][eq]=active`, never
`?status=active`) with its nine operators, sorting and paging (including each port's default
when `limit` is omitted), and the error bodies. To offset it, the Java and Kotlin generator
class lists, the TypeScript catalog pipeline and the prompt and report rules moved behind the
skills they already pointed to. Measured on the agent-context conformance stacks (C#,
Java + React, Java + Kotlin + React + TanStack, Python, TypeScript + React + TanStack), the
file an assistant loads each session is 5,580 / 6,045 / 6,705 / 5,585 / 6,572 bytes, from
4,978 / 5,767 / 6,731 / 4,983 / 6,207: the REST block adds more than the moved detail takes
out. The always-on bytes written to disk fall by 44 to 50% because the copy is gone. The C#,
Java and Python staleness notices no longer name `.claude/skills`.
- **Breaking, TypeScript: a value object is never renamed; one claim registry refuses real
collisions** (amending ADR-0044 and ADR-0056). `acme::alpha::Note` is declared as `Note` in its
own module; the `AcmeAlphaNote` derived name, `assignEmittedNames` and its
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ gated by `meta verify --codegen`, which is what keeps output from drifting. See
[write your own generator](docs/recipes/write-your-own-generator.md).

Java, Kotlin, C# and Python start the same way from their own package: see
[`docs/ports/`](docs/ports/).
[`docs/ports/`](docs/ports/). **Their schema migrations and database checks still run in the
Node `meta` tool** (`npx meta migrate`, `npx meta verify --db`), so those teams need Node where
migrations are written or applied. Where the ports behave differently (list defaults, error
bodies, field names, data access) is listed in
[What differs by language](docs/features/language-differences.md).

The metamodel is the **durable spine**; generated code is the **disposable
artifact**. Substrate is local-first: typed metadata lives in your repo as YAML or
Expand Down Expand Up @@ -341,6 +345,11 @@ metaobjects/
| C# | `dotnet tool install --global MetaObjects.Cli` → [`docs/ports/csharp.md`](docs/ports/csharp.md) |
| Python | `pip install metaobjects` → [`docs/ports/python.md`](docs/ports/python.md) |

Every language also needs the Node `meta` CLI for the schema: `npm i -D @metaobjectsdev/cli`,
then `npx meta init --config-only` in a Maven-, .NET- or pip-rooted project, and
`npx meta migrate` / `npx meta verify --db` from there. The per-language differences are in
[What differs by language](docs/features/language-differences.md).

## Cross-language conformance

Every port runs against the same fixture corpora at [`fixtures/`](fixtures/).
Expand Down
12 changes: 7 additions & 5 deletions agent-context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

This tree is the single source the assembler (`@metaobjectsdev/sdk`,
`src/agent-context/`) turns into the files scaffolded into a consumer project:
the slim always-on Markdown (`.metaobjects/AGENTS.md` + `CLAUDE.md`) and the six
`metaobjects-*` Claude skills (each a universal `SKILL.md` plus the
`references/<token>.md` fragments matching the project's resolved stack).
the slim always-on Markdown (`.metaobjects/AGENTS.md`, imported by the project's root
`AGENTS.md` / `CLAUDE.md`) and the six `metaobjects-*` skills (each a universal `SKILL.md` plus
the `references/<token>.md` fragments matching the project's resolved stack). The skills
install under `.claude/skills/` unless `--skills-dir` names another directory (such as
`.agents/skills/`); the always-on text names the directory through `{{skillsDir}}`.

- `servers/<lang>.meta.json` — per-server install, codegen command and the always-on's verify step (`verifyStep`).
- `templates/always-on.md.mustache` — the slim always-on body: a working loop first (`{{stackLine}}`, `{{stackLoops}}`, `{{verifySteps}}`), then the rules. The full principles it shortens live in `metaobjects-codegen/references/principles.md`.
- `templates/always-on.md.mustache` — the slim always-on body: a working loop first (`{{stackLine}}`, `{{stackLoops}}`, `{{verifySteps}}`), the REST conventions agents trip on, then the rules and the skill pointers (`{{skillsDir}}`). The full principles it shortens live in `metaobjects-codegen/references/principles.md`.
- `templates/loops/<server>.md`, `templates/loops/client.md` — the generate steps of that loop, one per declared server (and one for a web client), concatenated by the assembler.
- `skills/<skill>/SKILL.md` — universal skill body.
- `skills/<skill>/references/<token>.md` — language/framework fragment; installed iff `<token>` is in the stack (`Stack.tokens`). The requirements pages are universal topic files: they install in every stack, because whether a project declares `requirement.*` nodes changes after `meta init` runs.
Expand All @@ -18,7 +20,7 @@ The **scaffolded** set is exactly the six skills in the SDK's `SKILL_NAMES`
(`src/agent-context/types.ts`) — the assembler emits only those. `skills/`
additionally holds **`metaobjects-fit-assessment`**, a *pre-adoption* tool that is
deliberately **not** in `SKILL_NAMES` and so is never scaffolded (a not-yet-adopted
target has no `.claude/skills/metaobjects-*` to receive it); it carries
target has no installed `metaobjects-*` skills to receive it); it carries
`scaffold: false` under `metadata:` in its front-matter as the human marker (no code reads it). Its design + retro-test
validation live in `docs/superpowers/specs/2026-07-12-metaobjects-fit-assessment-design.md`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ reasons. Read it when a rule there does not settle the case in front of you.
## Principles

- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V<n>__`/`U<n>__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V<n>__`/`U<n>__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `AGENTS.md` or `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db <url>` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current.
Expand Down
63 changes: 37 additions & 26 deletions agent-context/templates/always-on.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
> {{stackLine}}
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
> **root `CLAUDE.md`**, not here — this file is regenerated.
> root `AGENTS.md` or `CLAUDE.md`, not here: this file is regenerated.

The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.

## Start here: the whole loop, in order

{{stackLoops}}
### Schema (every stack: the Node `meta` CLI owns it)
### Schema (every stack: the Node `meta` CLI owns it, so Java, Kotlin, C# and Python projects need Node too)

Never hand-write DDL, and never `ALTER` a live database by hand.

Expand All @@ -21,16 +21,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand.
- At boot or deploy: `npx meta migrate apply-pending --db <url>`.
- A later change: edit the metadata, regenerate, then
`npx meta migrate --dialect <dialect> --slug <name>` (offline, from the committed snapshot).
For a renamed field add `--rename-column <table>.<old_column>=<new_column>` (a renamed object:
`--rename-table <old>=<new>`), which keeps the data. A destructive change is refused until you
allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
`--allow drop-column` only when the data should go.
A renamed field needs `--rename-column <table>.<old_column>=<new_column>` (an object:
`--rename-table <old>=<new>`) to keep its data. A destructive change is refused until you
allow its kind by name: `--allow drop-check` for a changed enum, `--allow drop-column` only
when the data should go.

### Requirements, when the metadata declares `requirement.*` nodes

They list what the software must do. A commit that changes behaviour updates them in the same
commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
`meta verify` checks that links resolve, not that a status is true.
A commit that changes behaviour updates them in the same commit: `@status: live` once a claim
is built (never before), a new node for new behaviour.

### Before you call it done

Expand All @@ -39,6 +38,21 @@ commit: `@status: live` once a claim is built (never before), a new node for new
Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.

## Calling the generated REST API

When the project serves the generated routes (any port):

- Filter as `?filter[<field>][<op>]=<value>`: `?filter[status][eq]=active`, never
`?status=active` (TypeScript refuses that with 400; the other ports ignore it). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like`
(case-sensitive, `%` and `_`), `gt`, `gte`, `lt`, `lte`. Filters AND; only `@filterable`
fields filter.
- `?sort=<field>:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`.
Always send `limit`: without it TypeScript and C# return every row, the other ports 50.
- `PUT` is the same partial update as `PATCH`.
- Errors are `{"error": "<code>"}`: 400 for validation (only TypeScript adds `issues`) and for
a bad filter or sort (`invalid_filter_field`, `_op`, `_value`, `invalid_sort`, each naming
`field`), 404 `not_found`, 409 for a unique or foreign-key conflict.

## Rules

- What the metadata describes is generated, never hand-written (tables, types, validators,
Expand All @@ -47,27 +61,24 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and
broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
template, the data in a payload, a structured reply read through its generated extractor
(`metaobjects-prompts`).
- A grouped total, dashboard number or read-only report view (per client, per month) is an
`object.report` over `dimension.*` / `measure.*` (`metaobjects-authoring`,
`references/declaring-reports.md`). A hand-written `@sql` view body is the last resort.
- An LLM prompt in a project that declares `template.prompt` is a declared template
(`metaobjects-prompts`); a grouped total or report view is an `object.report`
(`metaobjects-authoring`). Open the skill before writing either by hand.
- Never invent metadata vocabulary. Search it: `npx meta types <term>`, and
`npx meta types <type>.<subType> --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
unique within a node, and package paths use `::`.

## Going deeper

The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
generator and the full principles (`metaobjects-codegen`), REST routes and the web client
(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
its task comes up; it names the one reference page to open.

This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
file you edited (only `--force` does, so never `--force` one with local edits): it writes
`<path>.new` beside it. Merge that into `<path>`, keeping your edits, and delete the `.new`.
The loop above needs no skill. For less common work load a `metaobjects-*` skill by name, or open
`{{skillsDir}}/<skill>/SKILL.md` if your assistant does not load skills: `metaobjects-authoring`
(unusual shapes, reports, adopting existing code), `metaobjects-codegen` (owning or writing a
generator), `metaobjects-runtime-ui` (REST routes, the web client), `metaobjects-prompts` (LLM
prompts), `metaobjects-verify` (migration edge cases, drift, upgrades), `metaobjects-audit` (an
adoption review). Each names the one reference page to open.

`meta init --refresh-docs` regenerates this file and the skills, so keep project guidance in
the root `AGENTS.md` or `CLAUDE.md` (or copy a skill to a new name). A refresh never overwrites a
file you edited; it writes `<path>.new` beside it, which you merge into `<path>` and delete.
Never `--force` a refresh over local edits.
9 changes: 3 additions & 6 deletions agent-context/templates/loops/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

1. **Config.** The `metaobjects-maven-plugin`'s `<generators>` in `pom.xml` is the generator
list, one `<generator><classname>` each, under `<loader><sourceDir>` pointing at the metadata.
Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`,
`SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and
`SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a
list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator`
writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The
full table is in the `metaobjects-codegen` skill's `references/java.md`.
Nothing is generated until it is listed; the build names any generator a listed one needs.
Which classes to list (the Spring REST tier, requirement tests, API docs) is in the
`metaobjects-codegen` skill's `references/java.md`.
2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames=<name> -Dport=java`
copies a generator to change what it emits.
Loading
Loading