From cd48da17a79fa9795b6acc78746d344c16116add Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 11:03:17 -0500 Subject: [PATCH 1/7] docs+agent-context: language-differences page, Node-for-schema callouts, assistant-neutral agent file Docs truth pass (1.3.0 plan PR 8): - Every port page and the README getting-started path now say that schema migrations and database checks run in the Node `meta` tool. - New docs/features/language-differences.md: one table of what differs by port (list default and cap, validation body, filter errors, field naming, data access, migrations, hand edits), linked from the port pages. - Docs that still described .metaobjects/CLAUDE.md are corrected. Assistant-neutral agent context (1.3.0 plan PR 36): - `meta init --skills-dir ` / `meta agent-docs --skills-dir` installs skills for any assistant (e.g. .agents/skills); the choice is recorded in the manifest and kept on refresh, and old copies are pruned on a move. - The duplicate .metaobjects/CLAUDE.md is no longer written (pruned on refresh); root wiring creates AGENTS.md and CLAUDE.md importing .metaobjects/AGENTS.md. - The always-on text gains a "Calling the generated REST API" block (filter[f][op]=v, operators, sort, paging, error shapes) and drops stack-specific catalogs in favour of skill pointers. - Staleness nudges in C#, Java and Python use the neutral wording. --- CHANGELOG.md | 30 +++++ README.md | 11 +- agent-context/README.md | 12 +- agent-context/templates/always-on.md.mustache | 64 +++++----- agent-context/templates/loops/java.md | 9 +- agent-context/templates/loops/kotlin.md | 10 +- agent-context/templates/loops/typescript.md | 16 +-- docs/README.md | 2 + docs/features/api-contract.md | 5 +- docs/features/cli.md | 5 +- docs/features/language-differences.md | 79 +++++++++++++ docs/llms/llms-full.txt | 8 +- docs/llms/llms.txt | 10 +- docs/ports/csharp.md | 2 + docs/ports/java.md | 2 + docs/ports/kotlin.md | 2 + docs/ports/python.md | 2 + docs/ports/typescript.md | 2 + .../csharp/expected/.metaobjects/AGENTS.md | 64 +++++----- .../csharp/expected/.metaobjects/CLAUDE.md | 91 --------------- .../expected/.metaobjects/AGENTS.md | 83 ++++++------- .../expected/.metaobjects/CLAUDE.md | 110 ------------------ .../expected/.metaobjects/AGENTS.md | 73 +++++++----- .../expected/.metaobjects/CLAUDE.md | 96 --------------- .../python/expected/.metaobjects/AGENTS.md | 64 +++++----- .../python/expected/.metaobjects/CLAUDE.md | 92 --------------- .../expected/.metaobjects/AGENTS.md | 80 +++++++------ .../expected/.metaobjects/CLAUDE.md | 109 ----------------- .../AgentContext/AgentContextScaffold.cs | 2 +- server/csharp/README.md | 7 +- server/java/README.md | 7 +- .../agentcontext/AgentContextScaffold.java | 2 +- .../AgentContextStalenessTest.java | 2 +- server/python/README.md | 7 +- .../src/metaobjects/agent_context/scaffold.py | 3 +- server/typescript/packages/cli/README.md | 8 +- .../packages/cli/src/commands/gen.ts | 2 +- .../packages/cli/src/commands/init.ts | 53 ++++++--- .../packages/cli/src/commands/verify.ts | 2 +- server/typescript/packages/cli/src/index.ts | 27 +++-- .../typescript/packages/cli/src/lib/args.ts | 7 ++ .../cli/test/__snapshots__/cli.test.ts.snap | 4 +- .../typescript/packages/cli/test/cli.test.ts | 2 +- .../cli/test/unit/init-agent-context.test.ts | 57 ++++++++- .../cli/test/unit/init-refresh-docs.test.ts | 13 +-- server/typescript/packages/forge/README.md | 4 +- server/typescript/packages/sdk/README.md | 5 +- .../sdk/src/agent-context/assemble.ts | 25 ++-- .../sdk/src/agent-context/scaffold.ts | 23 +++- .../packages/sdk/src/agent-context/types.ts | 26 +++++ .../sdk/test/agent-context/assemble.test.ts | 45 +++++-- .../sdk/test/agent-context/scaffold.test.ts | 12 ++ 52 files changed, 669 insertions(+), 809 deletions(-) create mode 100644 docs/features/language-differences.md delete mode 100644 fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md delete mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md delete mode 100644 fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md delete mode 100644 fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md delete mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 933b82d2c9..78da5b1094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,8 +35,38 @@ 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 ` and `meta agent-docs --skills-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 `//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 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,595 / 6,060 / 6,720 / 5,600 / 6,587 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`. - **Agent context routes report work to the reporting vocabulary.** The `meta init` agent guide and the `metaobjects-authoring` skill now send grouped totals, dashboard numbers and read-only report views to `object.report` (`references/declaring-reports.md`) before the `@sql` escape, diff --git a/README.md b/README.md index 72ea831217..c253871fe9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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/). diff --git a/agent-context/README.md b/agent-context/README.md index 81175b33bd..f2e32b7625 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -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/.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/.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/.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/.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.md` — universal skill body. - `skills//references/.md` — language/framework fragment; installed iff `` 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. @@ -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`. diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index cc1aaaf1bf..c2f1bb7cac 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -3,7 +3,7 @@ > {{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. @@ -11,7 +11,7 @@ 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, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -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 `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column .=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -39,6 +38,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -47,12 +62,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -60,14 +72,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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.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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/agent-context/templates/loops/java.md b/agent-context/templates/loops/java.md index 638e593fec..44210fe5d2 100644 --- a/agent-context/templates/loops/java.md +++ b/agent-context/templates/loops/java.md @@ -2,11 +2,8 @@ 1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator list, one `` each, under `` 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= -Dport=java` copies a generator to change what it emits. diff --git a/agent-context/templates/loops/kotlin.md b/agent-context/templates/loops/kotlin.md index ec62287786..ffc8805caf 100644 --- a/agent-context/templates/loops/kotlin.md +++ b/agent-context/templates/loops/kotlin.md @@ -1,13 +1,9 @@ ### Kotlin: generate with `mvn metaobjects:generate` 1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator - list, one `` each. Nothing is generated until it is listed. The Exposed - and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, - `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and - `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). - `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` - writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's - `references/kotlin.md`. + list, one `` each. Nothing is generated until it is listed. Which + classes to list (the Exposed and Spring tier, requirement tests, API docs) is in the + `metaobjects-codegen` skill's `references/kotlin.md`. 2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` copies a generator to change what it emits. Several Kotlin generators truncate their output on every run, so never put your own code in a generated file. diff --git a/agent-context/templates/loops/typescript.md b/agent-context/templates/loops/typescript.md index 6007a7ca92..f5133dc911 100644 --- a/agent-context/templates/loops/typescript.md +++ b/agent-context/templates/loops/typescript.md @@ -12,15 +12,9 @@ Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` - / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: - - ``` - npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - - ``` - - `npx meta eject ` copies one into `codegen/generators/` to change it; + / `routesFile()`, docs, `requirementTests()`) is one line too; `npx meta gen --list` is the + catalog. `npx meta eject ` copies one into `codegen/generators/` to change it; `npx meta generator new ` writes a new one. -2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. -3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: - "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and - `api/AGENT-API.md`. When they exist, read the one for the tier you touch. +2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. When + `agentDocsFile()` / `apiDocsFile()` are wired, read the `agent/*.md` or `api/AGENT-API.md` + page for the tier you touch. diff --git a/docs/README.md b/docs/README.md index 542ee95aaa..782ad18387 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,6 +36,7 @@ docs/ │ ├── extending-with-providers.md # custom metamodel subtypes/attrs via MetaDataTypeProvider │ ├── yaml-authoring.md │ ├── cli.md # CLI command matrix across all five ports +│ ├── language-differences.md # what differs by language: Node for the schema, list defaults, error bodies, names, data access │ ├── codegen-concepts.md # declarative template-codegen scopes (perEntity/perPackage/perModel) │ ├── codegen-data-shapes.md # the neutral template data dict shared cross-port │ ├── downstream-metadata-decisions.md # guidance for adopters extending the metamodel @@ -67,6 +68,7 @@ this tree is documentation, not the source of truth. | You want to … | Read | |---|---| | Pick a language and ship a small CRUD app | [`ports/.md`](ports/) | +| Know what behaves differently per language (Node for migrations, list defaults, validation errors, field names, data access) | [`features/language-differences.md`](features/language-differences.md) | | Understand what `object.entity`, `source.rdb`, `template.prompt` mean | [`features/`](features/) | | Compare what TS vs Java vs Kotlin vs C# vs Python emit for the same metadata | any [`features/*.md`](features/) — every feature shows all five ports side-by-side | | Author metadata in YAML instead of JSON | [`features/yaml-authoring.md`](features/yaml-authoring.md) | diff --git a/docs/features/api-contract.md b/docs/features/api-contract.md index 461dacf19b..ac530cddd1 100644 --- a/docs/features/api-contract.md +++ b/docs/features/api-contract.md @@ -281,7 +281,8 @@ each handler they register instead; an `HTTPException` is rethrown to your error responses"). A list over a large table should therefore always send a `limit`. A default page size is deliberately not introduced in a patch release: it would silently truncate the lists of every existing client that relies on - the unpaged answer. + the unpaged answer. Every per-port difference of this kind is collected in + [What differs by language](language-differences.md). - `withCount=1` — opt-in flag that switches the list response from `[...]` to `{ rows: [...], total: }` (needed for grid pagination). The grid hook always sends `withCount=1`. @@ -841,6 +842,8 @@ api-contract-conformance suite today (see ## See also +- [`docs/features/language-differences.md`](language-differences.md) — what this + contract leaves open, port by port (list defaults, error bodies, filter extensions) - [`docs/features/metadata-api.md`](metadata-api.md) — the `GET /_meta` model-shape contract this page's row-data contract complements - [`docs/features/loaders.md`](loaders.md) — how metadata maps to the diff --git a/docs/features/cli.md b/docs/features/cli.md index a2c84a40ee..0eece216ba 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -846,8 +846,9 @@ meta migrate --db "$DB_URL" --dialect postgres \ ## Agent-context scaffold is Node-only — by design -The `.metaobjects/AGENTS.md`/`CLAUDE.md` always-on files and the -`.claude/skills/metaobjects-*/` reference tree are assembled by **one** tool: the +The always-on `.metaobjects/AGENTS.md` and the `metaobjects-*` skills (under +`.claude/skills/` by default, or wherever `--skills-dir` puts them, such as `.agents/skills/` +for assistants that read that) are assembled by **one** tool: the Node `meta agent-docs` command. Per [ADR-0033](../../spec/decisions/ADR-0033-single-agent-context-assembler.md) the per-port native assemblers (Python/Java/C#) and their byte-identity conformance gates were removed — that content is effectively one static artifact, and every port diff --git a/docs/features/language-differences.md b/docs/features/language-differences.md new file mode 100644 index 0000000000..dbbcb6a629 --- /dev/null +++ b/docs/features/language-differences.md @@ -0,0 +1,79 @@ +# What differs by language + +One model, five ports. The metamodel, the loader, the drift gates and the cross-port REST +contract behave the same everywhere, and the shared conformance corpora check that they do. +Most of what a port *generates* follows its own language's conventions instead, and in a few +places the ports also differ in behaviour. This page lists those differences, so you can find +them here rather than discover them in production. + +This is the one place the list is kept. When a release changes a row, it changes it here, and +the port pages link to this page rather than repeating it. The table describes `main`. + +## You need Node for the schema, whatever your language + +**Schema migrations and database checks run in the Node `meta` tool, for every port** +([ADR-0015](../../spec/decisions/ADR-0015-single-shared-migrate-engine.md)). A .NET, Java, Kotlin or Python +team generates code with its own build tool (`dotnet meta`, `mvn metaobjects:*`, +`metaobjects`), but `meta migrate`, `meta migrate apply-pending`, `meta verify --db` and +`meta verify --replay` exist only in the Node CLI. So does `meta deps sync`, and so does +`meta agent-docs`, which writes the agent instructions. Install it per project with +`npm i -D @metaobjectsdev/cli` and run it as `npx meta`. Node is needed on the machine that +writes or applies migrations (a developer machine, CI, the deploy step), not in your running +service. `npx meta init --config-only` writes just `.metaobjects/config.json`, so a Maven- or +pip-rooted project can point the Node CLI at its metadata without gaining a TypeScript +project. + +## The table + +| | TypeScript | C# | Java | Kotlin | Python | +|---|---|---|---|---|---| +| **Generate code** | `npx meta gen` | `dotnet meta gen` | `mvn metaobjects:generate` | `mvn metaobjects:generate` | `metaobjects gen` | +| **Codegen drift check** | `npx meta verify --codegen` | `dotnet meta verify` | `mvn metaobjects:verify` | `mvn metaobjects:verify` | `metaobjects verify` | +| **Schema migrations** | `npx meta migrate` | `npx meta migrate` (Node), or `dotnet ef migrations add` over the generated `AppDbContext` ([C# port](../ports/csharp.md)) | `npx meta migrate` (Node); `--migration-format flyway` writes Flyway-named files | `npx meta migrate` (Node); `--migration-format flyway` as for Java | `npx meta migrate` (Node) | +| **Database drift check** | `npx meta verify --db` | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) | `npx meta verify --db` (Node) | +| **Generated data access** | Drizzle tables and row types (`entityFile()`), CRUD queries (`queriesFile()`) | EF Core entities and `AppDbContext` | Spring DTOs and a repository interface | Exposed tables and a repository base | None: the generated router calls a repository `Protocol` you implement | +| **Runtime metadata access** | `ObjectManager` on Kysely (`@metaobjectsdev/runtime-ts`) | None yet (roadmap) | OMDB (JDBC, Spring transactions) | Through Java's OMDB | `ObjectManager` on DB-API 2; not wired to the generated router | +| **Generated REST routes** | Fastify (`routesFile()`; Hono and Drizzle mounts in `runtime-ts`) | ASP.NET minimal API | Spring `@RestController` | Spring `@RestController` | FastAPI `APIRouter` | +| **List with no `limit`** | Every matching row | Every matching row | First 50 | First 50 | First 50 | +| **Largest `limit` on an entity list** | No cap (an opaque raw-SQL view: 1,000) | No cap | No cap | No cap | No cap | +| **Largest report page** | 10,000 rows, then 400 `report_too_large` | Same | Same | Same | Same | +| **Validation error body** | 400 `{"error": "validation", "issues": [...]}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` | 400 `{"error": "validation"}` | +| **Filter and sort errors** | 400 with `field`, plus `op`, `expected` and the allowed fields | 400 with `field` | 400 with `field` | 400 with `field` | 400 with `field` | +| **`?status=active` (a bare field, not `filter[status][eq]`)** | 400 `filter.bare_field`, naming the form that works | Ignored: every row comes back | Ignored | Ignored | Ignored | +| **Filter extras beyond the nine operators** | `?search=`, `filter[or]` / `filter[and]`, leading-`%` refusal, filter-value format check | None | None | None | None | +| **Malformed JSON, a malformed id, a bad page bound, a database constraint, a server error** | Pinned bodies: `invalid_json`, `invalid_id`, `pagination.invalid_value`, `constraint_violation`, 500 `internal` | As the framework answers (a report's bad page bound is pinned in every port) | Same as C# | Same as C# | Same as C# | +| **Field names in code** | The model's names (`createdAt`) | PascalCase properties (`CreatedAt`) | The model's names | The model's names | The model's names, not snake_case (`createdAt`) | +| **Field names on the wire** | The model's names, in every port | | | | | +| **Decimals and null members on the wire** | Each port writes its serializer's default; a `wire` setting makes them identical. The per-port table is under [The wire setting](api-contract.md#the-wire-setting) | | | | | +| **Hand edits to a generated file** | Merged (three-way) on the machine that generated it; refused by name elsewhere | Refused by name | Refused by name, or overwritten with `overwrite` | Same as Java | Refused by name | +| **Own a generator** | `npx meta eject` | `dotnet meta eject` | `mvn metaobjects:eject` | `mvn metaobjects:eject` | `metaobjects eject` | +| **Requirement authoring lint** | Yes (advisory) | No | No | No | No | + +The REST rows are the detail behind the [cross-port REST contract](api-contract.md): what that +contract pins is identical in every port, and these rows are what it leaves open. When an +agent or a client talks to a generated API, the safe subset is the one every port shares: +`filter[][]=` with the nine operators, an explicit `limit`, and a 400 that +carries `error` (and `field` for a filter or sort error). + +## Why these differ + +Two rules produce most of the table. First, **each port writes its own language's idiom**: +EF Core in C#, Spring in Java, Exposed in Kotlin, Pydantic and FastAPI in Python. Identical +output is something a project opts into (the `wire` setting), not a default the ports are +bent to fit ([ADR-0019 Amendment 2](../../spec/decisions/ADR-0019-runtime-return-type-contract.md)). +Second, **generated application code is a reference helper**, not a guarantee +([ADR-0034 Amendment 3](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md)): a route, +a repository or a DTO is a starting point you can eject and change, so a difference in what +the reference writes is a difference you can remove in your own copy. + +The rest (the list default, the validation detail, Python's field names and data access) are +known gaps rather than design, and the roadmap closes several of them. When one closes, its row +here changes in the same pull request. + +## See also + +- [Cross-port REST API contract](api-contract.md): the shared contract and its gates. +- [CLI architecture](cli.md): which commands each port's own CLI carries. +- [Migrations and drift](migrations-and-drift.md): the schema workflow every port uses. +- The port pages: [TypeScript](../ports/typescript.md), [C#](../ports/csharp.md), + [Java](../ports/java.md), [Kotlin](../ports/kotlin.md), [Python](../ports/python.md). diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt index 6384dd8536..b750c5743c 100644 --- a/docs/llms/llms-full.txt +++ b/docs/llms/llms-full.txt @@ -4,7 +4,7 @@ **Note for AI assistants:** This file is the concatenated reference material for MetaObjects, intended to be loaded as LLM context. For the short index, see [llms.txt](https://metaobjects.dev/llms.txt). For the canonical spec and source, see the [GitHub repo](https://github.com/metaobjectsdev/metaobjects). When this file is out of date relative to the GitHub source, the GitHub source wins. -**Adopting MetaObjects in a project?** Before anything else: install the port's package, run `meta init` (the Node `meta` CLI — works in any project with Node, including JVM/Python projects with a TS frontend), then load the scaffolded `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills. Those are version-matched to the installed release and far richer than this file. See "For AI assistants adopting MetaObjects" below. +**Adopting MetaObjects in a project?** Before anything else: install the port's package, run `meta init` (the Node `meta` CLI — works in any project with Node, including JVM/Python projects with a TS frontend), then load the scaffolded `.metaobjects/AGENTS.md` and the `metaobjects-*` skills. Those are version-matched to the installed release and far richer than this file. See "For AI assistants adopting MetaObjects" below. --- @@ -42,8 +42,8 @@ If you are an AI assistant helping someone adopt MetaObjects, do this **first** - Java / Kotlin: add `com.metaobjects:metaobjects-metadata`, `com.metaobjects:metaobjects-codegen-spring` (Java) or `com.metaobjects:metaobjects-codegen-kotlin` (Kotlin), and `com.metaobjects:metaobjects-maven-plugin`, all at `8.3.0`. - Python: `pip install metaobjects` - C#: install the MetaObjects .NET tool (invoked as `dotnet meta`). -2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project with a TypeScript frontend. It scaffolds a slim always-on `.metaobjects/AGENTS.md` + `.metaobjects/CLAUDE.md` (auto-wired via an `@.metaobjects/AGENTS.md` import line into the project's root `CLAUDE.md`/`AGENTS.md`, so it loads automatically) plus six `metaobjects-*` Claude Code skills under `.claude/skills/`, carrying only the project's language reference fragments. Re-run `meta init --refresh-docs` to update the agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. For a non-Node project, scaffold just the agent context with `npx meta agent-docs --server `.) -3. **Load `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version. +2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project with a TypeScript frontend. It scaffolds a slim always-on `.metaobjects/AGENTS.md` (auto-wired via a one-line pointer and an `@.metaobjects/AGENTS.md` import line into the project's root `AGENTS.md`/`CLAUDE.md`, so it loads automatically in any coding assistant) plus six `metaobjects-*` skills under `.claude/skills/` (or the directory `--skills-dir` names, such as `.agents/skills`), carrying only the project's language reference fragments. Re-run `meta init --refresh-docs` to update the agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. For a non-Node project, scaffold just the agent context with `npx meta agent-docs --server `.) +3. **Load `.metaobjects/AGENTS.md` and the `metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version. This file is the short corpus; the deep, version-matched how-to is the scaffolded local context. @@ -293,7 +293,7 @@ Run the CLI: ```bash # Scaffold metaobjects/, .metaobjects/, metaobjects.config.ts, AND the agent context -# (.metaobjects/AGENTS.md + CLAUDE.md + .claude/skills/metaobjects-*). Wires no generators. +# (.metaobjects/AGENTS.md + the metaobjects-* skills). Wires no generators. $ meta init # The catalog: every generator by layer, with a file count for YOUR model diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt index a8bb7fd3a5..8efd170782 100644 --- a/docs/llms/llms.txt +++ b/docs/llms/llms.txt @@ -31,14 +31,14 @@ If you are an AI assistant helping someone adopt MetaObjects in their project, d - **C#**: install the MetaObjects .NET tool (`dotnet tool install` the MetaObjects CLI tool; invoked as `dotnet meta`). 2. **Run `meta init`** (the Node `meta` CLI). It works in any project that has Node — including a JVM or Python project that happens to have a TypeScript frontend. It scaffolds, into the project: - - a slim always-on `.metaobjects/AGENTS.md` + `.metaobjects/CLAUDE.md` agent context, **auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`** (an `@.metaobjects/AGENTS.md` import line) so it loads automatically; and - - six `metaobjects-*` Claude Code skills under `.claude/skills/`, carrying only the language reference fragments for the project's stack. + - a slim always-on `.metaobjects/AGENTS.md` agent context, **auto-wired into the project's root `AGENTS.md`/`CLAUDE.md`** (a one-line pointer plus an `@.metaobjects/AGENTS.md` import line) so it loads automatically in any coding assistant; and + - six `metaobjects-*` skills under `.claude/skills/` (Claude Code's directory; pass `--skills-dir .agents/skills`, or any other directory your assistant reads), carrying only the language reference fragments for the project's stack. The always-on file names the directory, so an assistant that does not load skills opens them by path. Re-run `meta init --refresh-docs` to update the scaffolded agent docs after a CLI upgrade. (The Node `meta` CLI is the single agent-context scaffolder for every backend, by design — ADR-0033; the JVM/Python/C# CLIs deliberately redirect to it. Run `meta init` regardless of backend language, or `npx meta agent-docs --server ` to scaffold just the agent context.) **Upgrading a 1.0.x project to 1.1.0:** read before the first `gen`. It is the one page for every port: the steps in order and every change that needs action (the physical-naming default, documentation as a generator, the canonical decimal string, integer-only validator bounds). Several of them fail nothing when skipped. -3. **After scaffolding, load `.metaobjects/AGENTS.md` and the `.claude/skills/metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version. +3. **After scaffolding, load `.metaobjects/AGENTS.md` and the `metaobjects-*` skills.** They teach metadata authoring, codegen, runtime/UI, prompt construction, and `verify` — for this project's exact stack and version. This `llms.txt` is the short index; the deep, version-matched how-to is the scaffolded local context. @@ -75,7 +75,7 @@ MetaObjects pillars are capabilities of one metadata model, not separate product ## CLI and project setup -MetaObjects deliberately does **not** ship one universal binary. Schema operations are language-agnostic and live in **one canonical Node `meta` CLI**; codegen is language-specific and runs in each language's own build tool (ADR-0015). +MetaObjects deliberately does **not** ship one universal binary. Schema operations are language-agnostic and live in **one canonical Node `meta` CLI**; codegen is language-specific and runs in each language's own build tool (ADR-0015). So a C#, Java, Kotlin or Python team still needs Node wherever migrations are written or applied. What else differs per language (list defaults, error bodies, field names, data access) is listed in [What differs by language](https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/language-differences.md). - Node CLI binary name: `meta` - Project config file: `metaobjects.config.ts` (configures `outDir`, `targets`, `dialect`, `dbImport`, `apiPrefix`, and the `generators` array); column and table naming is `naming` in `.metaobjects/config.json`, read by every port @@ -89,7 +89,7 @@ MetaObjects deliberately does **not** ship one universal binary. Schema operatio ### Commands -- `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts` (`--with-docs` also wires `docsFile()` and `apiDocsFile()`), **plus** the agent context: a slim `.metaobjects/AGENTS.md` + `CLAUDE.md` (auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`) and six `metaobjects-*` Claude Code skills under `.claude/skills/` scoped to the project's stack. `meta init --refresh-docs` updates only the agent docs. +- `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts` (`--with-docs` also wires `docsFile()` and `apiDocsFile()`), **plus** the agent context: a slim `.metaobjects/AGENTS.md` (auto-wired into the project's root `AGENTS.md`/`CLAUDE.md`) and six `metaobjects-*` skills scoped to the project's stack, under `.claude/skills/` or the directory `--skills-dir` names. `meta init --refresh-docs` updates only the agent docs. - `meta gen [...]` — TS codegen from entities defined under `metaobjects/`. Supports `--dry-run` and `--watch`. Generators (`entityFile()`, `queriesFile()`, `routesFile()`, `formFile()`, `tanstackQuery()`, `tanstackGrid()`, `barrel()`) are reference helpers: `meta init` wires none. Import one from its package to run it as shipped; `meta gen --list` prints the import line. `meta eject ...` copies the ones you need to change (`entity`, `queries`, `routes`, `names`, `barrel`, `form`, `hooks`, `grid`, `grid-hook`, `routes-hono`) into `codegen/generators/` in your own repo (ADR-0034 scaffold-and-own) and prints the local import and entry to add to `metaobjects.config.ts`. The shipped defaults target Fastify/Drizzle/React/TanStack and are retargeted by editing an ejected copy, not by switching tools. `meta gen --strict` turns an unknown attribute into an error, as `meta verify` does. `meta eject --undo` removes ejected copies that never changed. Per-port codegen runs in that port's own build tool: `mvn metaobjects:generate` (Java/Kotlin), `dotnet meta gen` (C#), `metaobjects gen` (Python). - `meta verify` — drift check. `verify --codegen` (regen-and-diff vs committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), `verify --db` (live-DB schema drift, Node `meta` only). Per-port codegen verify: `mvn metaobjects:verify -Dmeta.verify.mode=codegen|templates` (Java/Kotlin), `dotnet meta verify` (C#), `metaobjects verify` (Python). - `meta verify` also checks any `requirement.*` nodes the model declares (capability requirements — what the system is supposed to do, as metadata). `@status` is a loader-enforced closed enum: `planned | live | partial | retired`. A dangling `@implementedBy` is an ERROR on `live`/`partial` and CORRECT on `planned` — an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened. **`retired` satisfies that by stating a prohibition in force.** A capability that was built and then deliberately removed keeps its entry, written as the rule that it must not come back (`@supersededBy` names what replaced it, and is RESOLVED, so it must be a full dotted path; `@implementedBy` is REFUSED on `retired`). Do NOT delete a retired capability's requirement — that entry is the whole point of the feature: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24. `@verifiedBy` and the `abandoned`/`superseded` statuses were retired in `0.24.0` and now fail the load. Entirely opt-in: no `requirement.*` nodes means no diagnostics. Registered vocabulary in all five ports, and every port's `verify` runs the same gate (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, `dotnet meta verify`); the authoring lint is the Node `meta` CLI only. diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md index 8723d47ffc..1695ce5839 100644 --- a/docs/ports/csharp.md +++ b/docs/ports/csharp.md @@ -5,6 +5,8 @@ A first-class full-stack target on .NET 8. Loader + conformance + EF Core codege Postgres + Npgsql. The baseline is **.NET 8** (`net8.0`); the tool and the packages are not built for earlier runtimes. +> **Schema migrations and database checks run in the Node `meta` tool.** `dotnet meta` generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). + ## Regeneration and hand edits `dotnet meta gen` has no three-way merge (that is TypeScript-only). It decides per file from a diff --git a/docs/ports/java.md b/docs/ports/java.md index ba2c998a6b..c998f0b5cf 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -22,6 +22,8 @@ the Java diff-and-converge migration engine and its `meta:migrate` / live-DB-dri path was also removed — OMDB is pure data-access (CRUD/query/codec/transactions). Prompt / template drift is still checked via the `metaobjects-render` `Verify` API. +> **Schema migrations and database checks run in the Node `meta` tool.** the Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). + ## Regeneration and hand edits There is no three-way merge on the JVM (that is TypeScript-only). `mvn metaobjects:generate` diff --git a/docs/ports/kotlin.md b/docs/ports/kotlin.md index 3dc4f382d7..08ebbd5a83 100644 --- a/docs/ports/kotlin.md +++ b/docs/ports/kotlin.md @@ -20,6 +20,8 @@ Two modules: - **`metaobjects-metadata-ktx`** — thin Kotlin facade over the Java loader + render engine for idiomatic Kotlin runtime use. +> **Schema migrations and database checks run in the Node `meta` tool.** the Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). + ## Regeneration and hand edits There is no three-way merge on the JVM (that is TypeScript-only). Kotlin runs through the same diff --git a/docs/ports/python.md b/docs/ports/python.md index 28006a7212..7543c17223 100644 --- a/docs/ports/python.md +++ b/docs/ports/python.md @@ -9,6 +9,8 @@ with no ORM of any kind, so your SQLAlchemy or Django models stay hand-written b Python port has no `migrate` command by design; it consumes the canonical `schema.postgres.sql` artifact verbatim. +> **Schema migrations and database checks run in the Node `meta` tool.** `metaobjects` generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). + ## Regeneration and hand edits `metaobjects gen` has no three-way merge (that is TypeScript-only). It decides per file from a diff --git a/docs/ports/typescript.md b/docs/ports/typescript.md index 51e88478ff..5f4e7c2fe8 100644 --- a/docs/ports/typescript.md +++ b/docs/ports/typescript.md @@ -5,6 +5,8 @@ The reference implementation. Published to npm at `1.2.0` as 14 runtimes: the generated code and the CLI run on stock Node (via `tsx` or after a `tsc` build), and Bun is simply the fastest dev workflow in this repo. +> The Node `meta` CLI is also the schema tool for the other four ports, so a Java, C# or Python team runs `npx meta migrate` and `npx meta verify --db` too. Where the TypeScript output behaves differently from theirs (list defaults, error bodies, filter extensions) is listed in [What differs by language](../features/language-differences.md). + ## Regeneration and hand edits Three-way merge is a TypeScript feature: on the machine that generated a file, `meta gen` diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md index a3f4014b0b..e9582ac1c7 100644 --- a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md @@ -3,7 +3,7 @@ > Stack: csharp server, no client. > > 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. @@ -28,7 +28,7 @@ truth. Code, schema and migrations are generated from it and are disposable. 2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo to change what it emits. -### Schema (every stack: the Node `meta` CLI owns it) +### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -38,16 +38,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand. - At boot or deploy: `npx meta migrate apply-pending --db `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -57,6 +56,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -65,12 +80,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -78,14 +90,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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 +`.claude/skills//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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md deleted file mode 100644 index a3f4014b0b..0000000000 --- a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md +++ /dev/null @@ -1,91 +0,0 @@ -# Working with MetaObjects in this project - -> Stack: csharp server, no client. -> -> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your -> **root `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 - -### C#: generate with `dotnet meta gen` - -1. **Config.** `codegen/Generators.cs` (written by `dotnet meta init`) is the generator list. - Nothing is generated until it is listed. The EF Core data layer and its routes: - - ```csharp - new MetaObjects.Codegen.Generators.EntityGenerator(), - new MetaObjects.Codegen.Generators.DbContextGenerator(), - new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(), - new MetaObjects.Codegen.Generators.RoutesGenerator(), - ``` - - `RequirementTestsGenerator { OutDir = "/Generated" }` writes one test per - requirement; `ApiDocsGenerator` writes `api/csharp/AGENT-API.md`. `dotnet meta gen --list` - is the catalog. -2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo - to change what it emits. - -### Schema (every stack: the Node `meta` CLI owns it) - -Never hand-write DDL, and never `ALTER` a live database by hand. - -- First migration (needs a reachable, empty database): - `npx meta migrate --from-db --db --dialect --slug init` writes - `.metaobjects/migrations/-init/up.sql` and `down.sql`. -- At boot or deploy: `npx meta migrate apply-pending --db `. -- A later change: edit the metadata, regenerate, then - `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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. - -### 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. - -### Before you call it done - -- `dotnet meta verify --codegen` (the generated code and the requirements). -- `npx meta verify --db ` (the live schema matches the metadata). - -Commit the metadata, the generated code, `.metaobjects/migrations/` and -`.metaobjects/.gen-state/.hashes.json`. - -## Rules - -- What the metadata describes is generated, never hand-written (tables, types, validators, - foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. -- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or - 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. -- Never invent metadata vocabulary. Search it: `npx meta types `, and - `npx meta types . --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 -`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index 6c4200f7c4..e357366045 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -3,7 +3,7 @@ > Stack: java, kotlin server, react, tanstack client. > > 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. @@ -14,25 +14,18 @@ truth. Code, schema and migrations are generated from it and are disposable. 1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator list, one `` each, under `` 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= -Dport=java` copies a generator to change what it emits. ### Kotlin: generate with `mvn metaobjects:generate` 1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator - list, one `` each. Nothing is generated until it is listed. The Exposed - and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, - `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and - `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). - `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` - writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's - `references/kotlin.md`. + list, one `` each. Nothing is generated until it is listed. Which + classes to list (the Exposed and Spring tier, requirement tests, API docs) is in the + `metaobjects-codegen` skill's `references/kotlin.md`. 2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` copies a generator to change what it emits. Several Kotlin generators truncate their output on every run, so never put your own code in a generated file. @@ -46,7 +39,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it) +### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -56,16 +49,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand. - At boot or deploy: `npx meta migrate apply-pending --db `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -76,6 +68,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -84,12 +92,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -97,14 +102,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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 +`.claude/skills//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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md deleted file mode 100644 index 6c4200f7c4..0000000000 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md +++ /dev/null @@ -1,110 +0,0 @@ -# Working with MetaObjects in this project - -> Stack: java, kotlin server, react, tanstack client. -> -> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your -> **root `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 - -### Java: generate with `mvn metaobjects:generate` - -1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator - list, one `` each, under `` 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`. -2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` - copies a generator to change what it emits. - -### Kotlin: generate with `mvn metaobjects:generate` - -1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator - list, one `` each. Nothing is generated until it is listed. The Exposed - and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, - `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and - `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). - `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` - writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's - `references/kotlin.md`. -2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` - copies a generator to change what it emits. Several Kotlin generators truncate their output - on every run, so never put your own code in a generated file. - -### Web client: generated by the Node `npx meta gen` - -The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` -from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from -`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and -`@metaobjectsdev/tanstack`. Wrap the app in ``: -`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every -hook without an error. The `metaobjects-runtime-ui` skill has the detail. - -### Schema (every stack: the Node `meta` CLI owns it) - -Never hand-write DDL, and never `ALTER` a live database by hand. - -- First migration (needs a reachable, empty database): - `npx meta migrate --from-db --db --dialect --slug init` writes - `.metaobjects/migrations/-init/up.sql` and `down.sql`. -- At boot or deploy: `npx meta migrate apply-pending --db `. -- A later change: edit the metadata, regenerate, then - `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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. - -### 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. - -### Before you call it done - -- `mvn metaobjects:verify` (the generated code and the requirements). -- `npx meta verify --codegen` (the generated web client). -- `npx meta verify --db ` (the live schema matches the metadata). - -Commit the metadata, the generated code, `.metaobjects/migrations/` and -`.metaobjects/.gen-state/.hashes.json`. - -## Rules - -- What the metadata describes is generated, never hand-written (tables, types, validators, - foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. -- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or - 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. -- Never invent metadata vocabulary. Search it: `npx meta types `, and - `npx meta types . --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 -`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index 074f87fdd1..bf2b5b0362 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -3,7 +3,7 @@ > Stack: java server, react client. > > 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. @@ -14,12 +14,9 @@ truth. Code, schema and migrations are generated from it and are disposable. 1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator list, one `` each, under `` 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= -Dport=java` copies a generator to change what it emits. @@ -32,7 +29,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it) +### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -42,16 +39,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand. - At boot or deploy: `npx meta migrate apply-pending --db `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -62,6 +58,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -70,12 +82,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -83,14 +92,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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 +`.claude/skills//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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md deleted file mode 100644 index 074f87fdd1..0000000000 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ /dev/null @@ -1,96 +0,0 @@ -# Working with MetaObjects in this project - -> Stack: java server, react client. -> -> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your -> **root `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 - -### Java: generate with `mvn metaobjects:generate` - -1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator - list, one `` each, under `` 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`. -2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` - copies a generator to change what it emits. - -### Web client: generated by the Node `npx meta gen` - -The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` -from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from -`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and -`@metaobjectsdev/tanstack`. Wrap the app in ``: -`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every -hook without an error. The `metaobjects-runtime-ui` skill has the detail. - -### Schema (every stack: the Node `meta` CLI owns it) - -Never hand-write DDL, and never `ALTER` a live database by hand. - -- First migration (needs a reachable, empty database): - `npx meta migrate --from-db --db --dialect --slug init` writes - `.metaobjects/migrations/-init/up.sql` and `down.sql`. -- At boot or deploy: `npx meta migrate apply-pending --db `. -- A later change: edit the metadata, regenerate, then - `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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. - -### 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. - -### Before you call it done - -- `mvn metaobjects:verify` (the generated code and the requirements). -- `npx meta verify --codegen` (the generated web client). -- `npx meta verify --db ` (the live schema matches the metadata). - -Commit the metadata, the generated code, `.metaobjects/migrations/` and -`.metaobjects/.gen-state/.hashes.json`. - -## Rules - -- What the metadata describes is generated, never hand-written (tables, types, validators, - foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. -- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or - 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. -- Never invent metadata vocabulary. Search it: `npx meta types `, and - `npx meta types . --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 -`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index 4ac33cdba1..73871b8d20 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -3,7 +3,7 @@ > Stack: python server, no client. > > 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. @@ -29,7 +29,7 @@ truth. Code, schema and migrations are generated from it and are disposable. 2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo to change what it emits. -### Schema (every stack: the Node `meta` CLI owns it) +### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -39,16 +39,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand. - At boot or deploy: `npx meta migrate apply-pending --db `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -58,6 +57,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -66,12 +81,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -79,14 +91,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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 +`.claude/skills//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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md deleted file mode 100644 index 4ac33cdba1..0000000000 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ /dev/null @@ -1,92 +0,0 @@ -# Working with MetaObjects in this project - -> Stack: python server, no client. -> -> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your -> **root `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 - -### Python: generate with `metaobjects gen` - -1. **Config.** `metaobjects.config.yaml` names each target's `outDir` and its generators. Nothing - is generated until it is listed: - - ```yaml - targets: - app: - outDir: generated - generators: [entity, names] # Pydantic models + physical-name constants - ``` - - `routes` and `filter-allowlist` add FastAPI routers over a repository `Protocol` you - implement; `requirement-tests` writes one pytest file per package of requirements; - `api-docs` writes `api/python/AGENT-API.md`. `metaobjects gen --list` is the catalog. - Install what the output imports: `pip install "pydantic[email]>=2" fastapi`. -2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo - to change what it emits. - -### Schema (every stack: the Node `meta` CLI owns it) - -Never hand-write DDL, and never `ALTER` a live database by hand. - -- First migration (needs a reachable, empty database): - `npx meta migrate --from-db --db --dialect --slug init` writes - `.metaobjects/migrations/-init/up.sql` and `down.sql`. -- At boot or deploy: `npx meta migrate apply-pending --db `. -- A later change: edit the metadata, regenerate, then - `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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. - -### 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. - -### Before you call it done - -- `metaobjects verify` (the generated code and the requirements; `--codegen` is its default). -- `npx meta verify --db ` (the live schema matches the metadata). - -Commit the metadata, the generated code, `.metaobjects/migrations/` and -`.metaobjects/.gen-state/.hashes.json`. - -## Rules - -- What the metadata describes is generated, never hand-written (tables, types, validators, - foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. -- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or - 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. -- Never invent metadata vocabulary. Search it: `npx meta types `, and - `npx meta types . --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 -`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index 32ec038942..4c1db26c29 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -3,7 +3,7 @@ > Stack: typescript server, react, tanstack client. > > 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. @@ -24,18 +24,12 @@ truth. Code, schema and migrations are generated from it and are disposable. Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` - / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: - - ``` - npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - - ``` - - `npx meta eject ` copies one into `codegen/generators/` to change it; + / `routesFile()`, docs, `requirementTests()`) is one line too; `npx meta gen --list` is the + catalog. `npx meta eject ` copies one into `codegen/generators/` to change it; `npx meta generator new ` writes a new one. -2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. -3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: - "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and - `api/AGENT-API.md`. When they exist, read the one for the tier you touch. +2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. When + `agentDocsFile()` / `apiDocsFile()` are wired, read the `agent/*.md` or `api/AGENT-API.md` + page for the tier you touch. ### Web client: generated by the Node `npx meta gen` @@ -46,7 +40,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it) +### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) Never hand-write DDL, and never `ALTER` a live database by hand. @@ -56,16 +50,15 @@ Never hand-write DDL, and never `ALTER` a live database by hand. - At boot or deploy: `npx meta migrate apply-pending --db `. - A later change: edit the metadata, regenerate, then `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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
.=` (an object: + `--rename-table =`) 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 @@ -75,6 +68,22 @@ 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[][]=`: `?filter[status][eq]=active`, never + `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every + row). 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=: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": ""}`: 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, @@ -83,12 +92,9 @@ 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 `, and `npx meta types . --detail` for one construct's valid attributes. - Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are @@ -96,14 +102,14 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and ## 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 -`.new` beside it. Merge that into ``, 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 +`.claude/skills//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 `.new` beside it, which you merge into `` and delete. +Never `--force` a refresh over local edits. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md deleted file mode 100644 index 32ec038942..0000000000 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ /dev/null @@ -1,109 +0,0 @@ -# Working with MetaObjects in this project - -> Stack: typescript server, react, tanstack client. -> -> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your -> **root `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 - -### TypeScript: generate with `npx meta gen` - -1. **Config.** In `metaobjects.config.ts` set `dialect` (`"postgres" | "sqlite" | "mysql"`; D1 is - `"sqlite"`) and list the generators. Nothing is generated until it is listed. The usual data - layer: - - ```ts - import { entityFile, namesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; - // generators: [entityFile(), namesFile(), barrel()] - ``` - - Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and - `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: - `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` - / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: - - ``` - npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - - ``` - - `npx meta eject ` copies one into `codegen/generators/` to change it; - `npx meta generator new ` writes a new one. -2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. -3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: - "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and - `api/AGENT-API.md`. When they exist, read the one for the tier you touch. - -### Web client: generated by the Node `npx meta gen` - -The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` -from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from -`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and -`@metaobjectsdev/tanstack`. Wrap the app in ``: -`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every -hook without an error. The `metaobjects-runtime-ui` skill has the detail. - -### Schema (every stack: the Node `meta` CLI owns it) - -Never hand-write DDL, and never `ALTER` a live database by hand. - -- First migration (needs a reachable, empty database): - `npx meta migrate --from-db --db --dialect --slug init` writes - `.metaobjects/migrations/-init/up.sql` and `down.sql`. -- At boot or deploy: `npx meta migrate apply-pending --db `. -- A later change: edit the metadata, regenerate, then - `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). - For a renamed field add `--rename-column
.=` (a renamed object: - `--rename-table =`), 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. - -### 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. - -### Before you call it done - -- `npx meta verify --codegen` (the generated code and docs pages; the requirement gate runs on every `meta verify`). -- `npx meta verify --db ` (the live schema matches the metadata). - -Commit the metadata, the generated code, `.metaobjects/migrations/` and -`.metaobjects/.gen-state/.hashes.json`. - -## Rules - -- What the metadata describes is generated, never hand-written (tables, types, validators, - foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. -- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or - 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. -- Never invent metadata vocabulary. Search it: `npx meta types `, and - `npx meta types . --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 -`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/server/csharp/MetaObjects/AgentContext/AgentContextScaffold.cs b/server/csharp/MetaObjects/AgentContext/AgentContextScaffold.cs index 509ae04ab5..8044917f02 100644 --- a/server/csharp/MetaObjects/AgentContext/AgentContextScaffold.cs +++ b/server/csharp/MetaObjects/AgentContext/AgentContextScaffold.cs @@ -50,7 +50,7 @@ public sealed record Manifest( var from = manifest.GeneratedBy ?? "an older MetaObjects"; return $"MetaObjects agent context was generated by {from}; you're on {currentVersion}. " + - "Re-run 'npx meta agent-docs' to refresh the .claude/skills docs (it keeps the stack recorded in .metaobjects/)."; + "Re-run 'npx meta agent-docs' to refresh the agent context and its skills (it keeps the stack and skills directory recorded in .metaobjects/)."; } /// without its +build suffix, or null. diff --git a/server/csharp/README.md b/server/csharp/README.md index 70e1dd36bb..ace9ce1131 100644 --- a/server/csharp/README.md +++ b/server/csharp/README.md @@ -23,9 +23,10 @@ Upgrading from 1.0.x: [upgrading-1.0-to-1.1.md](../../docs/features/migrations/u ## AI assistant context -To scaffold MetaObjects context files (`.metaobjects/AGENTS.md`, `.metaobjects/CLAUDE.md`, and -`.claude/skills/metaobjects-*/`) into your project so your AI assistant understands how to -author metadata and run codegen, use the Node `meta` CLI: +To scaffold MetaObjects context files (`.metaobjects/AGENTS.md` and the `metaobjects-*` skills) +into your project so your AI assistant understands how to author metadata and run codegen, use +the Node `meta` CLI. The skills go to `.claude/skills/` unless you pass `--skills-dir` (for +example `.agents/skills` for assistants that read that directory): ```bash npx meta agent-docs --server csharp diff --git a/server/java/README.md b/server/java/README.md index c9f270b60e..61de92e24c 100644 --- a/server/java/README.md +++ b/server/java/README.md @@ -148,9 +148,10 @@ Releases to Maven Central: see [docs/RELEASING-java.md](../../docs/RELEASING-jav ## AI assistant context -To scaffold MetaObjects context files (`.metaobjects/AGENTS.md`, `.metaobjects/CLAUDE.md`, and -`.claude/skills/metaobjects-*/`) into your project so your AI assistant understands how to -author metadata and run codegen, use the Node `meta` CLI: +To scaffold MetaObjects context files (`.metaobjects/AGENTS.md` and the `metaobjects-*` skills) +into your project so your AI assistant understands how to author metadata and run codegen, use +the Node `meta` CLI. The skills go to `.claude/skills/` unless you pass `--skills-dir` (for +example `.agents/skills` for assistants that read that directory): ```bash npx meta agent-docs --server java diff --git a/server/java/metadata/src/main/java/com/metaobjects/agentcontext/AgentContextScaffold.java b/server/java/metadata/src/main/java/com/metaobjects/agentcontext/AgentContextScaffold.java index 4402ef04b1..52f085d779 100644 --- a/server/java/metadata/src/main/java/com/metaobjects/agentcontext/AgentContextScaffold.java +++ b/server/java/metadata/src/main/java/com/metaobjects/agentcontext/AgentContextScaffold.java @@ -74,7 +74,7 @@ public static String staleness(Manifest manifest, String currentVersion) { String from = manifest.generatedBy() != null ? manifest.generatedBy() : "an older MetaObjects"; return "MetaObjects agent context was generated by " + from + "; you're on " + currentVersion + ". Run 'npx meta agent-docs' to refresh " - + "the .claude/skills docs (it keeps the stack recorded in .metaobjects/)."; + + "the agent context and its skills (it keeps the stack and skills directory recorded in .metaobjects/)."; } /** diff --git a/server/java/metadata/src/test/java/com/metaobjects/agentcontext/AgentContextStalenessTest.java b/server/java/metadata/src/test/java/com/metaobjects/agentcontext/AgentContextStalenessTest.java index 634ac15322..269c5fb760 100644 --- a/server/java/metadata/src/test/java/com/metaobjects/agentcontext/AgentContextStalenessTest.java +++ b/server/java/metadata/src/test/java/com/metaobjects/agentcontext/AgentContextStalenessTest.java @@ -102,7 +102,7 @@ public void differingVersionNudgesNamingBoth() { String msg = AgentContextScaffold.staleness(manifest("7.1.0"), "7.2.1"); assertEquals( "MetaObjects agent context was generated by 7.1.0; you're on 7.2.1. " - + "Run 'npx meta agent-docs' to refresh the .claude/skills docs (it keeps the stack recorded in .metaobjects/).", + + "Run 'npx meta agent-docs' to refresh the agent context and its skills (it keeps the stack and skills directory recorded in .metaobjects/).", msg); } diff --git a/server/python/README.md b/server/python/README.md index a4958eb706..b1f293e1c5 100644 --- a/server/python/README.md +++ b/server/python/README.md @@ -70,9 +70,10 @@ wins. See [the upgrade guide](../../docs/features/migrations/upgrading-1.0-to-1. ## AI assistant context -To scaffold MetaObjects context files (`.metaobjects/AGENTS.md`, `.metaobjects/CLAUDE.md`, and -`.claude/skills/metaobjects-*/`) into your project so your AI assistant understands how to -author metadata and run codegen, use the Node `meta` CLI: +To scaffold MetaObjects context files (`.metaobjects/AGENTS.md` and the `metaobjects-*` skills) +into your project so your AI assistant understands how to author metadata and run codegen, use +the Node `meta` CLI. The skills go to `.claude/skills/` unless you pass `--skills-dir` (for +example `.agents/skills` for assistants that read that directory): ```bash npx meta agent-docs --server python diff --git a/server/python/src/metaobjects/agent_context/scaffold.py b/server/python/src/metaobjects/agent_context/scaffold.py index 20f3176504..04ae849d43 100644 --- a/server/python/src/metaobjects/agent_context/scaffold.py +++ b/server/python/src/metaobjects/agent_context/scaffold.py @@ -241,5 +241,6 @@ def agent_context_staleness( return ( f"MetaObjects agent context was generated by {frm}; " f"you're on {current_version}. Run 'npx meta agent-docs' to refresh the " - f".claude/skills docs (it keeps the stack recorded in .metaobjects/)." + f"agent context and its skills (it keeps the stack and skills directory recorded in " + f".metaobjects/)." ) diff --git a/server/typescript/packages/cli/README.md b/server/typescript/packages/cli/README.md index e5f43d0924..914893e571 100644 --- a/server/typescript/packages/cli/README.md +++ b/server/typescript/packages/cli/README.md @@ -107,7 +107,7 @@ Running `meta` with no arguments prints a concise status line (whether a `metaob ### `meta init` -Scaffolds `metaobjects/` (visible entity declarations, with a placeholder `meta.common.json`), `.metaobjects/` (hidden tool state: `config.json`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`, `.gen-state/`), an **empty** `codegen/generators/` plus its `tsconfig.codegen.json`, and `metaobjects.config.ts` at the repo root. +Scaffolds `metaobjects/` (visible entity declarations, with a placeholder `meta.common.json`), `.metaobjects/` (hidden tool state: `config.json`, the always-on `AGENTS.md`, `.gitignore`, `.gen-state/`), the `metaobjects-*` skills under `.claude/skills/` (or `--skills-dir`), a root `AGENTS.md` / `CLAUDE.md` import of the always-on file, an **empty** `codegen/generators/` plus its `tsconfig.codegen.json`, and `metaobjects.config.ts` at the repo root. **It copies no generators and declares no dependencies.** Codegen is opt-in (ADR-0034 Amendment 2): the scaffolded config carries `generators: []` and a comment pointing at the catalog. `meta gen --list --probe` shows what each generator would emit for your model, with the import line that runs it as shipped; `meta gen --list --format json` adds what to install (`install.dev`, `install.runtime`) and the config keys each generator reads (`configKeys`), which the text listing does not print. Import a generator from its package to use it unchanged; `meta eject ...` copies the ones you want to change into `codegen/generators/` — **yours to edit** — and prints the import line to swap in, the entry to add, the install command and any config keys those generators read. `meta gen` runs whatever `generators: [...]` holds: the package's generator where you imported it, your copy where you ejected it. @@ -115,7 +115,9 @@ Flags: - `--force` — overwrite scaffold files (memory records preserved) - `--quiet` — suppress next-steps output - `--print-only` — show what would be created without writing -- `--refresh-docs` — refresh AGENTS.md + CLAUDE.md after a CLI upgrade +- `--refresh-docs` — refresh `.metaobjects/AGENTS.md` and the skills after a CLI upgrade +- `--skills-dir ` — install the skills somewhere other than `.claude/skills` (Claude Code's directory), such as `.agents/skills` for assistants that read that; remembered for later refreshes +- `--no-wire-root` — do not add the `.metaobjects/AGENTS.md` import to the root `AGENTS.md` / `CLAUDE.md` - `--dialect sqlite|postgres|mysql|d1` — the database the project targets. Writes the codegen `dialect`, and `migrate.dialect` in `.metaobjects/config.json` for sqlite, postgres and d1 (`--d1` is `--dialect d1`; a conflicting pair is refused). MySQL is codegen-only: `meta migrate` does not own its schema. Default `sqlite`; the next steps print that dialect's first migration. ### `meta gen [...]` @@ -395,7 +397,7 @@ example: [docs/features/metadata-sources.md](../../../../docs/features/metadata- ## Metadata format -See `.metaobjects/AGENTS.md` (scaffolded by `meta init`) for the metaobjects metamodel rules, attribute conventions, and worked examples. Deeper references: +`.metaobjects/AGENTS.md` (scaffolded by `meta init`) holds the short working loop and rules; the `metaobjects-authoring` skill it points to has the metamodel detail and worked examples. Deeper references: - `@metaobjectsdev/metadata` [METAMODEL.md](../metadata/METAMODEL.md) — full metaobjects metamodel reference - `@metaobjectsdev/sdk` [FORGE-METADATA.md](../sdk/FORGE-METADATA.md) — MetaObjects metadata additions diff --git a/server/typescript/packages/cli/src/commands/gen.ts b/server/typescript/packages/cli/src/commands/gen.ts index 16550c3926..0519e45c06 100644 --- a/server/typescript/packages/cli/src/commands/gen.ts +++ b/server/typescript/packages/cli/src/commands/gen.ts @@ -129,7 +129,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat // metadata from unrelated trees. Identical object when the two configs sit together. const genCollection = await resolveGenCollection(collection, projectRoot); - // Advisory: nudge to refresh the .claude/skills docs if they predate this CLI. + // Advisory: nudge to refresh the agent context and skills if they predate this CLI. // Rooted at `projectRoot`, not ambient cwd — the scaffolded agent context sits // with the project that declares the metadata, so a run from a subdirectory // would find no manifest there and silently skip the nudge. `meta verify` makes diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index d9cc1791fd..d013150029 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -5,7 +5,7 @@ import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from " import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, LOCK_FILE } from "@metaobjectsdev/sdk"; import { assemble, resolveAgentContextRoot, planScaffold, - AGENT_CONTEXT_MANIFEST_PATH, type Manifest, type Stack, + AGENT_CONTEXT_MANIFEST_PATH, DEFAULT_SKILLS_DIR, normalizeSkillsDir, type Manifest, type Stack, } from "@metaobjectsdev/sdk/agent-context"; import { assertKnownStackValues, resolveStack } from "../lib/detect-stack.js"; import { reportIgnoredScaffold } from "../lib/ignored-scaffold-check.js"; @@ -207,6 +207,13 @@ export interface InitOptions { servers?: string[]; clients?: string[]; noSkills?: boolean; + /** + * `--skills-dir`: where the skills install, relative to the project root. Unset reuses what + * the prior manifest recorded, then {@link DEFAULT_SKILLS_DIR}. Claude Code reads + * `.claude/skills`; other assistants read their own directory (`.agents/skills` is shared + * by several), and the always-on text names the choice either way. + */ + skillsDir?: string; wireRoot?: boolean; /** Scaffold ONLY the agent-context (always-on + skills + root wiring), skipping the metaobjects/ project scaffold — for dropping context into an existing/polyglot repo. */ docsOnly?: boolean; @@ -279,9 +286,9 @@ function findGitRoot(start: string): string | undefined { /** * Issue #77 — Claude Code discovers `.claude/skills/` only from cwd + ANCESTOR - * dirs + the user level; it never walks DOWN into subdirs. So scaffolding the - * agent-context into a monorepo subdir means a root-launched session won't load - * the skills (the common case). When the init dir is inside a git repo whose + * dirs + the user level, and the assistants that read `.agents/skills/` search the + * same way; none walks DOWN into subdirs. So scaffolding the agent-context into a + * monorepo subdir means a root-launched session won't load the skills (the common case). When the init dir is inside a git repo whose * root is an ANCESTOR (i.e. a subdir init), warn and point the user at the repo * root. The metadata/config/migrations correctly stay in the subdir regardless. */ @@ -292,7 +299,8 @@ function warnIfMonorepoSubdir(opts: InitOptions, result: InitResult): void { const lang = opts.servers && opts.servers.length > 0 ? opts.servers[0]! : ""; result.warnings.push( "agent-context skills scaffolded into a monorepo subdir won't be discovered from a " + - "root-launched session (Claude Code only walks cwd + ancestors). Scaffold the context " + + "root-launched session (assistants search the working directory and its ancestors, not " + + "subdirectories). Scaffold the context " + `at the repo root instead: cd && meta init --docs-only --server ${lang}`, ); } @@ -347,8 +355,11 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise warnIfMonorepoSubdir(opts, result); const prior = await readManifest(opts.cwd); const stack = await stackForAgentContext(opts, prior); - let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack, docsRef: docsRefForVersion(cliVersion()) }); - if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(".claude/skills/")); + // An explicit --skills-dir wins; otherwise the directory this project chose last time, so a + // refresh never quietly moves the skills back to the default. + const skillsDir = normalizeSkillsDir(opts.skillsDir ?? prior?.skillsDir ?? DEFAULT_SKILLS_DIR); + let assembled = assemble({ contentRoot: resolveAgentContextRoot(), stack, docsRef: docsRefForVersion(cliVersion()), skillsDir }); + if (opts.noSkills) assembled = assembled.filter((f) => !f.path.startsWith(`${skillsDir}/`)); const generatedBy = cliVersion(); const contentHash = currentContentHash().currentContentHash; @@ -360,6 +371,7 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise }, generatedBy, ...(contentHash !== undefined && { contentHash }), + skillsDir, }); // A pre-release CLI stamps its own pre-release version into the committed // .metaobjects/.agent-context.json ("generatedBy") and the skills/docs it scaffolds may @@ -434,16 +446,26 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise } const ROOT_IMPORT_LINE = "@.metaobjects/AGENTS.md"; +/** + * What the root file gains. The `@` line is Claude Code's import; the sentence before it is + * what any other assistant acts on, since only Claude Code expands an `@` path. + */ +const ROOT_IMPORT_BLOCK = + "Read `.metaobjects/AGENTS.md` before changing this project's metadata, generated code or schema.\n" + + `${ROOT_IMPORT_LINE}\n`; async function wireRootMemory(cwd: string, result: InitResult, dryRun = false): Promise { const claudePath = join(cwd, "CLAUDE.md"); const agentsPath = join(cwd, "AGENTS.md"); const claudeExists = await fileExists(claudePath); const agentsExists = await fileExists(agentsPath); - // If neither root memory file exists, create CLAUDE.md (Claude Code's canonical) with the import. + // If neither root memory file exists, create both: AGENTS.md for the assistants that read it, + // CLAUDE.md for Claude Code. if (!claudeExists && !agentsExists) { - await writeUnlessDryRun(cwd, dryRun, "CLAUDE.md", `# Project memory\n\n${ROOT_IMPORT_LINE}\n`); - result.created.push(`CLAUDE.md (${verbed(dryRun, "created")} with MetaObjects @import)`); + for (const target of ["AGENTS.md", "CLAUDE.md"]) { + await writeUnlessDryRun(cwd, dryRun, target, `# Project memory\n\n${ROOT_IMPORT_BLOCK}`); + result.created.push(`${target} (${verbed(dryRun, "created")} with MetaObjects @import)`); + } return; } // Otherwise append the import to whichever exist (idempotent — never double-add). @@ -452,7 +474,7 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false): const body = await readFile(path, "utf8"); if (body.includes(ROOT_IMPORT_LINE)) continue; const target = path.endsWith("AGENTS.md") ? "AGENTS.md" : "CLAUDE.md"; - await writeUnlessDryRun(cwd, dryRun, target, `${body.replace(/\n*$/, "\n")}\n${ROOT_IMPORT_LINE}\n`); + await writeUnlessDryRun(cwd, dryRun, target, `${body.replace(/\n*$/, "\n")}\n${ROOT_IMPORT_BLOCK}`); // Past tense only when it actually happened — this one mutates a file the user // owns, so a dry run reporting it as done is the most misleading of the three. result.warnings.push(`${verbed(dryRun, "wired")} ${ROOT_IMPORT_LINE} into ${target} so the MetaObjects context loads`); @@ -609,6 +631,7 @@ export async function init(opts: InitOptions): Promise { // generated context. assertKnownStackValues({ servers: opts.servers ?? [], clients: opts.clients ?? [] }); resolveInitDialect(opts); + if (opts.skillsDir !== undefined) normalizeSkillsDir(opts.skillsDir); const result: InitResult = { created: [], preserved: [], removed: [], warnings: [] }; const agentDir = join(opts.cwd, DEFAULT_METAOBJECTS_DIR); const metaobjectsDir = join(opts.cwd, DEFAULT_METADATA_DIR); @@ -941,6 +964,7 @@ export async function initCommand(args: string[], cwd: string): Promise // init failure gets. `init()` refuses it too, which is what covers every other door. assertKnownStackValues({ servers: flags.servers ?? [], clients: flags.clients ?? [] }); resolveInitDialect(flags); + if (flags.skillsDir !== undefined) normalizeSkillsDir(flags.skillsDir); } catch (err) { log.error(describeError(err)); return 2; @@ -958,6 +982,7 @@ export async function initCommand(args: string[], cwd: string): Promise servers: flags.servers, clients: flags.clients, noSkills: flags.noSkills, + ...(flags.skillsDir !== undefined && { skillsDir: flags.skillsDir }), wireRoot: flags.wireRoot, docsOnly: flags.docsOnly, configOnly: flags.configOnly, @@ -990,14 +1015,14 @@ export async function initCommand(args: string[], cwd: string): Promise // was available from the outputs this command had just written. reportIgnoredScaffold(cwd, result.created); if (flags.docsOnly) { - log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.`); + log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + the metaobjects-* skills.`); for (const w of result.warnings) log.info(` ${w}`); - log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import."); + log.info("Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root AGENTS.md/CLAUDE.md @import."); } else if (result.refreshedDocsOnly) { // Report the refresh, not a scaffold. `result.refreshedDocsOnly` (not `flags.refreshDocs`) // because --refresh-docs on an uninitialized repo falls through to a full init, which // genuinely DID scaffold and must still say so. - log.info(`Refreshed the MetaObjects agent context in place (${result.created.length} files): .metaobjects/AGENTS.md + .metaobjects/CLAUDE.md + .claude/skills/metaobjects-*.`); + log.info(`Refreshed the MetaObjects agent context in place (${result.created.length} files): .metaobjects/AGENTS.md + the metaobjects-* skills.`); log.info("Your metadata, .metaobjects/config.json, codegen/generators/ and metaobjects.config.ts were left untouched."); for (const w of result.warnings) log.warn(w); } else if (flags.configOnly) { diff --git a/server/typescript/packages/cli/src/commands/verify.ts b/server/typescript/packages/cli/src/commands/verify.ts index 3e03dd836d..60987efa20 100644 --- a/server/typescript/packages/cli/src/commands/verify.ts +++ b/server/typescript/packages/cli/src/commands/verify.ts @@ -300,7 +300,7 @@ export async function verifyCommand( // project. const genConfigDir = resolveGenConfigDir(cwd, collection.configDir); - // Advisory: nudge to refresh the .claude/skills docs if they predate this CLI. + // Advisory: nudge to refresh the agent context and skills if they predate this CLI. warnIfAgentContextStale(projectRoot); // Advisory: the committed hash manifest is what makes hand-edit detection work on a // machine that did not generate the output. Silent unless it is ignored. Keyed on diff --git a/server/typescript/packages/cli/src/index.ts b/server/typescript/packages/cli/src/index.ts index f60793bc2d..7588c84b92 100644 --- a/server/typescript/packages/cli/src/index.ts +++ b/server/typescript/packages/cli/src/index.ts @@ -30,10 +30,10 @@ USAGE: COMMANDS: init Scaffold a MetaObjects project in the current repo - init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades + init --refresh-docs Refresh .metaobjects/AGENTS.md and the skills after CLI upgrades init --with-docs Scaffold with documentation wired (docsFile(), apiDocsFile()) so the first \`meta gen\` writes it init --config-only Write only .metaobjects/config.json — for a Maven- or pip-rooted project - agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports + agent-docs Scaffold only the agent-context (.metaobjects/AGENTS.md + skills) — canonical redirect target for all language ports gen [...] Codegen TS targets from your declared metadata generator new Write a generator of your own into codegen/generators/ and wire it — the way to get any output MetaObjects does not ship (Zod, a client, …) @@ -505,7 +505,7 @@ USAGE: meta init [flags] FLAGS: - --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades + --refresh-docs Refresh .metaobjects/AGENTS.md and the skills after CLI upgrades --docs-only Write only the agent-context files — no metadata sources, no codegen scaffold, no config. Pair with --refresh-docs to update an existing project after upgrading. @@ -515,7 +515,10 @@ FLAGS: value is refused, never dropped) --client Declare a client framework for the agent context (repeatable): react | tanstack | angular - --no-skills Skip the .claude/skills/ scaffold + --no-skills Skip the skills scaffold + --skills-dir Where the skills install (default .claude/skills, which Claude Code + reads; .agents/skills for assistants that read that). Remembered + for later refreshes --force Overwrite existing files --quiet Suppress output --print-only Print what would be written, don't write @@ -524,14 +527,15 @@ FLAGS: in .metaobjects/config.json, so neither is edited by hand. d1 is --d1; mysql is codegen only (meta migrate does not own MySQL schema) --d1 Include D1 (Cloudflare) migration config (same as --dialect d1) - --no-wire-root Skip wiring root metaobjects.config.ts + --no-wire-root Skip wiring the .metaobjects/AGENTS.md import into the root + AGENTS.md / CLAUDE.md --config-only Write only .metaobjects/config.json (no TS scaffold) — declares metadata sources for the Node CLI from a Maven- or pip-rooted project --with-docs Wire docsFile() and apiDocsFile() and a docs target in the scaffolded config, so the first \`meta gen\` writes documentation --help, -h Print this help `, - "agent-docs": `meta agent-docs — scaffold the agent-context (.metaobjects/ always-on files + .claude/skills/) + "agent-docs": `meta agent-docs — scaffold the agent-context (.metaobjects/AGENTS.md + the metaobjects-* skills) USAGE: meta agent-docs [--server ]... [--client ]... [--out ] [flags] @@ -540,8 +544,10 @@ FLAGS: --server Server language (repeatable): typescript|java|kotlin|csharp|python --client Client framework (repeatable): react|tanstack|angular --out Output directory (default: current directory) - --no-skills Skip .claude/skills/ scaffold - --no-wire-root Skip wiring root CLAUDE.md @import + --no-skills Skip the skills scaffold + --skills-dir Where the skills install (default .claude/skills; .agents/skills for + assistants that read that). Remembered for later refreshes + --no-wire-root Skip wiring the import into the root AGENTS.md / CLAUDE.md --help, -h Print this help NOTE: This is the canonical scaffolder for all language ports. Non-Node CLIs redirect here. @@ -723,12 +729,13 @@ export async function run(argv: string[]): Promise { servers: flags.servers, clients: flags.clients, noSkills: flags.noSkills, + ...(flags.skillsDir !== undefined && { skillsDir: flags.skillsDir }), wireRoot: flags.wireRoot, docsOnly: true, }); - log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.`); + log.info(`Scaffolded the MetaObjects agent context (${result.created.length} files): .metaobjects/AGENTS.md + the metaobjects-* skills.`); for (const w of result.warnings) log.info(` ${w}`); - log.info("Re-run agent-docs to update; --no-wire-root to skip the root CLAUDE.md @import."); + log.info("Re-run agent-docs to update; --no-wire-root to skip the root AGENTS.md/CLAUDE.md @import."); return 0; } catch (err) { log.error(describeError(err)); diff --git a/server/typescript/packages/cli/src/lib/args.ts b/server/typescript/packages/cli/src/lib/args.ts index fc00d6c938..ea8965a757 100644 --- a/server/typescript/packages/cli/src/lib/args.ts +++ b/server/typescript/packages/cli/src/lib/args.ts @@ -17,6 +17,8 @@ export interface InitFlags { servers: string[]; clients: string[]; noSkills: boolean; + /** `--skills-dir`; validated by `init()`, not here, so every door refuses the same values. */ + skillsDir: string | undefined; wireRoot: boolean; docsOnly: boolean; configOnly: boolean; @@ -34,6 +36,7 @@ export const INIT_OPTIONS = { server: { type: "string", multiple: true }, client: { type: "string", multiple: true }, "no-skills": { type: "boolean", default: false }, + "skills-dir": { type: "string" }, "no-wire-root": { type: "boolean", default: false }, "docs-only": { type: "boolean", default: false }, "config-only": { type: "boolean", default: false }, @@ -57,6 +60,7 @@ export function parseInitArgs(argv: string[]): InitFlags { servers: (values.server as string[] | undefined) ?? [], clients: (values.client as string[] | undefined) ?? [], noSkills: !!values["no-skills"], + skillsDir: values["skills-dir"] as string | undefined, wireRoot: !values["no-wire-root"], docsOnly: !!values["docs-only"], configOnly: !!values["config-only"], @@ -73,6 +77,7 @@ export interface AgentDocsFlags { clients: string[]; out: string | undefined; noSkills: boolean; + skillsDir: string | undefined; wireRoot: boolean; } @@ -82,6 +87,7 @@ export const AGENT_DOCS_OPTIONS = { client: { type: "string", multiple: true }, out: { type: "string" }, "no-skills": { type: "boolean", default: false }, + "skills-dir": { type: "string" }, "no-wire-root": { type: "boolean", default: false }, } as const; @@ -97,6 +103,7 @@ export function parseAgentDocsArgs(argv: string[]): AgentDocsFlags { clients: (values.client as string[] | undefined) ?? [], out: values.out as string | undefined, noSkills: !!values["no-skills"], + skillsDir: values["skills-dir"] as string | undefined, wireRoot: !values["no-wire-root"], }; } diff --git a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap index 6e8edb3474..07c38e7a02 100644 --- a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap +++ b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap @@ -8,10 +8,10 @@ USAGE: COMMANDS: init Scaffold a MetaObjects project in the current repo - init --refresh-docs Refresh .metaobjects/AGENTS.md + CLAUDE.md after CLI upgrades + init --refresh-docs Refresh .metaobjects/AGENTS.md and the skills after CLI upgrades init --with-docs Scaffold with documentation wired (docsFile(), apiDocsFile()) so the first \`meta gen\` writes it init --config-only Write only .metaobjects/config.json — for a Maven- or pip-rooted project - agent-docs Scaffold only the agent-context (.metaobjects/ + .claude/skills/) — canonical redirect target for all language ports + agent-docs Scaffold only the agent-context (.metaobjects/AGENTS.md + skills) — canonical redirect target for all language ports gen [...] Codegen TS targets from your declared metadata generator new Write a generator of your own into codegen/generators/ and wire it — the way to get any output MetaObjects does not ship (Zod, a client, …) diff --git a/server/typescript/packages/cli/test/cli.test.ts b/server/typescript/packages/cli/test/cli.test.ts index 7ae077ae7a..968f235ebf 100644 --- a/server/typescript/packages/cli/test/cli.test.ts +++ b/server/typescript/packages/cli/test/cli.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; // wireRoot defaults TRUE (root-wiring is on by default so the scaffolded always-on actually loads; --no-wire-root opts out). -const defaultInitFlags = { force: false, quiet: false, printOnly: false, refreshDocs: false, d1: false, dialect: undefined, servers: [], clients: [], noSkills: false, wireRoot: true, docsOnly: false, configOnly: false, withDocs: false }; +const defaultInitFlags = { force: false, quiet: false, printOnly: false, refreshDocs: false, d1: false, dialect: undefined, servers: [], clients: [], noSkills: false, wireRoot: true, docsOnly: false, configOnly: false, withDocs: false, skillsDir: undefined }; describe("parseInitArgs", () => { test("default flags (wireRoot on by default, others off)", () => { diff --git a/server/typescript/packages/cli/test/unit/init-agent-context.test.ts b/server/typescript/packages/cli/test/unit/init-agent-context.test.ts index ab276a407c..30b77dcba4 100644 --- a/server/typescript/packages/cli/test/unit/init-agent-context.test.ts +++ b/server/typescript/packages/cli/test/unit/init-agent-context.test.ts @@ -14,7 +14,8 @@ describe("init() — agent-context scaffolding", () => { test("scaffolds always-on + skills + the stack's language reference fragments + a manifest", async () => { await init({ cwd, servers: javaReact.servers, clients: javaReact.clients }); expect(existsSync(join(cwd, ".metaobjects/AGENTS.md"))).toBe(true); - expect(existsSync(join(cwd, ".metaobjects/CLAUDE.md"))).toBe(true); + // One always-on file: the root AGENTS.md / CLAUDE.md import it, so no duplicate is written. + expect(existsSync(join(cwd, ".metaobjects/CLAUDE.md"))).toBe(false); expect(existsSync(join(cwd, ".claude/skills/metaobjects-codegen/SKILL.md"))).toBe(true); // stack-scoped: only the declared stack's language references install (058be51c); // this stack is java + react, so java.md installs and the other language refs do not. @@ -61,9 +62,57 @@ describe("init() — agent-context scaffolding", () => { const after = readFileSync(join(cwd, "CLAUDE.md"), "utf8"); expect(after.match(/@\.metaobjects\/AGENTS\.md/g)?.length).toBe(1); // not double-added }); - test("--wire-root creates a root CLAUDE.md with the @import when none exists", async () => { + test("--wire-root creates a root AGENTS.md and CLAUDE.md with the @import when neither exists", async () => { await init({ cwd, servers: javaReact.servers, clients: javaReact.clients, wireRoot: true }); - expect(existsSync(join(cwd, "CLAUDE.md"))).toBe(true); - expect(readFileSync(join(cwd, "CLAUDE.md"), "utf8")).toContain("@.metaobjects/AGENTS.md"); + for (const root of ["AGENTS.md", "CLAUDE.md"]) { + const body = readFileSync(join(cwd, root), "utf8"); + expect(body).toContain("@.metaobjects/AGENTS.md"); + // The plain sentence is what an assistant that does not expand `@` imports acts on. + expect(body).toContain("Read `.metaobjects/AGENTS.md` before changing"); + } + }); + test("--wire-root with only a root AGENTS.md wires that and creates no CLAUDE.md", async () => { + writeFileSync(join(cwd, "AGENTS.md"), "# team rules\n", "utf8"); + await init({ cwd, servers: javaReact.servers, clients: javaReact.clients, wireRoot: true }); + expect(readFileSync(join(cwd, "AGENTS.md"), "utf8")).toContain("@.metaobjects/AGENTS.md"); + expect(existsSync(join(cwd, "CLAUDE.md"))).toBe(false); + }); + + test("--skills-dir installs the skills there, names it in the always-on, and a refresh keeps it", async () => { + await init({ cwd, servers: ["python"], clients: [], skillsDir: ".agents/skills" }); + expect(existsSync(join(cwd, ".agents/skills/metaobjects-codegen/SKILL.md"))).toBe(true); + expect(existsSync(join(cwd, ".claude/skills"))).toBe(false); + expect(readFileSync(join(cwd, ".metaobjects/AGENTS.md"), "utf8")).toContain("`.agents/skills//SKILL.md`"); + // No flag on the refresh: the manifest's recorded directory wins over the default. + await init({ cwd, refreshDocs: true, docsOnly: true }); + expect(existsSync(join(cwd, ".claude/skills"))).toBe(false); + expect(existsSync(join(cwd, ".agents/skills/metaobjects-codegen/SKILL.md"))).toBe(true); + }); + + test("moving the skills with --skills-dir prunes the untouched copies at the old path", async () => { + await init({ cwd, servers: ["python"], clients: [] }); + expect(existsSync(join(cwd, ".claude/skills/metaobjects-codegen/SKILL.md"))).toBe(true); + await init({ cwd, refreshDocs: true, docsOnly: true, skillsDir: ".agents/skills" }); + expect(existsSync(join(cwd, ".agents/skills/metaobjects-codegen/SKILL.md"))).toBe(true); + expect(existsSync(join(cwd, ".claude/skills/metaobjects-codegen/SKILL.md"))).toBe(false); + }); + + test("a --skills-dir outside the project is refused before anything is written", async () => { + await expect(init({ cwd, servers: ["python"], clients: [], skillsDir: "../elsewhere" })).rejects.toThrow(/--skills-dir/); + expect(existsSync(join(cwd, ".metaobjects"))).toBe(false); + }); + + test("a refresh removes the duplicate .metaobjects/CLAUDE.md an earlier release wrote", async () => { + await init({ cwd, servers: ["python"], clients: [] }); + // Simulate the earlier layout: the duplicate on disk and tracked in the manifest. + const agents = readFileSync(join(cwd, ".metaobjects/AGENTS.md"), "utf8"); + writeFileSync(join(cwd, ".metaobjects/CLAUDE.md"), agents, "utf8"); + const mPath = join(cwd, ".metaobjects/.agent-context.json"); + const m = JSON.parse(readFileSync(mPath, "utf8")); + m.files[".metaobjects/CLAUDE.md"] = m.files[".metaobjects/AGENTS.md"]; + writeFileSync(mPath, JSON.stringify(m, null, 2), "utf8"); + const r = await init({ cwd, refreshDocs: true, docsOnly: true }); + expect(existsSync(join(cwd, ".metaobjects/CLAUDE.md"))).toBe(false); + expect(r.removed).toContain(".metaobjects/CLAUDE.md"); }); }); diff --git a/server/typescript/packages/cli/test/unit/init-refresh-docs.test.ts b/server/typescript/packages/cli/test/unit/init-refresh-docs.test.ts index f7968edee9..64f14ba05e 100644 --- a/server/typescript/packages/cli/test/unit/init-refresh-docs.test.ts +++ b/server/typescript/packages/cli/test/unit/init-refresh-docs.test.ts @@ -13,24 +13,17 @@ afterEach(() => { }); // NOTE: the legacy single-blob AGENT_DOCS_BODY was replaced by the agent-context -// assembler (always-on AGENTS.md/CLAUDE.md + per-stack skill fragments + a manifest). +// assembler (always-on AGENTS.md + per-stack skill fragments + a manifest). // The detailed authoring/codegen/tanstack/filter/projection/currency prose now lives // in the `metaobjects-*` skill reference fragments, NOT in the always-on body — so // the old content-blob assertions are gone. These tests cover the always-on body and // the non-destructive refresh contract. describe("init writes agent docs", () => { - test("writes AGENTS.md and CLAUDE.md under .metaobjects/", async () => { + test("writes AGENTS.md, and no duplicate CLAUDE.md, under .metaobjects/", async () => { await init({ cwd }); expect(existsSync(join(cwd, ".metaobjects", "AGENTS.md"))).toBe(true); - expect(existsSync(join(cwd, ".metaobjects", "CLAUDE.md"))).toBe(true); - }); - - test("AGENTS.md and CLAUDE.md have identical content", async () => { - await init({ cwd }); - const a = readFileSync(join(cwd, ".metaobjects", "AGENTS.md"), "utf8"); - const c = readFileSync(join(cwd, ".metaobjects", "CLAUDE.md"), "utf8"); - expect(a).toBe(c); + expect(existsSync(join(cwd, ".metaobjects", "CLAUDE.md"))).toBe(false); }); test("scaffolds the agent-context manifest", async () => { diff --git a/server/typescript/packages/forge/README.md b/server/typescript/packages/forge/README.md index 583c84dfd2..37b29b253c 100644 --- a/server/typescript/packages/forge/README.md +++ b/server/typescript/packages/forge/README.md @@ -16,8 +16,8 @@ nothing here should accumulate dependencies from the published packages. If you are looking for: -- **AI-assistant context files** (`.metaobjects/AGENTS.md`, `.metaobjects/CLAUDE.md`, - `.claude/skills/metaobjects-*/`) → `meta agent-docs`, implemented in +- **AI-assistant context files** (`.metaobjects/AGENTS.md` and the `metaobjects-*` + skills) → `meta agent-docs`, implemented in `@metaobjectsdev/sdk`. - **Prompt construction** (declared payloads, deterministic render, drift checks) → [`docs/features/templates-and-payloads.md`](../../../../docs/features/templates-and-payloads.md). diff --git a/server/typescript/packages/sdk/README.md b/server/typescript/packages/sdk/README.md index 8e3d41ac55..4f9315fd7b 100644 --- a/server/typescript/packages/sdk/README.md +++ b/server/typescript/packages/sdk/README.md @@ -23,8 +23,9 @@ const root = await loadMemory(collection.configDir, { files: collection.files }) ## agent-context `@metaobjectsdev/sdk/agent-context` assembles the downstream AI-assistant context -(the slim `.metaobjects/AGENTS.md`/`CLAUDE.md` + the six `metaobjects-*` Claude -skills with only the project's language reference fragments) from the repo-root +(the slim `.metaobjects/AGENTS.md` + the six `metaobjects-*` skills with only the +project's language reference fragments, under `.claude/skills/` or a `skillsDir` the caller +names) from the repo-root `agent-context/` source tree. `makeStack`/`detectStack` resolve the project's server+client axes; `assemble({ contentRoot, stack })` emits the files. Design: `docs/superpowers/specs/2026-06-02-downstream-agent-context-design.md`. diff --git a/server/typescript/packages/sdk/src/agent-context/assemble.ts b/server/typescript/packages/sdk/src/agent-context/assemble.ts index a612deb1aa..5f9ed3a324 100644 --- a/server/typescript/packages/sdk/src/agent-context/assemble.ts +++ b/server/typescript/packages/sdk/src/agent-context/assemble.ts @@ -2,7 +2,8 @@ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs"; import { join } from "node:path"; import { pinDocsRefs } from "@metaobjectsdev/metadata"; import { - SKILL_NAMES, SERVER_LANGS, CLIENT_FRAMEWORKS, type AssembledFile, type Stack, + SKILL_NAMES, SERVER_LANGS, CLIENT_FRAMEWORKS, DEFAULT_SKILLS_DIR, normalizeSkillsDir, + type AssembledFile, type Stack, } from "./types.js"; // Language/framework reference fragments are stack-scoped, and so is a `-.md` @@ -134,27 +135,37 @@ function applyTemplate( * `main`, which is what the content tree is authored with). The scaffolder passes the tag * of the running release (`docsRefForVersion`), so the links an agent follows are the * guides of the version that wrote them and do not move after the cut. + * + * `skillsDir` is where the skills install (default {@link DEFAULT_SKILLS_DIR}); the always-on + * text names it, so an assistant that does not load skills can open them by path. */ -export function assemble(opts: { contentRoot: string; stack: Stack; docsRef?: string }): AssembledFile[] { +export function assemble(opts: { contentRoot: string; stack: Stack; docsRef?: string; skillsDir?: string }): AssembledFile[] { const { contentRoot, stack } = opts; const docsRef = opts.docsRef ?? "main"; + const skillsDir = normalizeSkillsDir(opts.skillsDir ?? DEFAULT_SKILLS_DIR); const out: AssembledFile[] = []; - // 1. Always-on (AGENTS.md + CLAUDE.md, identical contents). + // 1. Always-on: one file, `.metaobjects/AGENTS.md`. Every assistant reads it through the root + // AGENTS.md or CLAUDE.md that `meta init` wires; a second identical `.metaobjects/CLAUDE.md` + // used to be written beside it, which nothing imported. const tpl = readFileSync(join(contentRoot, "templates", "always-on.md.mustache"), "utf8"); const alwaysOn = applyTemplate( tpl, - { stackLine: stackLine(stack), stackLoops: stackLoops(contentRoot, stack), verifySteps: verifySteps(contentRoot, stack) }, + { + stackLine: stackLine(stack), + stackLoops: stackLoops(contentRoot, stack), + verifySteps: verifySteps(contentRoot, stack), + skillsDir, + }, {}, ); out.push({ path: ".metaobjects/AGENTS.md", contents: alwaysOn }); - out.push({ path: ".metaobjects/CLAUDE.md", contents: alwaysOn }); // 2. Skills: body + only the references whose token is in the stack. for (const skill of SKILL_NAMES) { const skillDir = join(contentRoot, "skills", skill); const body = readFileSync(join(skillDir, "SKILL.md"), "utf8"); - out.push({ path: `.claude/skills/${skill}/SKILL.md`, contents: body }); + out.push({ path: `${skillsDir}/${skill}/SKILL.md`, contents: body }); const refDir = join(skillDir, "references"); if (existsSync(refDir) && statSync(refDir).isDirectory()) { @@ -171,7 +182,7 @@ export function assemble(opts: { contentRoot: string; stack: Stack; docsRef?: st const scope = fragmentScope(token); if (scope !== undefined && !stack.tokens.has(scope)) continue; out.push({ - path: `.claude/skills/${skill}/references/${token}.md`, + path: `${skillsDir}/${skill}/references/${token}.md`, contents: readFileSync(join(refDir, `${token}.md`), "utf8"), }); } diff --git a/server/typescript/packages/sdk/src/agent-context/scaffold.ts b/server/typescript/packages/sdk/src/agent-context/scaffold.ts index b99341d827..5d24a6bccb 100644 --- a/server/typescript/packages/sdk/src/agent-context/scaffold.ts +++ b/server/typescript/packages/sdk/src/agent-context/scaffold.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative, sep } from "node:path"; -import type { AssembledFile, Stack } from "./types.js"; +import { DEFAULT_SKILLS_DIR, type AssembledFile, type Stack } from "./types.js"; import { docsBlobUrl, docsRefForVersion } from "@metaobjectsdev/metadata"; /** Consumer-relative path of the sidecar manifest that tracks scaffolded files. */ @@ -26,6 +26,11 @@ export interface Manifest { contentHash?: string; servers: string[]; clients: string[]; + /** + * Where the skills were installed, when it is not {@link DEFAULT_SKILLS_DIR}. A refresh + * reuses it, so a project that chose `--skills-dir` once keeps it without repeating the flag. + */ + skillsDir?: string; /** consumer-relative path → sha256 of the contents as last scaffolded. */ files: Record; } @@ -93,8 +98,10 @@ export function planScaffold(opts: { generatedBy: string; /** {@link hashAgentContextRoot} of the content being scaffolded — stamped into the manifest. */ contentHash?: string; + /** Where the skills were assembled to — stamped into the manifest when not the default. */ + skillsDir?: string; }): ScaffoldDecision { - const { stack, assembled, prior, readCurrent, generatedBy, contentHash } = opts; + const { stack, assembled, prior, readCurrent, generatedBy, contentHash, skillsDir } = opts; const writes: ScaffoldDecision["writes"] = []; const conflicts: ScaffoldDecision["conflicts"] = []; const files: Record = {}; @@ -150,7 +157,12 @@ export function planScaffold(opts: { conflicts, // `files` holds only what this run assembled, so a pruned orphan does not survive into // the new manifest — otherwise it would be re-reported as an orphan on every run. - manifest: { version: 1, generatedBy, ...(contentHash !== undefined && { contentHash }), servers: stack.servers, clients: stack.clients, files }, + manifest: { + version: 1, generatedBy, ...(contentHash !== undefined && { contentHash }), + servers: stack.servers, clients: stack.clients, + ...(skillsDir !== undefined && skillsDir !== DEFAULT_SKILLS_DIR && { skillsDir }), + files, + }, prunes, removed, }; @@ -279,7 +291,7 @@ export function agentContextStaleness(opts: { const guides = minorStepGuides(manifest.generatedBy, currentVersion); return ( `MetaObjects agent context was generated by ${from}; you're on ${currentVersion}. ` + - `Re-run 'meta init --docs-only --refresh-docs' to refresh the .claude/skills docs.` + + `Re-run 'meta init --docs-only --refresh-docs' to refresh the agent context and its skills.` + (guides.length > 0 ? ` This is a minor upgrade: read the upgrade guide first (${guides.join("; ")}).` : "") ); } @@ -305,7 +317,8 @@ export function agentContextCoverage(opts: { const { manifest, declaresRequirements } = opts; if (manifest === undefined || !declaresRequirements) return null; const paths = Object.keys(manifest.files); - if (!paths.some((p) => p.startsWith(".claude/skills/"))) return null; + // Any installed SKILL.md means skills were scaffolded, wherever `--skills-dir` put them. + if (!paths.some((p) => p.endsWith("/SKILL.md"))) return null; if (paths.some((p) => p.endsWith(REQUIREMENTS_PAGE_SUFFIX))) return null; return ( "The model declares requirement.* nodes, but the MetaObjects agent context was scaffolded " + diff --git a/server/typescript/packages/sdk/src/agent-context/types.ts b/server/typescript/packages/sdk/src/agent-context/types.ts index 3827704cb0..24dda99d2d 100644 --- a/server/typescript/packages/sdk/src/agent-context/types.ts +++ b/server/typescript/packages/sdk/src/agent-context/types.ts @@ -30,3 +30,29 @@ export interface AssembledFile { path: string; // e.g. ".metaobjects/AGENTS.md", ".claude/skills/metaobjects-codegen/references/java.md" contents: string; } + +/** + * Where the skills install unless the project chose otherwise: the directory Claude Code reads. + * Any other assistant takes `--skills-dir` (`.agents/skills` is the shared location several + * read), and the always-on text names the chosen directory so an assistant that does not load + * skills can still open them by path. + */ +export const DEFAULT_SKILLS_DIR = ".claude/skills"; + +/** + * A skills directory as a clean project-relative POSIX path, or a thrown error naming what is + * wrong. Refuses an absolute path and any `..` segment: the scaffolder writes and prunes files + * under it, so it must stay inside the project. + */ +export function normalizeSkillsDir(raw: string): string { + const posix = raw.trim().replace(/\\/g, "/"); + if (posix.startsWith("/") || /^[A-Za-z]:\//.test(posix)) { + throw new Error(`--skills-dir must be relative to the project root, got '${raw}'`); + } + const segments = posix.split("/").filter((s) => s !== "" && s !== "."); + if (segments.length === 0) throw new Error("--skills-dir must name a directory inside the project"); + if (segments.includes("..")) { + throw new Error(`--skills-dir must stay inside the project (no '..'), got '${raw}'`); + } + return segments.join("/"); +} diff --git a/server/typescript/packages/sdk/test/agent-context/assemble.test.ts b/server/typescript/packages/sdk/test/agent-context/assemble.test.ts index 7d36e6f87d..3822fb87fb 100644 --- a/server/typescript/packages/sdk/test/agent-context/assemble.test.ts +++ b/server/typescript/packages/sdk/test/agent-context/assemble.test.ts @@ -27,13 +27,13 @@ describe("assemble pins guide links", () => { }); describe("assemble", () => { - test("emits AGENTS.md + CLAUDE.md with the stack line + codegen command substituted", () => { + test("emits one AGENTS.md (no duplicate CLAUDE.md) with the stack line + codegen command substituted", () => { const stack = makeStack(["typescript"], ["react"]); const files = assemble({ contentRoot: CONTENT_ROOT, stack }); const agents = files.find((f) => f.path === ".metaobjects/AGENTS.md")!; - const claude = files.find((f) => f.path === ".metaobjects/CLAUDE.md")!; expect(agents).toBeDefined(); - expect(claude.contents).toBe(agents.contents); + // The root AGENTS.md / CLAUDE.md import this one file; a second identical copy was noise. + expect(files.find((f) => f.path === ".metaobjects/CLAUDE.md")).toBeUndefined(); expect(agents.contents).not.toContain("{{stackLine}}"); expect(agents.contents).not.toContain("{{codegenCommand}}"); expect(agents.contents).toContain("npx meta gen"); @@ -65,7 +65,8 @@ describe("assemble", () => { const java = onAgents(["java"], []); expect(java).toContain("`mvn metaobjects:generate`"); - expect(java).toContain("JavaApiDocsGenerator"); + // The generator class list lives in the codegen skill; the loop points at it. + expect(java).toContain("`metaobjects-codegen` skill's `references/java.md`"); expect(java).not.toContain("### Kotlin"); // A polyglot stack gets each declared port, in SERVER_LANGS order, and the client step. @@ -89,16 +90,44 @@ describe("assemble", () => { expect(onAgents([], [])).toContain("meta init --docs-only --refresh-docs --server"); }); - test("the always-on never tells an agent to cat or read a skill file", () => { - // Skills are loaded by name. Reading SKILL.md by path bypasses that, costs the whole file, - // and teaches the agent that the loop needs a skill when it does not. + test("the always-on loads skills by name, and offers a path only to assistants that cannot", () => { + // Skills are loaded by name; reading SKILL.md by path bypasses that and costs the whole + // file. The one path the text gives is the fallback for an assistant with no skill + // loading, which would otherwise have no way in. const c = assemble({ contentRoot: CONTENT_ROOT, stack: makeStack(["typescript"], ["react"]) }) .find((f) => f.path === ".metaobjects/AGENTS.md")!.contents; - expect(c).not.toContain("SKILL.md"); + expect(c.match(/SKILL\.md/g)?.length).toBe(1); + expect(c).toContain("load a `metaobjects-*` skill by name, or open\n`.claude/skills//SKILL.md` if your assistant does not load skills"); expect(c).not.toMatch(/\bcat\b/); expect(c).toContain("The loop above needs no skill."); }); + test("--skills-dir moves every skill file and the always-on names the chosen directory", () => { + const files = assemble({ contentRoot: CONTENT_ROOT, stack: makeStack(["python"], []), skillsDir: "./.agents/skills/" }); + const skillPaths = files.map((f) => f.path).filter((p) => p.endsWith(".md") && !p.startsWith(".metaobjects/")); + expect(skillPaths.length).toBeGreaterThan(0); + for (const p of skillPaths) expect(p.startsWith(".agents/skills/metaobjects-")).toBe(true); + const c = files.find((f) => f.path === ".metaobjects/AGENTS.md")!.contents; + expect(c).toContain("`.agents/skills//SKILL.md`"); + expect(c).not.toContain(".claude/skills"); + }); + + test("a skills directory outside the project is refused", () => { + for (const bad of ["/abs/skills", "../skills", "a/../../b", "", "C:/skills"]) { + expect(() => assemble({ contentRoot: CONTENT_ROOT, stack: makeStack([], []), skillsDir: bad })).toThrow(/--skills-dir/); + } + }); + + test("the always-on states the REST conventions agents trip on", () => { + const c = assemble({ contentRoot: CONTENT_ROOT, stack: makeStack(["java"], []) }) + .find((f) => f.path === ".metaobjects/AGENTS.md")!.contents; + expect(c).toContain("`?filter[status][eq]=active`, never\n `?status=active`"); + for (const op of ["`eq`", "`ne`", "`isNull`", "`in`", "`like`", "`gt`", "`gte`", "`lt`", "`lte`"]) expect(c).toContain(op); + expect(c).toContain("`?limit=N&offset=N`"); + expect(c).toContain("`invalid_sort`"); + expect(c).toContain("so Java, C# and Python projects need Node too"); + }); + test("a malformed section tag THROWS rather than shipping into a file an agent reads", () => { // The mechanism resolves sections in one non-greedy pass, so a mistyped close tag, an // unclosed section and a nested pair all leave a literal `{{#…}}` behind. Passing that diff --git a/server/typescript/packages/sdk/test/agent-context/scaffold.test.ts b/server/typescript/packages/sdk/test/agent-context/scaffold.test.ts index 252e26f126..4ae5e2bdff 100644 --- a/server/typescript/packages/sdk/test/agent-context/scaffold.test.ts +++ b/server/typescript/packages/sdk/test/agent-context/scaffold.test.ts @@ -311,5 +311,17 @@ describe("agentContextCoverage", () => { test("silent with no manifest, or one scaffolded with --no-skills", () => { expect(agentContextCoverage({ manifest: undefined, declaresRequirements: true })).toBeNull(); expect(agentContextCoverage({ manifest: withFiles([".metaobjects/AGENTS.md"]), declaresRequirements: true })).toBeNull(); + }); test("skills under a --skills-dir count as scaffolded skills", () => { + const w = agentContextCoverage({ manifest: withFiles([".agents/skills/metaobjects-authoring/SKILL.md"]), declaresRequirements: true }); + expect(w).toContain("requirement.* nodes"); + }); +}); + +describe("planScaffold records the skills directory", () => { + test("a chosen directory is recorded so a refresh keeps it; the default is not", () => { + const chosen = planScaffold({ stack, assembled: files, prior: undefined, readCurrent: () => undefined, generatedBy: "1.3.0", skillsDir: ".agents/skills" }); + expect(chosen.manifest.skillsDir).toBe(".agents/skills"); + const dflt = planScaffold({ stack, assembled: files, prior: undefined, readCurrent: () => undefined, generatedBy: "1.3.0", skillsDir: ".claude/skills" }); + expect(dflt.manifest.skillsDir).toBeUndefined(); }); }); From 24389423e42c5ae6920403efd2ce523485b45159 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 11:11:36 -0500 Subject: [PATCH 2/7] no-mistakes(review): apply reviewer inline fixes --- docs/features/language-differences.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/language-differences.md b/docs/features/language-differences.md index dbbcb6a629..27f74d8a52 100644 --- a/docs/features/language-differences.md +++ b/docs/features/language-differences.md @@ -45,7 +45,7 @@ project. | **Field names in code** | The model's names (`createdAt`) | PascalCase properties (`CreatedAt`) | The model's names | The model's names | The model's names, not snake_case (`createdAt`) | | **Field names on the wire** | The model's names, in every port | | | | | | **Decimals and null members on the wire** | Each port writes its serializer's default; a `wire` setting makes them identical. The per-port table is under [The wire setting](api-contract.md#the-wire-setting) | | | | | -| **Hand edits to a generated file** | Merged (three-way) on the machine that generated it; refused by name elsewhere | Refused by name | Refused by name, or overwritten with `overwrite` | Same as Java | Refused by name | +| **Hand edits to a generated file** | Merged (three-way) on the machine that generated it; refused by name elsewhere | Refused by name, or overwritten with `WithHandEdits(HandEdits.Overwrite)` | Refused by name, or overwritten with `overwrite` | Same as Java | Refused by name, or overwritten with `handEdits: overwrite` | | **Own a generator** | `npx meta eject` | `dotnet meta eject` | `mvn metaobjects:eject` | `mvn metaobjects:eject` | `metaobjects eject` | | **Requirement authoring lint** | Yes (advisory) | No | No | No | No | From 1bb149dee62f32efffc44063bd378aaf5457bf23 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 11:13:42 -0500 Subject: [PATCH 3/7] no-mistakes(review): Fix agent-context default-page wording; add Kotlin --- agent-context/templates/always-on.md.mustache | 5 ++--- .../csharp/expected/.metaobjects/AGENTS.md | 5 ++--- .../expected/.metaobjects/AGENTS.md | 5 ++--- .../java-react/expected/.metaobjects/AGENTS.md | 5 ++--- .../python/expected/.metaobjects/AGENTS.md | 5 ++--- .../ts-react-tanstack/expected/.metaobjects/AGENTS.md | 5 ++--- .../packages/sdk/test/agent-context/assemble.test.ts | 2 +- 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index c2f1bb7cac..f942c0eace 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -11,7 +11,7 @@ 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, so Java, C# and Python projects need Node too) +### 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. @@ -43,8 +43,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md index e9582ac1c7..d9e60606b5 100644 --- a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md @@ -28,7 +28,7 @@ truth. Code, schema and migrations are generated from it and are disposable. 2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo to change what it emits. -### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) +### 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. @@ -61,8 +61,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index e357366045..1e428811d0 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -39,7 +39,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) +### 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. @@ -73,8 +73,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index bf2b5b0362..41c8595636 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -29,7 +29,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) +### 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. @@ -63,8 +63,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index 73871b8d20..590875c400 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -29,7 +29,7 @@ truth. Code, schema and migrations are generated from it and are disposable. 2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo to change what it emits. -### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) +### 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. @@ -62,8 +62,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index 4c1db26c29..1fb543ac57 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -40,7 +40,7 @@ from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` `baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every hook without an error. The `metaobjects-runtime-ui` skill has the detail. -### Schema (every stack: the Node `meta` CLI owns it, so Java, C# and Python projects need Node too) +### 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. @@ -73,8 +73,7 @@ Commit the metadata, the generated code, `.metaobjects/migrations/` and When the project serves the generated routes (any port): - Filter as `?filter[][]=`: `?filter[status][eq]=active`, never - `?status=active` (TypeScript refuses that with 400; the other ports ignore it and return every - row). Operators: `eq`, `ne`, `isNull` (`true`/`false`), `in` (comma list, at most 100), `like` + `?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=:asc|desc`, `?limit=N&offset=N`; `withCount=1` returns `{"rows", "total"}`. diff --git a/server/typescript/packages/sdk/test/agent-context/assemble.test.ts b/server/typescript/packages/sdk/test/agent-context/assemble.test.ts index 3822fb87fb..3410d3df3a 100644 --- a/server/typescript/packages/sdk/test/agent-context/assemble.test.ts +++ b/server/typescript/packages/sdk/test/agent-context/assemble.test.ts @@ -125,7 +125,7 @@ describe("assemble", () => { for (const op of ["`eq`", "`ne`", "`isNull`", "`in`", "`like`", "`gt`", "`gte`", "`lt`", "`lte`"]) expect(c).toContain(op); expect(c).toContain("`?limit=N&offset=N`"); expect(c).toContain("`invalid_sort`"); - expect(c).toContain("so Java, C# and Python projects need Node too"); + expect(c).toContain("so Java, Kotlin, C# and Python projects need Node too"); }); test("a malformed section tag THROWS rather than shipping into a file an agent reads", () => { From 328d5a114b7c050aee003b637e89e771d1e27e54 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 11:14:59 -0500 Subject: [PATCH 4/7] no-mistakes(review): apply reviewer inline fixes --- docs/ports/java.md | 2 +- docs/ports/kotlin.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ports/java.md b/docs/ports/java.md index c998f0b5cf..a44d2b2cb8 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -22,7 +22,7 @@ the Java diff-and-converge migration engine and its `meta:migrate` / live-DB-dri path was also removed — OMDB is pure data-access (CRUD/query/codec/transactions). Prompt / template drift is still checked via the `metaobjects-render` `Verify` API. -> **Schema migrations and database checks run in the Node `meta` tool.** the Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). +> **Schema migrations and database checks run in the Node `meta` tool.** The Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). ## Regeneration and hand edits diff --git a/docs/ports/kotlin.md b/docs/ports/kotlin.md index 08ebbd5a83..2e8bd3d110 100644 --- a/docs/ports/kotlin.md +++ b/docs/ports/kotlin.md @@ -20,7 +20,7 @@ Two modules: - **`metaobjects-metadata-ktx`** — thin Kotlin facade over the Java loader + render engine for idiomatic Kotlin runtime use. -> **Schema migrations and database checks run in the Node `meta` tool.** the Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). +> **Schema migrations and database checks run in the Node `meta` tool.** The Maven plugin generates and verifies code, but writing and applying migrations (`npx meta migrate`, `npx meta migrate apply-pending`) and checking a live database against the model (`npx meta verify --db`) need Node: `npm i -D @metaobjectsdev/cli`, then `npx meta init --config-only` to point it at your metadata. Node is needed where migrations are written or applied, not in your running service. What else behaves differently in this port is listed in [What differs by language](../features/language-differences.md). ## Regeneration and hand edits From 662300036d84341641b7d01f7f26eda2b1c0cfe1 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 13:51:08 -0500 Subject: [PATCH 5/7] no-mistakes(test): init: first init creates root AGENTS.md when only CLAUDE.md exists; refresh advises --- CHANGELOG.md | 5 +++- .../packages/cli/src/commands/init.ts | 22 ++++++++++++++-- .../cli/test/unit/init-agent-context.test.ts | 25 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78da5b1094..5ac5c20237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,10 @@ loaded before stops loading. This vocabulary ships in 1.3.0 with `metamodelVersi 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 new "Calling the + `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 diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index d013150029..9e1cb22f8c 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -442,7 +442,11 @@ async function writeAgentContext(opts: InitOptions, result: InitResult): Promise ); } - if (opts.wireRoot) await wireRootMemory(opts.cwd, result, dryRun); + // A refresh never creates a root memory file the user did not ask for; a first init may + // (see wireRootMemory). "Refresh" is the flag OR a prior agent-context manifest — `meta + // agent-docs` carries no flag of its own and reaches this same path with docsOnly. + const isRefresh = opts.refreshDocs === true || prior !== undefined; + if (opts.wireRoot) await wireRootMemory(opts.cwd, result, dryRun, isRefresh); } const ROOT_IMPORT_LINE = "@.metaobjects/AGENTS.md"; @@ -453,7 +457,7 @@ const ROOT_IMPORT_LINE = "@.metaobjects/AGENTS.md"; const ROOT_IMPORT_BLOCK = "Read `.metaobjects/AGENTS.md` before changing this project's metadata, generated code or schema.\n" + `${ROOT_IMPORT_LINE}\n`; -async function wireRootMemory(cwd: string, result: InitResult, dryRun = false): Promise { +async function wireRootMemory(cwd: string, result: InitResult, dryRun = false, isRefresh = false): Promise { const claudePath = join(cwd, "CLAUDE.md"); const agentsPath = join(cwd, "AGENTS.md"); const claudeExists = await fileExists(claudePath); @@ -468,6 +472,20 @@ async function wireRootMemory(cwd: string, result: InitResult, dryRun = false): } return; } + // A repo that predates the assistant-neutral layout has CLAUDE.md but no AGENTS.md, so every + // assistant other than Claude Code misses the context. A first init creates AGENTS.md too; a + // refresh never creates a root file — it says so once and leaves the decision to the user. + if (claudeExists && !agentsExists) { + if (!isRefresh) { + await writeUnlessDryRun(cwd, dryRun, "AGENTS.md", `# Project memory\n\n${ROOT_IMPORT_BLOCK}`); + result.created.push(`AGENTS.md (${verbed(dryRun, "created")} with MetaObjects @import)`); + } else { + result.warnings.push( + "no root AGENTS.md — assistants other than Claude Code will not load .metaobjects/AGENTS.md; " + + "add a root AGENTS.md containing @.metaobjects/AGENTS.md", + ); + } + } // Otherwise append the import to whichever exist (idempotent — never double-add). for (const [path, exists] of [[claudePath, claudeExists], [agentsPath, agentsExists]] as const) { if (!exists) continue; diff --git a/server/typescript/packages/cli/test/unit/init-agent-context.test.ts b/server/typescript/packages/cli/test/unit/init-agent-context.test.ts index 30b77dcba4..c500e071d1 100644 --- a/server/typescript/packages/cli/test/unit/init-agent-context.test.ts +++ b/server/typescript/packages/cli/test/unit/init-agent-context.test.ts @@ -78,6 +78,31 @@ describe("init() — agent-context scaffolding", () => { expect(existsSync(join(cwd, "CLAUDE.md"))).toBe(false); }); + test("--wire-root with only a root CLAUDE.md on a first init also creates AGENTS.md", async () => { + // A repo that predates the assistant-neutral layout: CLAUDE.md only. Assistants that + // read AGENTS.md would miss the context, so the first init brings the second file. + writeFileSync(join(cwd, "CLAUDE.md"), "# my notes\n\nUse pnpm.\n", "utf8"); + await init({ cwd, servers: javaReact.servers, clients: javaReact.clients, wireRoot: true }); + const claude = readFileSync(join(cwd, "CLAUDE.md"), "utf8"); + expect(claude).toContain("# my notes"); // hand-written content kept + expect(claude.match(/@\.metaobjects\/AGENTS\.md/g)?.length).toBe(1); // import appended once + const agents = readFileSync(join(cwd, "AGENTS.md"), "utf8"); + expect(agents).toContain("@.metaobjects/AGENTS.md"); + expect(agents).toContain("Read `.metaobjects/AGENTS.md` before changing"); + }); + + test("a refresh over a repo with only a root CLAUDE.md creates no AGENTS.md and advises once", async () => { + // Established project (no root files yet), then the legacy-layout CLAUDE.md appears. + await init({ cwd, servers: javaReact.servers, clients: javaReact.clients }); + writeFileSync(join(cwd, "CLAUDE.md"), "# my notes\n", "utf8"); + const r = await init({ cwd, servers: javaReact.servers, clients: javaReact.clients, wireRoot: true, refreshDocs: true }); + expect(existsSync(join(cwd, "AGENTS.md"))).toBe(false); // a refresh creates no root file + expect(readFileSync(join(cwd, "CLAUDE.md"), "utf8")).toContain("@.metaobjects/AGENTS.md"); // still wired + const advisories = r.warnings.filter((w) => w.includes("no root AGENTS.md")); + expect(advisories.length).toBe(1); // exactly once, naming the remedy + expect(advisories[0]).toContain("add a root AGENTS.md containing @.metaobjects/AGENTS.md"); + }); + test("--skills-dir installs the skills there, names it in the always-on, and a refresh keeps it", async () => { await init({ cwd, servers: ["python"], clients: [], skillsDir: ".agents/skills" }); expect(existsSync(join(cwd, ".agents/skills/metaobjects-codegen/SKILL.md"))).toBe(true); From 2ae15597b35c49ac7fe5dfe394469748ea72399b Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 14:36:37 -0500 Subject: [PATCH 6/7] no-mistakes(document): Verify new per-language docs against code; fix api-contract default-page contradiction --- docs/features/api-contract.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/features/api-contract.md b/docs/features/api-contract.md index ac530cddd1..7b51cfe4d7 100644 --- a/docs/features/api-contract.md +++ b/docs/features/api-contract.md @@ -279,8 +279,9 @@ each handler they register instead; an `HTTPException` is rethrown to your explicit `limit` on an entity — `limit=100000` is honoured as asked — except the TS opaque-view route, which refuses a `limit` above 1000 (see "TS-only error responses"). A list over a large table should therefore always send a - `limit`. A default page size is deliberately not introduced in a patch release: - it would silently truncate the lists of every existing client that relies on + `limit`. A default page size is deliberately not introduced on the mounts that + return every row — and no port's default changes in a patch release: either + would silently truncate the lists of every existing client that relies on the unpaged answer. Every per-port difference of this kind is collected in [What differs by language](language-differences.md). - `withCount=1` — opt-in flag that switches the list response from From 1d1aad19c46873c8c294397017aa5c6a76615113 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Fri, 13 Nov 2026 14:39:45 -0500 Subject: [PATCH 7/7] no-mistakes(document): Fix last Claude-only mention in codegen skill --- .../skills/metaobjects-codegen/references/principles.md | 2 +- .../.claude/skills/metaobjects-codegen/references/principles.md | 2 +- .../.claude/skills/metaobjects-codegen/references/principles.md | 2 +- .../.claude/skills/metaobjects-codegen/references/principles.md | 2 +- .../.claude/skills/metaobjects-codegen/references/principles.md | 2 +- .../.claude/skills/metaobjects-codegen/references/principles.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/agent-context/skills/metaobjects-codegen/references/principles.md b/agent-context/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/agent-context/skills/metaobjects-codegen/references/principles.md +++ b/agent-context/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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. diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2c2f61d72d..6308156c9d 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -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__`/`U__` 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__`/`U__` 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 ` 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.