From ddc689eedefbce3b2dca327684a7bd7716ed4804 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 08:14:39 -0400 Subject: [PATCH 01/10] feat(codegen-ts): stock generators are importable again; eject stays the way to customize 1.0 removed entityFile / queriesFile / routesFile / barrel from @metaobjectsdev/codegen-ts/generators so that an owned copy was the only way to run them. An assessment of an adopter found its owned copies were almost pure duplication of the reference and had already fallen behind it. The rule is now "import a stock generator to run it as shipped, eject it to change what it emits" (ADR-0034 Amendment 5). Both paths are supported and tested. TypeScript - The four factories are exported from the /generators subpath again. The config loader's two diagnostics for the removed imports are deleted with the removal. - meta gen --list prints the import line that runs each generator as shipped (wire.import / wire.entry on the JSON row). Registry entries carry factoryExport, gated against each package's real exports and against the symbol the reference template exports, so moving between the two paths is one import line. - callable and trace-helper gain reference templates and can be ejected. The trace-helper module body moves to templates/trace-helper.ts so the built-in and the reference call one composer; output is byte-identical. - meta init still wires nothing. Its scaffolded comment and next steps show the import first and eject as the way to change output. Why the removal's problem stays solved: ADR-0034 Decision 3 removed the import so an adopter would not have to fork the package to customize. meta eject, which did not exist then, now copies any reference at any time, so the exit is one command away whichever path a project starts on. Existing adopters: owned copies run unchanged, and meta eject --list and meta verify --codegen behave as before. Replacing an unmodified owned copy with the import is documented in own-your-codegen, with the diff to expect: entity and routes move their HTTP-adapter import back to the runtime package, queries and barrel regenerate nothing. "Everything ejectable" audit, after this change - TypeScript: every generator is importable. Not ejectable, each for a stated reason: template (the Mustache primitive, same ruling as the other ports), shared-model (emits a hash-pinned contract artifact), and docs / api-docs / mermaid-er (the engine behind meta docs, which has no seam for an owned copy). - C#: all ejectable except template. - Java: all ejectable except entity, extractor and template. - Kotlin: all ejectable. - Python: all ejectable except template. - Every port's generators run as shipped, selected by name. Gaps outside TypeScript are reported here, not fixed. Docs: ADR-0034 Amendment 5, own-your-codegen (the import-or-own table and the migration note), codegen-concepts, the 0.x-to-1.0 guide, the CLI and codegen-ts READMEs, the codegen / audit / prompts skills and the always-on agent context (with regenerated conformance goldens), and the CHANGELOG. --- .claude/rules/codegen-architecture.md | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 50 +++ README.md | 5 +- .../skills/metaobjects-audit/SKILL.md | 17 +- .../references/typescript.md | 57 +-- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../references/typescript.md | 20 +- .../references/typescript.md | 25 +- agent-context/templates/always-on.md.mustache | 6 +- docs/features/codegen-concepts.md | 16 +- docs/features/codegen-data-shapes.md | 5 +- docs/features/migrations/0.x-to-1.0.md | 7 + docs/features/own-your-codegen.md | 108 +++++- docs/recipes/write-your-own-generator.md | 6 +- .../advanced-modeling/metaobjects.config.ts | 10 +- examples/showcase/metaobjects.config.ts | 13 +- .../.claude/skills/metaobjects-audit/SKILL.md | 17 +- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../expected/.metaobjects/AGENTS.md | 6 +- .../expected/.metaobjects/CLAUDE.md | 6 +- .../.claude/skills/metaobjects-audit/SKILL.md | 17 +- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../expected/.metaobjects/AGENTS.md | 6 +- .../expected/.metaobjects/CLAUDE.md | 6 +- .../.claude/skills/metaobjects-audit/SKILL.md | 17 +- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../python/expected/.metaobjects/AGENTS.md | 6 +- .../python/expected/.metaobjects/CLAUDE.md | 6 +- .../.claude/skills/metaobjects-audit/SKILL.md | 17 +- .../references/typescript.md | 57 +-- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../references/typescript.md | 20 +- .../references/typescript.md | 25 +- .../expected/.metaobjects/AGENTS.md | 6 +- .../expected/.metaobjects/CLAUDE.md | 6 +- .../.claude/skills/metaobjects-audit/SKILL.md | 17 +- .../references/typescript.md | 57 +-- .../skills/metaobjects-codegen/SKILL.md | 20 +- .../references/owning-generators.md | 22 +- .../references/typescript.md | 20 +- .../references/typescript.md | 25 +- .../expected/.metaobjects/AGENTS.md | 6 +- .../expected/.metaobjects/CLAUDE.md | 6 +- server/typescript/packages/cli/README.md | 36 +- .../packages/cli/src/commands/eject.ts | 12 +- .../packages/cli/src/commands/init.ts | 30 +- .../packages/cli/src/lib/catalog-listing.ts | 31 +- .../packages/cli/src/lib/catalog.ts | 57 +++ .../cli/src/lib/load-metaobjects-config.ts | 101 +---- .../typescript/packages/cli/src/lib/output.ts | 2 +- .../cli/test/catalog-factory-exports.test.ts | 147 +++++++ .../packages/cli/test/eject-multi.test.ts | 8 +- .../packages/cli/test/gen-list.test.ts | 7 +- .../stock-generators-importable.test.ts | 183 +++++++++ .../cli/test/load-metaobjects-config.test.ts | 32 ++ .../cli/test/unit/init-refresh-docs.test.ts | 6 +- .../test/unit/init-scaffold-config.test.ts | 8 +- .../unit/removed-generator-diagnostic.test.ts | 63 --- .../unit/removed-generator-imports.test.ts | 65 ---- .../src/generator-registry.ts | 1 + .../src/generator-registry.ts | 3 + .../typescript/packages/codegen-ts/README.md | 24 +- .../codegen-ts/src/generator-registry.ts | 49 ++- .../codegen-ts/src/generators/index.ts | 33 +- .../src/generators/trace-helper-file.ts | 364 +----------------- .../packages/codegen-ts/src/index.ts | 4 + .../codegen-ts/src/reference-templates.ts | 4 + .../codegen-ts/src/reference/callable.ts | 69 ++++ .../codegen-ts/src/reference/trace-helper.ts | 68 ++++ .../codegen-ts/src/templates/trace-helper.ts | 358 +++++++++++++++++ .../test/reference-byte-identical.test.ts | 135 +++++++ .../test/reference-templates.test.ts | 5 +- .../packages/test-generators/README.md | 11 +- .../packages/test-generators/src/index.ts | 11 +- .../ADR-0034-codegen-scaffold-and-own.md | 70 ++++ spec/roadmap.md | 3 +- 82 files changed, 1988 insertions(+), 962 deletions(-) create mode 100644 server/typescript/packages/cli/test/catalog-factory-exports.test.ts create mode 100644 server/typescript/packages/cli/test/integration/stock-generators-importable.test.ts delete mode 100644 server/typescript/packages/cli/test/unit/removed-generator-diagnostic.test.ts delete mode 100644 server/typescript/packages/cli/test/unit/removed-generator-imports.test.ts create mode 100644 server/typescript/packages/codegen-ts/src/reference/callable.ts create mode 100644 server/typescript/packages/codegen-ts/src/reference/trace-helper.ts create mode 100644 server/typescript/packages/codegen-ts/src/templates/trace-helper.ts diff --git a/.claude/rules/codegen-architecture.md b/.claude/rules/codegen-architecture.md index 61916e190..0d0ec99f4 100644 --- a/.claude/rules/codegen-architecture.md +++ b/.claude/rules/codegen-architecture.md @@ -33,7 +33,7 @@ generator new [--scope entity|package|model]` writes a working one into are exported from the package root (`objectRefTarget`, `enumValues`, `servedPath`, the case helpers); the guide and every port's shape are in `docs/recipes/write-your-own-generator.md`. -**Built-in factories**: `entityFile`, `queriesFile`, `routesFile`, `formFile`, `barrel`. Per ADR-0034 (scaffold-and-own) and its Amendment 2 (opt-in codegen), `meta init` scaffolds `codegen/generators/` EMPTY with `generators: []`; `meta gen --list --probe` is the catalog, and `meta eject ...` copies each chosen reference template into the consumer repo at `codegen/generators/*.ts` and prints the import and entry to wire. The owned copy is the ONLY import path for `entityFile`/`queriesFile`/`routesFile`/`barrel`: the deprecated `@metaobjectsdev/codegen-ts/generators` re-export of them was **removed at the 1.0 cut**. +**Built-in factories**: `entityFile`, `queriesFile`, `routesFile`, `formFile`, `barrel`. Per ADR-0034 (scaffold-and-own) and its Amendment 2 (opt-in codegen), `meta init` scaffolds `codegen/generators/` EMPTY with `generators: []`; `meta gen --list --probe` is the catalog, and `meta eject ...` copies each chosen reference template into the consumer repo at `codegen/generators/*.ts` and prints the import and entry to wire. `entityFile`/`queriesFile`/`routesFile`/`barrel` are also importable from `@metaobjectsdev/codegen-ts/generators` to run them as shipped (that export was removed at the 1.0 cut and **restored in 1.1**, ADR-0034 Amendment 5); the owned copy is how you change what one emits. **User wiring** (`metaobjects.config.ts`): diff --git a/AGENTS.md b/AGENTS.md index 21c1d119d..03fd7527d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ MetaObjects is a **cross-language metadata standard** for declaring typed entity The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, generated code is idiomatic per-language output with **no proprietary runtime**: the generated entity and model code imports nothing from MetaObjects at runtime, and the REST, prompt, client and runtime tiers import ordinary Apache-2.0 packages you can vendor, fork or replace. If `@metaobjectsdev/*` disappears tomorrow, you keep working code. -**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `callable`, `trace-helper`, `template`, the docs tier and `shared-model`; ADR-0034 Amendment 3 correction, 2026-09-26). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee. +**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `template`, the docs tier and `shared-model`; ADR-0034 Amendment 3 correction, 2026-09-26, narrowed by Amendment 5). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee. ## Six pillars diff --git a/CHANGELOG.md b/CHANGELOG.md index 787d9022b..4bf6b4313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,56 @@ here.** ## [Unreleased] +### Added + +- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again + (TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from + `@metaobjectsdev/codegen-ts/generators` once more, beside `namesFile`, `routesFileHono` + and the prompt tier. 1.0 had removed those four so that an owned copy was the only way to + run them. An assessment of an adopter found its owned copies were almost pure duplication + of the reference and had already fallen behind it, so the rule is now: **import a stock + generator to run it as shipped, and `meta eject` it when you need to change what it + emits.** Both paths are supported and tested, and the factory keeps its name in an + ejected copy, so moving between them is one import line. Ruling and reasoning: + ADR-0034 Amendment 5. + - **Nothing changes for a project that owns its copies.** They run as before, and + `meta eject --list` and `meta verify --codegen` behave as before. + - **To drop a copy you never changed**, replace its local import with the package import, + delete the file and run `meta gen`. `queries` and `barrel` regenerate nothing if the + copy matched the reference. `entity` and `routes` also move their HTTP-adapter import + from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which then has to be + installed again. Steps and the diff to expect: + [own-your-codegen](docs/features/own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). + - **An imported generator is still a reference helper.** Its output is not covered by the + compatibility policy and can change in a MINOR; `meta verify --codegen` reports that + until you regenerate. Own the copy if output has to hold still across upgrades. +- **`meta gen --list` prints how to import each generator.** Every row outside the `docs` + layer shows the import line that runs the generator as shipped, and `--format json` + carries it as `wire.import` and `wire.entry`. A gate imports each package's factory module + and requires the name the catalog prints, and requires an ejectable generator's reference + template to export the same symbol. +- **`callable` and `trace-helper` can be ejected (TypeScript).** Both gained a reference + template, so `meta eject callable` and `meta eject trace-helper` copy a generator you + own: which objects get a module and where it lands. The module body stays in the package + (`renderCallableFile`, `renderTraceHelper`, now exported from `@metaobjectsdev/codegen-ts`), + the same split the prompt tier uses. Still package-only: the `template` primitive, the + docs tier (`docs`, `api-docs`, `mermaid-er`) and `shared-model`. + +### Changed + +- **`meta init`'s scaffolded `metaobjects.config.ts` shows the import first.** The selection + is still empty (`generators: []`), and an existing project is not touched. The comment + now shows the package import as the way to wire a generator and `meta eject` as the way to + change one. `meta init`'s next steps and `meta gen`'s "no generators are wired" help say + the same. +- **The config loader no longer rejects an import of the four generators.** 1.0.x answered + `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"` with a message naming + `meta eject`. That import now loads. +- **The agent-context skills teach "import by default, eject to customize".** The codegen + skill and the always-on context no longer send an agent to eject a generator it is not + going to change. The audit skill reports an unmodified owned copy as a finding, and reads + an import of the four as a blocker only on a 1.0.x install. + ### Fixed - `meta verify --docs` now reports a generated page in the docs root that no current object diff --git a/README.md b/README.md index d078fd2d9..d6aa08839 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ MetaObjects has two layers, and only the first is a promise | | Core — guaranteed | Helpers — yours | |---|---|---| | **What** | The metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager`, not the HTTP adapters that mount it); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser | Every generator that writes code into your repo — the ones you write for the outputs you need, and the reference routes, controllers, ORM wiring, DTOs, forms, grids and hooks you copy | -| **Promise** | Conformance-gated, the same behaviour in every port that ships it, covered by the [compatibility policy](docs/compatibility-policy.md) | Reference starting points that compile and pass their reference fixtures. Copy one with `meta eject` and change it freely | +| **Promise** | Conformance-gated, the same behaviour in every port that ships it, covered by the [compatibility policy](docs/compatibility-policy.md) | Reference starting points that compile and pass their reference fixtures. Run one as shipped, or copy it with `meta eject` and change it freely | | **A defect is** | A MetaObjects bug, fixed in a release | A bug in the reference, fixed there; your copy is yours | The test is mechanical: what the tool guarantees is core; what it writes into your repo @@ -212,7 +212,8 @@ sixth ships two libraries at their own stability labels: 20-line shape plus JSON Schema and OpenAPI examples to copy. The reference generators (Drizzle/Zod + Fastify for TS, Spring REST + DTO + repository for Java, `data class` + Exposed for Kotlin, EF Core record + ASP.NET routes for C#, Pydantic + FastAPI for - Python) are starting points: eject the one that is close, change it, and regenerate. + Python) are starting points: run one as shipped when it already emits what you want, or + eject the one that is close, change it, and regenerate. The engine that runs generators is core; their output is yours. 2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs; typed tool payloads are diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/agent-context/skills/metaobjects-audit/references/typescript.md b/agent-context/skills/metaobjects-audit/references/typescript.md index 337872160..feab2c601 100644 --- a/agent-context/skills/metaobjects-audit/references/typescript.md +++ b/agent-context/skills/metaobjects-audit/references/typescript.md @@ -71,43 +71,52 @@ here — the table is its second pass, not its definition. `meta eject ...` copies the reference generator templates a project chooses into `codegen/generators/*.ts`. Since 1.0.4 `meta init` scaffolds that directory EMPTY with `generators: []` (a project initialised on an earlier CLI got four wired copies, so both -shapes are normal). `metaobjects.config.ts` imports the **owned** local copies: +shapes are normal). A generator reaches `metaobjects.config.ts` one of two ways, and both +are supported from 1.1: ```ts -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; +// run as shipped — nothing copied, fixes arrive by upgrading +import { entityFile, queriesFile } from "@metaobjectsdev/codegen-ts/generators"; +// owned — a local copy the project can change +import { routesFile } from "./codegen/generators/routes"; ``` -**Removed import path.** `entityFile` / `queriesFile` / `routesFile` / `barrel` were -exported from `@metaobjectsdev/codegen-ts/generators` and deprecated; **1.0 removed them** -(ADR-0035 A3). On 1.0+ this no longer resolves: +**Audit the owned copies first.** A copy exists to carry a change. One that carries none +is duplication, and it falls behind the reference: -```ts -import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; +```bash +meta eject --list ``` -**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported, -non-deprecated home of the generators it still exports — the prompt/output tier -(`promptRender`, `outputParser`, `outputPrompt`, `extractor`, `renderHelper`), -`traceHelperFile`, `routesFileHono`, `namesFile` and `callableFile`. The prompt tier can -also be ejected (`meta eject prompt-render`), but owning it is optional; `traceHelperFile` -and `callableFile` are package-only and this subpath is their only home. The CLI's own -prompt-gate warning tells adopters to import `promptRender` from exactly this path. A -project importing those from it is CORRECT, and reporting it as un-adopted scaffold-and-own -is a false finding. - -**Finding this in a project:** grep for the four removed NAMES on that path, never the path -alone: +marks each owned copy `identical to the reference`, `same content as the reference, your +formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is +identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing +its local import with the package import and deleting the file, then `meta gen` and a +review of the diff. A copy with lines of its own is an intentional customization. Check +only how far behind it is. + +**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported +home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, +`routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, +`outputParser`, `outputPrompt`, `extractor`, `renderHelper`), `traceHelperFile`, +`callableFile` and the rest. The CLI's own prompt-gate warning tells adopters to import +`promptRender` from exactly this path. A project importing from it is CORRECT, and +reporting it as un-adopted scaffold-and-own is a false finding. + +**The one version where an import is a blocker.** `entityFile` / `queriesFile` / +`routesFile` / `barrel` were removed from that subpath at 1.0 (ADR-0035 A3) and restored in +1.1 (ADR-0034 Amendment 5). On a 1.0.x install a config importing them fails to load with +a message naming `meta eject`. Check the installed version before reading a hit as a +finding, and match the four NAMES, never the path alone: ```bash +npm list @metaobjectsdev/codegen-ts grep -rn '@metaobjectsdev/codegen-ts/generators' . \ | grep -E 'entityFile|queriesFile|routesFile|barrel' ``` -A hit is a 1.0 upgrade blocker (the import fails to resolve) — recommend `meta init` (or -`meta eject `) to scaffold the owned copies, then import those locally. The owned -generator files themselves (`codegen/generators/*.ts`) are the second thing to audit: are -they stale relative to the reference templates, or intentionally customized? +On 1.0.x a hit is an upgrade blocker: recommend upgrading to 1.1, or `meta eject ` +and importing the copies locally. On 1.1 and later a hit is correct. --- diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom diff --git a/agent-context/skills/metaobjects-codegen/references/typescript.md b/agent-context/skills/metaobjects-codegen/references/typescript.md index 0462bfdd7..62e7aa00c 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript.md @@ -119,16 +119,16 @@ PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself. ## The generators Server-side, framework-neutral. **None is wired by default** — `meta init` writes -`generators: []` and an empty `codegen/generators/`. `meta eject ...` copies the -ownable ones into your repo, imported from `./codegen/generators/*` (ADR-0034); 1.0 -REMOVED their `@metaobjectsdev/codegen-ts/generators` export, so an owned copy is the only -path for those. The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. The `/generators` subpath itself is NOT deprecated. It -exports the prompt tier (`promptRender`, `outputParser`, `outputPrompt`, `extractor`, -`renderHelper`), `namesFile` and `requirementTests`, which you may import from there OR -eject to own, and it is the only home of the package-only generators — `traceHelperFile`, -`callableFile` — which ship no reference template (`meta gen --list` marks them -`package-only`). +`generators: []` and an empty `codegen/generators/`. Every generator below is imported from +`@metaobjectsdev/codegen-ts/generators` to run it as shipped; `meta gen --list` prints +each import line. To change what one emits, `meta eject ...` copies it into your +repo, imported from `./codegen/generators/*` instead (ADR-0034); the factory keeps its +name. Import by default and eject to customize: an owned copy you never change only falls +behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed +from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the +only path for those four.) The engine primitives come from the package main entry, +`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in +`meta gen --list`: the `template` primitive and `shared-model`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/agent-context/skills/metaobjects-prompts/references/typescript.md b/agent-context/skills/metaobjects-prompts/references/typescript.md index 786b20764..189c9a88f 100644 --- a/agent-context/skills/metaobjects-prompts/references/typescript.md +++ b/agent-context/skills/metaobjects-prompts/references/typescript.md @@ -22,19 +22,18 @@ provider/LLM-call layer — you compose the call yourself. ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// `meta init` scaffolds ./codegen/generators/ EMPTY (ADR-0034 Amendment 2). The entity -// generators arrive by `meta eject entity queries barrel`, which copies each into that -// directory as a file you own and prints the import to add — these three lines. They -// have no package import: the old `@metaobjectsdev/codegen-ts/generators` re-export of -// them was removed at 1.0. The parser's reply type is the @responseRef value object's -// own interface, which `entity` emits, so `entity` has to be in the run. -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -// promptRender / outputParser can be imported from the package (below) OR owned: -// `meta eject prompt-render output-parser` copies them into ./codegen/generators/ the -// same way. Either way the render and parse ENGINES they call stay in the package. -import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators"; +// `meta init` wires nothing (ADR-0034 Amendment 2). Each generator is imported from the +// package to run it as shipped. The parser's reply type is the @responseRef value +// object's own interface, which `entity` emits, so `entity` has to be in the run. +// (On a 1.0.x install the first three have no package export: `meta eject entity queries +// barrel` and import them from ./codegen/generators/ instead.) +// +// Any of these can be OWNED instead: `meta eject prompt-render output-parser` copies them +// into ./codegen/generators/ and prints the local import to swap in. Either way the +// render and parse ENGINES they call stay in the package. +import { + entityFile, queriesFile, barrel, promptRender, outputParser, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index e0bcbffbf..69a23e79f 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -52,11 +52,11 @@ itself for those three: it is the source those pages are generated from. - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/docs/features/codegen-concepts.md b/docs/features/codegen-concepts.md index e40c6d544..83a433696 100644 --- a/docs/features/codegen-concepts.md +++ b/docs/features/codegen-concepts.md @@ -36,8 +36,9 @@ gates it with nothing to register. `meta generator new ` scaffolds a worki TypeScript; every port's shape and two worked examples are in [Write your own generator](../recipes/write-your-own-generator.md). -The library also ships a documented **template library** of reference generators; when -one is close to what you need, you copy it into your repo and own it. A single standard template set has never fit +The library also ships a documented **template library** of reference generators. When one +already emits what you want, import it and run it as shipped; when one is close to what you +need, you copy it into your repo and own it. A single standard template set has never fit across projects — framework conventions, naming, structure, and business patterns are per-project. Don't fight a black-box generator; own a starting point and edit it. Choosing and adapting a starting template is a **human/Claude judgment call**, not a @@ -48,9 +49,14 @@ scaffolds `codegen/generators/` empty with `generators: []`. `meta gen --list -- the catalog — every generator, with how many files it would emit for your model — and `meta eject ...` copies the ones you choose from the reference templates into `codegen/generators/`, printing the import and entry to wire, so `meta gen` runs from -generators you own from the first run. `eject` never clobbers a file you already own. The -old `@metaobjectsdev/codegen-ts/generators` import of `entityFile` / `queriesFile` / -`routesFile` / `barrel` was **removed at 1.0** — own the local copy. +generators you own. `eject` never clobbers a file you already own. + +Owning is how you **change** a generator, not a precondition for running one. Every stock +generator also runs as shipped: import its factory from its package (`meta gen --list` +prints the line) and wire it, and an upgrade is how its fixes reach you. The +`@metaobjectsdev/codegen-ts/generators` import of `entityFile` / `queriesFile` / +`routesFile` / `barrel` was removed at 1.0 and **restored in 1.1** (ADR-0034 Amendment 5). +See [Import a stock generator, or own a copy](own-your-codegen.md#import-a-stock-generator-or-own-a-copy-typescript). ## 3. Authoring mechanisms — and their tradeoffs diff --git a/docs/features/codegen-data-shapes.md b/docs/features/codegen-data-shapes.md index 7479e4158..185ccea8b 100644 --- a/docs/features/codegen-data-shapes.md +++ b/docs/features/codegen-data-shapes.md @@ -168,8 +168,9 @@ emits an out-of-spec shape fails the cross-port conformance suite. ## Where to find the types Everything on this page comes from the package **main entry**. The -`@metaobjectsdev/codegen-ts/generators` subpath re-exports some of the same names, -but it is deprecated (ADR-0034) and removed at the 1.0 cut — do not import from it. +`@metaobjectsdev/codegen-ts/generators` subpath re-exports some of the same names, but its +job is the generator factories (`entityFile`, `promptRender`, …); import these shapes from +the main entry. The neutral structural shapes, which is what a declarative `scope` generator renders against: diff --git a/docs/features/migrations/0.x-to-1.0.md b/docs/features/migrations/0.x-to-1.0.md index 5683b125e..fffdc2cc5 100644 --- a/docs/features/migrations/0.x-to-1.0.md +++ b/docs/features/migrations/0.x-to-1.0.md @@ -299,6 +299,13 @@ points at the parameter that works: `ObjectManager(..., column_naming=...)`. Importing the built-in generators from `@metaobjectsdev/codegen-ts/generators` (`entityFile` / `queriesFile` / `routesFile` / `barrel`) is **removed** at 1.0. +> **1.1 restored these four imports** (ADR-0034 Amendment 5). If you are upgrading +> straight to 1.1.0 or later, a config that imports them keeps loading and this section +> needs no action. If you took owned copies on 1.0.x, they keep working; to drop the ones +> you never changed, see +> [Replacing an unmodified owned copy with the import](../own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). +> The rest of this section describes 1.0.x. + - **Migration:** import the owned copies `meta init` scaffolds into `codegen/generators/*` and wire those in `metaobjects.config.ts`. On an existing project, `meta eject entity` / `queries` / `routes` / `barrel` copies each one in place diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index fc145a5b3..c46940fff 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -22,14 +22,20 @@ JSON Schema and OpenAPI 3.1 examples to copy: [Write your own generator](../recipes/write-your-own-generator.md). Eject a reference (below) only when one already emits something close to what you want. +**A stock generator already emits what you want? Import it.** Every stock generator runs +as shipped from its package, with nothing copied into your repo, and an upgrade is how its +fixes reach you. Eject it when you need to change what it emits. Both are supported in +every port ([ADR-0034 Amendment 5](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md#amendment-5-2026-10-11--the-stock-generators-are-importable-again-eject-is-how-you-customize)); +the TypeScript side is in [Import a stock generator, or own a copy](#import-a-stock-generator-or-own-a-copy-typescript). + "Own your codegen" means three related things: 1. **You own the invocation** — codegen runs through your own build, on your terms, in every port. 2. **You write the generators you need** — see above. -3. **You own the templates** — `meta eject ...` copies the reference generators - *into your repo* so you can edit them (ADR-0034 scaffold-and-own). **Every port - can eject** — see [Python](#python-metaobjects-eject), +3. **You can own the templates** — `meta eject ...` copies the reference generators + *into your repo* so you can edit them (ADR-0034 scaffold-and-own). It is how you + customize one, not a precondition for running it. **Every port can eject** — see [Python](#python-metaobjects-eject), [Java and Kotlin](#java-and-kotlin-mvn-metaobjectseject) and [C#](#c-dotnet-meta-eject) below. Before eject existed outside TypeScript, customization there was limited to the declarative template-codegen surface (`--template-spec` / @@ -37,6 +43,87 @@ JSON Schema and OpenAPI 3.1 examples to copy: intentional; Amendment 3 supersedes that, because subclassing and selection do not let an adopter own a generator's emit logic.) +## Import a stock generator, or own a copy (TypeScript) + +Every stock generator can be used two ways: + +| | Import it | Own a copy | +|---|---|---| +| How | `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";` | `meta eject entity`, then `import { entityFile } from "./codegen/generators/entity.js";` | +| In your repo | the import, nothing else | `codegen/generators/entity.ts`, and for `entity`, `routes` and `routes-hono` the HTTP adapter source under `codegen/runtime/` | +| An upstream fix | arrives when you upgrade the package | arrives when you merge it into your copy | +| Changing what it emits | only through its options (`filter`, `target`, `expose`, …) | edit the file | +| After a package upgrade | output can change; `meta verify --codegen` reports it until you run `meta gen` | output changes only when your copy does | + +Start with the import. `meta gen --list` prints the import line under each generator +(`wire.import` and `wire.entry` in `--format json`), beside what to install and which +config keys it reads: + +```ts +import { defineConfig } from "@metaobjectsdev/cli"; +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; + +export default defineConfig({ + outDir: "src/generated", + dialect: "postgres", + dbImport: "../db", + generators: [entityFile(), queriesFile(), routesFile(), barrel()], +}); +``` + +The engine's factories come from `@metaobjectsdev/codegen-ts/generators`. `formFile` comes +from `@metaobjectsdev/codegen-ts-react`, and `tanstackQuery`, `tanstackGrid` and +`tanstackGridHook` from `@metaobjectsdev/codegen-ts-tanstack`. + +Eject when you need a generator to emit something its options cannot express. The factory +keeps its name in the copy, so the change to your config is one import line, and +`meta eject` prints it. Replace the package import; a second import beside it leaves +`generators` bound to the packaged generator and your edits do nothing. + +An imported generator is still a reference helper, not a guarantee. Its output is not +covered by the [compatibility policy](../compatibility-policy.md) and can change in a +MINOR, which is what receiving fixes on upgrade means. If you need generated output to +hold still across upgrades, own the copy. + +### Replacing an unmodified owned copy with the import + +Between 1.0 and 1.1 an owned copy was the only way to run `entity`, `queries`, `routes` +and `barrel`, so many projects carry copies they never changed. A copy like that only +falls behind the reference. To drop one: + +1. Run `meta eject --list`. A copy marked `identical to the reference` or + `same content as the reference, your formatting` is unmodified. So is one marked + `DIFFERS` with `0 line(s) of your own`: it is only behind. A copy with lines of your + own is a customization; keep it, or check what the lines are with + `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts`. +2. In `metaobjects.config.ts`, replace the local import with the package one. The factory + name does not change: + + ```diff + -import { queriesFile } from "./codegen/generators/queries.js"; + +import { queriesFile } from "@metaobjectsdev/codegen-ts/generators"; + ``` + +3. Delete `codegen/generators/.ts`. +4. Run `meta gen`, review the diff, then `meta verify --codegen`. + +What the diff in step 4 holds: + +- **A copy that matched the reference** regenerates nothing for `queries` and `barrel`. +- **A copy that was behind** regenerates the changes it was missing. That is the reference + catching up, so read it the way you would read an upgrade. +- **`entity` and `routes` also move one import.** An ejected `entity` or `routes` points + its output at the adapter copy in `codegen/runtime/`, and the packaged generator imports + `@metaobjectsdev/runtime-ts`. So those import lines change, and the project needs + `@metaobjectsdev/runtime-ts` installed again (`meta gen --list --format json` gives the + range under `install.runtime`). A copy wired with + `runtimeImport: "@metaobjectsdev/runtime-ts"` already imported the package and moves + nothing. Once no owned generator points at `codegen/runtime/` and nothing of your own + imports it, delete that directory too. + +Nothing requires this. An owned copy keeps working, and `meta eject --list` keeps telling +you how far it is from the reference. + ## What `meta eject` hands over, and what stays core (TypeScript) Owning a generator means owning the code it writes **and the code that code calls**, @@ -291,7 +378,7 @@ generator code. The 20-line programmatic shape for each port is in | Port | Invocation | Programmatic — write a `Generator` | Declarative — template + scope | |---|---|---|---| -| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To start from a reference instead: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to add to `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. `requirement-tests` ejects as one file holding the generator and its default stub renderer, so the stub text is yours to change; the requirement walk, each test's identity and the claim digest stay in the package. `cube-model` (the Cube data-model exporter, [cube-export.md](cube-export.md)) ejects as one file as well: which entities get a cube, where the files land and the YAML call are yours, while the mapping (`buildCubeModel`, which raises every refusal) and the YAML writer (`renderCubeYaml`) stay in the package. Not every registered generator is ejectable: `callable`, `trace-helper`, the `template` primitive, the docs tier (`docs`, `api-docs`, `mermaid-er`, whose door is `meta docs`) and `shared-model` ship no reference template, so they are **package-only** — `meta gen --list` marks them so and `meta eject` names them as such. `shared-model` (FR-023's publisher generator) stays package-only deliberately, since it emits a hash-pinned cross-port contract artifact. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | +| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To use a stock generator as shipped, import its factory from its package (`meta gen --list` prints the line) and wire it. To change one: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to put in `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. `requirement-tests` ejects as one file holding the generator and its default stub renderer, so the stub text is yours to change; the requirement walk, each test's identity and the claim digest stay in the package. `cube-model` (the Cube data-model exporter, [cube-export.md](cube-export.md)) ejects as one file as well: which entities get a cube, where the files land and the YAML call are yours, while the mapping (`buildCubeModel`, which raises every refusal) and the YAML writer (`renderCubeYaml`) stay in the package. `callable` and `trace-helper` eject the same way as the prompt tier: the copy decides which objects get a module and where it lands, and the module body comes from `renderCallableFile` / `renderTraceHelper` in the package. Three kinds of registered generator are not ejectable and stay **package-only** — `meta gen --list` marks them so and `meta eject` names them as such: the `template` primitive, which has no emit logic of its own to own (the same ruling as the C#, Java and Python ports); the docs tier (`docs`, `api-docs`, `mermaid-er`), which is the engine behind `meta docs` and is run by that command, not by `generators: [...]`; and `shared-model` (FR-023's publisher generator), which emits a hash-pinned cross-port contract artifact. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | | **Java / Kotlin** | `mvn metaobjects:generate` / `mvn metaobjects:verify` (`metaobjects-maven-plugin`) | **Yes.** Extend `FileEmittingGenerator` and read the model through `ModelWalk` (both in `metaobjects-codegen-base`) for one of your own. Every generator — built-in or your own — is named in `` and loaded from the project classpath: one seam, not two. There is no default suite, so `` is the complete list. Kotlin runs through the same goal. | **Yes** — `TemplateScopeGenerator` wired as an ordinary ``. No CLI flag: `` is already the seam. | | **C#** | `dotnet meta gen` / `dotnet meta verify` (.NET tool) | **Yes.** Implement `IGenerator` in the owned console project `codegen/` and list it in `codegen/Program.cs`; `dotnet meta gen` / `verify --codegen` hand off to that project whenever `codegen/Codegen.csproj` exists. `dotnet meta eject ` scaffolds the project, or write its two files by hand. An owned generator the `--generators` selection does not name still runs. | **Yes** — `dotnet meta gen --template-spec --template-root `. | | **Python** | `metaobjects gen` / `metaobjects verify` (console-script) | **Yes.** Name your generator as `module:symbol` in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one. Read the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol` registers **metamodel vocabulary**, not a generator.) | **Yes** — `metaobjects gen --template-spec --templates `. | @@ -413,12 +500,15 @@ This split follows each ecosystem's norms — interpreted ports (TS / Python) na import the provider module; compiled ports (JVM) discover it on the build classpath — the same "idiomatic per port" principle as generator ownership ([Per port](#per-port)). -## Deprecated (removed at 1.0) +## Removed at 1.0, restored in 1.1 -Importing the built-in generators from `@metaobjectsdev/codegen-ts/generators` -(`entityFile`, `queriesFile`, `routesFile`, `barrel`) is **deprecated** (ADR-0034) and -**removed at the 1.0/8.0 release**. Use the owned copies `meta eject` writes into -`codegen/generators/*` and import those from your `metaobjects.config.ts`. +Importing `entityFile`, `queriesFile`, `routesFile` and `barrel` from +`@metaobjectsdev/codegen-ts/generators` was deprecated by ADR-0034 and **removed at the +1.0/8.0 release**, which made an owned copy the only way to run them. **1.1 restored the +import** (ADR-0034 Amendment 5). On 1.0.x a config importing them fails to load with a +message naming `meta eject`; from 1.1.0 it loads. A project that took owned copies in +between needs no change, and can drop the ones it never modified: +[Replacing an unmodified owned copy with the import](#replacing-an-unmodified-owned-copy-with-the-import). ## Python: `metaobjects eject` diff --git a/docs/recipes/write-your-own-generator.md b/docs/recipes/write-your-own-generator.md index 99a151c82..cf196227e 100644 --- a/docs/recipes/write-your-own-generator.md +++ b/docs/recipes/write-your-own-generator.md @@ -30,8 +30,10 @@ emits it**, or none emits it the way your application needs it: Hand-writing any of these is the anti-pattern: it drifts the day the model changes, and nothing tells you. A generator regenerates on every model change and is drift-gated. -Reach for **`eject`** instead only when a reference generator already emits something -close to what you want (`meta gen --list` is the catalog); then edit your copy. Hand-write +When a reference generator already emits what you want, run it as shipped: import it from +its package, or select it by name on the non-TypeScript ports (`meta gen --list` is the +catalog). Reach for **`eject`** when one is close and you need to change it; then edit your +copy. Hand-write only what the model genuinely cannot express: business logic, calls to other systems. ## The shape, in every port diff --git a/examples/advanced-modeling/metaobjects.config.ts b/examples/advanced-modeling/metaobjects.config.ts index c48521471..24721e78e 100644 --- a/examples/advanced-modeling/metaobjects.config.ts +++ b/examples/advanced-modeling/metaobjects.config.ts @@ -1,9 +1,9 @@ import { defineConfig } from "@metaobjectsdev/cli"; -// ADR-0034 scaffold-and-own: entityFile / queriesFile / routesFile / barrel are OWNED -// local copies under codegen/generators/, exactly what `meta init` scaffolds into a real -// consumer project (and what `meta eject ` copies on demand). They are the only -// import path for these four as of 1.0 — the deprecated -// `@metaobjectsdev/codegen-ts/generators` export of them was removed at the cut. +// ADR-0034 scaffold-and-own: this example OWNS its four core generators, as local copies +// under codegen/generators/ — what `meta eject entity queries routes barrel` writes. That +// is the customization path. Importing the same four from +// `@metaobjectsdev/codegen-ts/generators` runs them as shipped and works too (removed at +// 1.0, restored in 1.1 — ADR-0034 Amendment 5). import { entityFile } from "./codegen/generators/entity"; import { queriesFile } from "./codegen/generators/queries"; import { routesFile } from "./codegen/generators/routes"; diff --git a/examples/showcase/metaobjects.config.ts b/examples/showcase/metaobjects.config.ts index 71f2a554b..f3cc3ba72 100644 --- a/examples/showcase/metaobjects.config.ts +++ b/examples/showcase/metaobjects.config.ts @@ -1,14 +1,15 @@ import { defineConfig } from "@metaobjectsdev/cli"; -// ADR-0034 scaffold-and-own: the four ownable generators are OWNED local copies under -// codegen/generators/, exactly what `meta init` scaffolds into a real consumer project. -// They are the only import path for these four as of 1.0 — the deprecated -// `@metaobjectsdev/codegen-ts/generators` export of them was removed at the cut. +// ADR-0034 scaffold-and-own: this example OWNS its four core generators, as local copies +// under codegen/generators/ — what `meta eject entity queries routes barrel` writes. That +// is the customization path. Importing the same four from +// `@metaobjectsdev/codegen-ts/generators` runs them as shipped and works too (removed at +// 1.0, restored in 1.1 — ADR-0034 Amendment 5). import { entityFile } from "./codegen/generators/entity"; import { queriesFile } from "./codegen/generators/queries"; import { routesFile } from "./codegen/generators/routes"; import { barrel } from "./codegen/generators/barrel"; -// The prompt/render tier has no reference template to eject yet, so it is imported from -// the package. To own one, replace it in `generators` with your own Generator. It +// The prompt/render tier is imported from the package and run as shipped. To own one, +// `meta eject prompt-render` (or any of the others) and import the local copy. It // references the value-object types entityFile() emits (ADR-0056), so it needs // entityFile() in the same run. import { diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom 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 74b607241..6df7df873 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 @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. 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 index 74b607241..6df7df873 100644 --- 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 @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom 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 a824e187d..e1e2a89c6 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md index a824e187d..e1e2a89c6 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index 61e40088b..017c7d41b 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -31,11 +31,11 @@ itself for those three: it is the source those pages are generated from. - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md index 61e40088b..017c7d41b 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md @@ -31,11 +31,11 @@ itself for those three: it is the source those pages are generated from. - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md index 337872160..feab2c601 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -71,43 +71,52 @@ here — the table is its second pass, not its definition. `meta eject ...` copies the reference generator templates a project chooses into `codegen/generators/*.ts`. Since 1.0.4 `meta init` scaffolds that directory EMPTY with `generators: []` (a project initialised on an earlier CLI got four wired copies, so both -shapes are normal). `metaobjects.config.ts` imports the **owned** local copies: +shapes are normal). A generator reaches `metaobjects.config.ts` one of two ways, and both +are supported from 1.1: ```ts -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; +// run as shipped — nothing copied, fixes arrive by upgrading +import { entityFile, queriesFile } from "@metaobjectsdev/codegen-ts/generators"; +// owned — a local copy the project can change +import { routesFile } from "./codegen/generators/routes"; ``` -**Removed import path.** `entityFile` / `queriesFile` / `routesFile` / `barrel` were -exported from `@metaobjectsdev/codegen-ts/generators` and deprecated; **1.0 removed them** -(ADR-0035 A3). On 1.0+ this no longer resolves: +**Audit the owned copies first.** A copy exists to carry a change. One that carries none +is duplication, and it falls behind the reference: -```ts -import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; +```bash +meta eject --list ``` -**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported, -non-deprecated home of the generators it still exports — the prompt/output tier -(`promptRender`, `outputParser`, `outputPrompt`, `extractor`, `renderHelper`), -`traceHelperFile`, `routesFileHono`, `namesFile` and `callableFile`. The prompt tier can -also be ejected (`meta eject prompt-render`), but owning it is optional; `traceHelperFile` -and `callableFile` are package-only and this subpath is their only home. The CLI's own -prompt-gate warning tells adopters to import `promptRender` from exactly this path. A -project importing those from it is CORRECT, and reporting it as un-adopted scaffold-and-own -is a false finding. - -**Finding this in a project:** grep for the four removed NAMES on that path, never the path -alone: +marks each owned copy `identical to the reference`, `same content as the reference, your +formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is +identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing +its local import with the package import and deleting the file, then `meta gen` and a +review of the diff. A copy with lines of its own is an intentional customization. Check +only how far behind it is. + +**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported +home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, +`routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, +`outputParser`, `outputPrompt`, `extractor`, `renderHelper`), `traceHelperFile`, +`callableFile` and the rest. The CLI's own prompt-gate warning tells adopters to import +`promptRender` from exactly this path. A project importing from it is CORRECT, and +reporting it as un-adopted scaffold-and-own is a false finding. + +**The one version where an import is a blocker.** `entityFile` / `queriesFile` / +`routesFile` / `barrel` were removed from that subpath at 1.0 (ADR-0035 A3) and restored in +1.1 (ADR-0034 Amendment 5). On a 1.0.x install a config importing them fails to load with +a message naming `meta eject`. Check the installed version before reading a hit as a +finding, and match the four NAMES, never the path alone: ```bash +npm list @metaobjectsdev/codegen-ts grep -rn '@metaobjectsdev/codegen-ts/generators' . \ | grep -E 'entityFile|queriesFile|routesFile|barrel' ``` -A hit is a 1.0 upgrade blocker (the import fails to resolve) — recommend `meta init` (or -`meta eject `) to scaffold the owned copies, then import those locally. The owned -generator files themselves (`codegen/generators/*.ts`) are the second thing to audit: are -they stale relative to the reference templates, or intentionally customized? +On 1.0.x a hit is an upgrade blocker: recommend upgrading to 1.1, or `meta eject ` +and importing the copies locally. On 1.1 and later a hit is correct. --- diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 0462bfdd7..62e7aa00c 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -119,16 +119,16 @@ PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself. ## The generators Server-side, framework-neutral. **None is wired by default** — `meta init` writes -`generators: []` and an empty `codegen/generators/`. `meta eject ...` copies the -ownable ones into your repo, imported from `./codegen/generators/*` (ADR-0034); 1.0 -REMOVED their `@metaobjectsdev/codegen-ts/generators` export, so an owned copy is the only -path for those. The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. The `/generators` subpath itself is NOT deprecated. It -exports the prompt tier (`promptRender`, `outputParser`, `outputPrompt`, `extractor`, -`renderHelper`), `namesFile` and `requirementTests`, which you may import from there OR -eject to own, and it is the only home of the package-only generators — `traceHelperFile`, -`callableFile` — which ship no reference template (`meta gen --list` marks them -`package-only`). +`generators: []` and an empty `codegen/generators/`. Every generator below is imported from +`@metaobjectsdev/codegen-ts/generators` to run it as shipped; `meta gen --list` prints +each import line. To change what one emits, `meta eject ...` copies it into your +repo, imported from `./codegen/generators/*` instead (ADR-0034); the factory keeps its +name. Import by default and eject to customize: an owned copy you never change only falls +behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed +from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the +only path for those four.) The engine primitives come from the package main entry, +`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in +`meta gen --list`: the `template` primitive and `shared-model`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md index 786b20764..189c9a88f 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md @@ -22,19 +22,18 @@ provider/LLM-call layer — you compose the call yourself. ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// `meta init` scaffolds ./codegen/generators/ EMPTY (ADR-0034 Amendment 2). The entity -// generators arrive by `meta eject entity queries barrel`, which copies each into that -// directory as a file you own and prints the import to add — these three lines. They -// have no package import: the old `@metaobjectsdev/codegen-ts/generators` re-export of -// them was removed at 1.0. The parser's reply type is the @responseRef value object's -// own interface, which `entity` emits, so `entity` has to be in the run. -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -// promptRender / outputParser can be imported from the package (below) OR owned: -// `meta eject prompt-render output-parser` copies them into ./codegen/generators/ the -// same way. Either way the render and parse ENGINES they call stay in the package. -import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators"; +// `meta init` wires nothing (ADR-0034 Amendment 2). Each generator is imported from the +// package to run it as shipped. The parser's reply type is the @responseRef value +// object's own interface, which `entity` emits, so `entity` has to be in the run. +// (On a 1.0.x install the first three have no package export: `meta eject entity queries +// barrel` and import them from ./codegen/generators/ instead.) +// +// Any of these can be OWNED instead: `meta eject prompt-render output-parser` copies them +// into ./codegen/generators/ and prints the local import to swap in. Either way the +// render and parse ENGINES they call stay in the package. +import { + entityFile, queriesFile, barrel, promptRender, outputParser, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", 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 64e971853..da561ebbe 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 @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. 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 index 64e971853..da561ebbe 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md index ecf5cc3ac..4682f2370 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -25,13 +25,16 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid - [ ] MetaObjects present? (`metaobjects/` dir, metadata sources, `@metaobjectsdev/*` / `com.metaobjects:*` / `metaobjects` / `MetaObjects.*` deps). - [ ] Count metadata source lines + all `@generated` / `DO NOT EDIT` files repo-wide. -- [ ] **Owned-generators check:** does the project own generators at `codegen/generators/*` - (scaffold-and-own via `meta eject`, or `meta init` on a CLI before 1.0.4), or still import `entityFile` / `queriesFile` / - `routesFile` / `barrel` from `@metaobjectsdev/codegen-ts/generators`? Those four were - REMOVED at 1.0, so an import of them is an upgrade blocker, not just a style finding. - **Match the four NAMES, never the subpath alone** — that subpath is the supported home of - the non-ownable generators (`promptRender`, `outputParser`, `routesFileHono`, …), so - flagging a hit on the path convicts a correct project. +- [ ] **Owned-generators check:** which generators does the project own at + `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and + which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported + from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that + falls behind the reference: `meta eject --list` marks each copy, and one with no lines + of its own should be replaced by the package import. **An import of `entityFile` / + `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 + removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by + upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise + correct and is never a finding by itself. - [ ] **Cross-language version consistency (silent-drift check).** If the project uses MetaObjects in more than one language (e.g. a TS web client + a Java/Python/C# backend), enumerate EVERY MetaObjects package across ALL ecosystems (npm `@metaobjectsdev/*`, Maven `com.metaobjects:*`, PyPI `metaobjects`, NuGet `MetaObjects.*`) and record each version. **The version-number LINES differ by ecosystem (npm/PyPI/NuGet `0.x`/`1.x` vs Maven `7.x`/`8.x`), so you CANNOT eyeball drift** — a `0.12` next to a `7.7` looks fine but can be badly out of sync. Compare the **`metamodelVersion`** each port reports (the shared spec version on the registry manifest): a mismatch is real cross-language drift and a **finding** — the ports disagree on vocabulary/wire behavior. Also flag any port not on the latest release for its ecosystem. (This is a known real-world failure mode: newest backend, stale client, invisible because the numbers differ.) - [ ] Classify: **Greenfield** (none/minimal) · **Partial** · **Deep** → choose path below. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md index 337872160..feab2c601 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -71,43 +71,52 @@ here — the table is its second pass, not its definition. `meta eject ...` copies the reference generator templates a project chooses into `codegen/generators/*.ts`. Since 1.0.4 `meta init` scaffolds that directory EMPTY with `generators: []` (a project initialised on an earlier CLI got four wired copies, so both -shapes are normal). `metaobjects.config.ts` imports the **owned** local copies: +shapes are normal). A generator reaches `metaobjects.config.ts` one of two ways, and both +are supported from 1.1: ```ts -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; +// run as shipped — nothing copied, fixes arrive by upgrading +import { entityFile, queriesFile } from "@metaobjectsdev/codegen-ts/generators"; +// owned — a local copy the project can change +import { routesFile } from "./codegen/generators/routes"; ``` -**Removed import path.** `entityFile` / `queriesFile` / `routesFile` / `barrel` were -exported from `@metaobjectsdev/codegen-ts/generators` and deprecated; **1.0 removed them** -(ADR-0035 A3). On 1.0+ this no longer resolves: +**Audit the owned copies first.** A copy exists to carry a change. One that carries none +is duplication, and it falls behind the reference: -```ts -import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; +```bash +meta eject --list ``` -**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported, -non-deprecated home of the generators it still exports — the prompt/output tier -(`promptRender`, `outputParser`, `outputPrompt`, `extractor`, `renderHelper`), -`traceHelperFile`, `routesFileHono`, `namesFile` and `callableFile`. The prompt tier can -also be ejected (`meta eject prompt-render`), but owning it is optional; `traceHelperFile` -and `callableFile` are package-only and this subpath is their only home. The CLI's own -prompt-gate warning tells adopters to import `promptRender` from exactly this path. A -project importing those from it is CORRECT, and reporting it as un-adopted scaffold-and-own -is a false finding. - -**Finding this in a project:** grep for the four removed NAMES on that path, never the path -alone: +marks each owned copy `identical to the reference`, `same content as the reference, your +formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is +identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing +its local import with the package import and deleting the file, then `meta gen` and a +review of the diff. A copy with lines of its own is an intentional customization. Check +only how far behind it is. + +**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported +home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, +`routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, +`outputParser`, `outputPrompt`, `extractor`, `renderHelper`), `traceHelperFile`, +`callableFile` and the rest. The CLI's own prompt-gate warning tells adopters to import +`promptRender` from exactly this path. A project importing from it is CORRECT, and +reporting it as un-adopted scaffold-and-own is a false finding. + +**The one version where an import is a blocker.** `entityFile` / `queriesFile` / +`routesFile` / `barrel` were removed from that subpath at 1.0 (ADR-0035 A3) and restored in +1.1 (ADR-0034 Amendment 5). On a 1.0.x install a config importing them fails to load with +a message naming `meta eject`. Check the installed version before reading a hit as a +finding, and match the four NAMES, never the path alone: ```bash +npm list @metaobjectsdev/codegen-ts grep -rn '@metaobjectsdev/codegen-ts/generators' . \ | grep -E 'entityFile|queriesFile|routesFile|barrel' ``` -A hit is a 1.0 upgrade blocker (the import fails to resolve) — recommend `meta init` (or -`meta eject `) to scaffold the owned copies, then import those locally. The owned -generator files themselves (`codegen/generators/*.ts`) are the second thing to audit: are -they stale relative to the reference templates, or intentionally customized? +On 1.0.x a hit is an upgrade blocker: recommend upgrading to 1.1, or `meta eject ` +and importing the copies locally. On 1.1 and later a hit is correct. --- diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index 6efd42f91..d0e4c90e4 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -16,8 +16,9 @@ replace — if the libraries disappeared tomorrow, you keep working code. **MetaObjects is the core; the generators are yours** (ADR-0034 Amendments 3 and 4). The core — model, loader, registry, `verify`, `migrate`, prompt render and reply extract — is what MetaObjects guarantees. On it, you build the generators the application needs. The -generators MetaObjects ships are reference examples: copy one with `eject` when it is -close to what you need, and write your own when it is not. When a generated file is +generators MetaObjects ships are reference helpers: run one as shipped when it already +emits what you want, copy it with `eject` when it is close and you need to change it, and +write your own when nothing fits. When a generated file is wrong, fix the generator that emitted it — do not hand-edit around it, and do not treat it as a broken core guarantee. @@ -114,8 +115,13 @@ change) and two worked examples to copy — JSON Schema and OpenAPI 3.1, example product surface — are in the guide "Write your own generator": . -**Eject instead** only when a reference generator already emits something close to what -you need: `meta gen --list --probe` is the catalog, `meta eject ` copies one in. +**Use a stock generator as shipped** when one already emits what you need: +`meta gen --list --probe` is the catalog, and each row says how to wire it with nothing +copied into the repo (an import line on TypeScript, a stable name on the other ports). An +upgrade is then how its fixes reach you. **Eject** when a reference generator is close and +you need to change what it emits: `meta eject ` copies it in. Do not eject a +generator you are not going to change, because an unmodified copy only duplicates the +reference and falls behind it. **Hand-write** only what the model cannot express — business logic, calls to other systems — and have it import the generated types. @@ -263,7 +269,11 @@ they emit. - Do **NOT** apply that rule to `client`. `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. -5. **`meta eject --format json`** — copies each into `codegen/generators/` +5. **Wire what you chose.** A generator you will run unchanged is wired from its package: + on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` + and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + For a generator whose output you need to change, + **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one consolidated install command, and any config keys those generators read. On TypeScript, a generator whose output calls an HTTP adapter (`routes`, `routes-hono`, diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 8af3875e2..e4abee17d 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected, `meta eject ` it first. A generator that `meta eject --list` does not name (the prompt tier, for one) has no reference template yet: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | @@ -42,6 +42,26 @@ Every port gets a reference generator's source the same way — its `eject` comm writes a new one against the same small interface (the top of the skill). Your language reference has the per-port mechanics. +## Run it as shipped, or own a copy + +Owning a generator is how you **change** it. It is not a precondition for running one, and +a copy you never change is a cost: it duplicates the reference and falls behind it, so the +project misses fixes it would have received by upgrading. + +| You want | Do | +|---|---| +| A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | +| To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | + +A generator you import is still a reference helper: its output can change when you upgrade, +and `verify --codegen` reports that as drift until you regenerate. If output has to hold +still across upgrades, own the copy. + +On TypeScript `@metaobjectsdev/*` 1.0.x, `entityFile`, `queriesFile`, `routesFile` and +`barrel` are not exported from the package and an owned copy is the only way to run them. +1.1 restored the import. Check the installed version before replacing a copy with it. + ## A generator of your own beside the references Real apps routinely need output no reference emits as-is — a bespoke REST contract, custom diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 0462bfdd7..62e7aa00c 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -119,16 +119,16 @@ PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself. ## The generators Server-side, framework-neutral. **None is wired by default** — `meta init` writes -`generators: []` and an empty `codegen/generators/`. `meta eject ...` copies the -ownable ones into your repo, imported from `./codegen/generators/*` (ADR-0034); 1.0 -REMOVED their `@metaobjectsdev/codegen-ts/generators` export, so an owned copy is the only -path for those. The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. The `/generators` subpath itself is NOT deprecated. It -exports the prompt tier (`promptRender`, `outputParser`, `outputPrompt`, `extractor`, -`renderHelper`), `namesFile` and `requirementTests`, which you may import from there OR -eject to own, and it is the only home of the package-only generators — `traceHelperFile`, -`callableFile` — which ship no reference template (`meta gen --list` marks them -`package-only`). +`generators: []` and an empty `codegen/generators/`. Every generator below is imported from +`@metaobjectsdev/codegen-ts/generators` to run it as shipped; `meta gen --list` prints +each import line. To change what one emits, `meta eject ...` copies it into your +repo, imported from `./codegen/generators/*` instead (ADR-0034); the factory keeps its +name. Import by default and eject to customize: an owned copy you never change only falls +behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed +from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the +only path for those four.) The engine primitives come from the package main entry, +`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in +`meta gen --list`: the `template` primitive and `shared-model`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md index 786b20764..189c9a88f 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md @@ -22,19 +22,18 @@ provider/LLM-call layer — you compose the call yourself. ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// `meta init` scaffolds ./codegen/generators/ EMPTY (ADR-0034 Amendment 2). The entity -// generators arrive by `meta eject entity queries barrel`, which copies each into that -// directory as a file you own and prints the import to add — these three lines. They -// have no package import: the old `@metaobjectsdev/codegen-ts/generators` re-export of -// them was removed at 1.0. The parser's reply type is the @responseRef value object's -// own interface, which `entity` emits, so `entity` has to be in the run. -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -// promptRender / outputParser can be imported from the package (below) OR owned: -// `meta eject prompt-render output-parser` copies them into ./codegen/generators/ the -// same way. Either way the render and parse ENGINES they call stay in the package. -import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators"; +// `meta init` wires nothing (ADR-0034 Amendment 2). Each generator is imported from the +// package to run it as shipped. The parser's reply type is the @responseRef value +// object's own interface, which `entity` emits, so `entity` has to be in the run. +// (On a 1.0.x install the first three have no package export: `meta eject entity queries +// barrel` and import them from ./codegen/generators/ instead.) +// +// Any of these can be OWNED instead: `meta eject prompt-render output-parser` copies them +// into ./codegen/generators/ and prints the local import to swap in. Either way the +// render and parse ENGINES they call stay in the package. +import { + entityFile, queriesFile, barrel, promptRender, outputParser, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md index 0fd2eaae3..2b95f274b 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md index 0fd2eaae3..2b95f274b 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md @@ -33,11 +33,11 @@ Not there? Run `meta docs`. It reads the metadata and the config and needs nothi - **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. - Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and `meta eject ...` copies the ones you choose into your repo. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Eject a reference generator only when one already emits something close; never hand-write output the model fully describes. +- **Nothing is generated until you wire it.** No port ships a default generator suite: a fresh `meta init` writes `generators: []` and an empty `codegen/generators/`, and `--generators` is required on the C# and Python CLIs. `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. - **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. - **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path, your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are starting points you take a copy of, not a vendor dependency. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. - Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. - **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. - The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases. diff --git a/server/typescript/packages/cli/README.md b/server/typescript/packages/cli/README.md index 035ea164e..a3c5def43 100644 --- a/server/typescript/packages/cli/README.md +++ b/server/typescript/packages/cli/README.md @@ -67,8 +67,11 @@ $EDITOR metaobjects/meta.myapp.json # see .metaobjects/AGENTS.md for format # how many files it would emit, so you can see what your metadata already asks for. meta gen --list --probe -# 4. Take the ones you want. Prints the import, the entry to wire, and what to install. -meta eject entity queries routes barrel +# 4. Wire the ones you want. The catalog prints the import that runs each generator as +# shipped; add it to metaobjects.config.ts and call it in `generators: [...]`. +# Need to change what one emits? Copy it into your repo and own it. Prints the import +# to swap in, the entry to wire, and what to install. +meta eject entity # Need an output none of them emits (OpenAPI, JSON Schema, a client, docs)? Write your # own: this scaffolds a working, commented generator and wires it into the config. @@ -100,7 +103,7 @@ Running `meta` with no arguments prints a concise status line (whether a `metaob 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. -**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; `meta eject ...` copies the ones you choose into `codegen/generators/` — **yours to edit** — and prints the import line, the entry to add, the install command and any config keys those generators read. `meta gen` runs your local copies, never the package's. +**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. 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. Flags: - `--force` — overwrite scaffold files (memory records preserved) @@ -185,15 +188,11 @@ Flags: Two config files, by design: -**`metaobjects.config.ts`** (at repo root) — generator wiring and codegen knobs, type-checked TS. It starts with `generators: []`; each import below appears when you `meta eject` that generator, which copies it into `codegen/generators/` (ADR-0034 scaffold-and-own) and prints the line to add: +**`metaobjects.config.ts`** (at repo root) — generator wiring and codegen knobs, type-checked TS. It starts with `generators: []`. Import a stock generator from its package to run it as shipped (`meta gen --list` prints each line): ```ts import { defineConfig } from "@metaobjectsdev/cli"; -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { namesFile } from "./codegen/generators/names"; -import { barrel } from "./codegen/generators/barrel"; +import { entityFile, queriesFile, routesFile, namesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "packages/database/src/generated", @@ -205,9 +204,14 @@ export default defineConfig({ }); ``` -> Importing these generator factories from `@metaobjectsdev/codegen-ts/generators` -> still works but is **deprecated** (ADR-0034) — own a local copy instead. The -> package export will be removed in a future major. +To change what one emits, `meta eject ` copies it into `codegen/generators/` +(ADR-0034 scaffold-and-own) and prints the local import to put in place of the package +one, for example `import { entityFile } from "./codegen/generators/entity";`. The factory +keeps its name, so nothing else in the config changes. + +> `entityFile`, `queriesFile`, `routesFile` and `barrel` were removed from +> `@metaobjectsdev/codegen-ts/generators` at 1.0 and restored in 1.1 (ADR-0034 +> Amendment 5). Both the import and an owned copy are supported. ### Multiple output targets @@ -217,11 +221,9 @@ concern — declare named **targets** and point generators at them with `target` ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators — copied in by `meta eject` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Stock generators, run as shipped. An owned copy (`meta eject`) is wired the same way, +// from "./codegen/generators/". +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack"; diff --git a/server/typescript/packages/cli/src/commands/eject.ts b/server/typescript/packages/cli/src/commands/eject.ts index 65cf485df..892b80751 100644 --- a/server/typescript/packages/cli/src/commands/eject.ts +++ b/server/typescript/packages/cli/src/commands/eject.ts @@ -24,7 +24,7 @@ import { parseEjectArgs } from "../lib/args.js"; import { log } from "../lib/log.js"; import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js"; import { compareOwnedCopy, type OwnedComparison } from "../lib/owned-copy.js"; -import { composeCatalog, packageOf } from "../lib/catalog.js"; +import { composeCatalog, factoryModuleOf, packageOf } from "../lib/catalog.js"; import { installSetFor, type InstallSet, type OwnedRuntimeInstall } from "../lib/install-set.js"; import { ejectRuntime, ejectsRuntime, runtimeCopyStatus, runtimeDiffCommand, runtimeInstall, @@ -531,12 +531,6 @@ function reportLibrary(result: LibraryEjectResult): void { * already-copied half as "preserved" and the adopter cannot tell what happened. * Returns the unknown names, or an empty array. */ -/** Where a package-only generator's factory is imported from. The core package serves - * its generator factories from the `/generators` subpath; the UI packages from the root. */ -function packageFactoryPath(name: string): string { - const pkg = packageOf(name) ?? ""; - return pkg === CORE_CODEGEN_PACKAGE ? `${pkg}/generators` : pkg; -} function unknownNames(names: readonly string[]): string[] { return names.filter((n) => resolveSource(n) === undefined && !isLibraryName(n)); @@ -613,7 +607,7 @@ function reportOne(result: EjectResult, name: string): void { log.info(`In metaobjects.config.ts, "${result.exportName}" must resolve to this file:`); log.info(` ${result.importLine}`); log.info( - ` - If it is imported from "${result.packageName}", REPLACE that import with the ` + + ` - If it is imported from "${factoryModuleOf(name) ?? result.packageName}", REPLACE that import with the ` + "line above. Adding a second one leaves `generators` bound to the PACKAGED " + "generator, and your edits to this file do nothing.", ); @@ -691,7 +685,7 @@ export async function ejectCommand( const unknown = unmatched.filter((n) => packageOf(n) === undefined); if (packageOnly.length > 0) { log.error( - `package-only generator(s): ${packageOnly.map((n) => `${n} (${packageFactoryPath(n)})`).join(", ")}. ` + + `package-only generator(s): ${packageOnly.map((n) => `${n} (${factoryModuleOf(n) ?? ""})`).join(", ")}. ` + "No reference template ships for them, so they cannot be ejected: import the factory " + "from the package named beside each one and wire it in `generators: [...]`. " + "Nothing was ejected.", diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index 32543b739..f6e10fa23 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -17,8 +17,9 @@ import { DEFAULT_DOCS_DIR } from "@metaobjectsdev/codegen-ts"; import { describeError } from "../lib/error-text.js"; import { SUBTYPE_ROOT, TYPE_METADATA, TYPE_SUBTYPE_SEPARATOR } from "@metaobjectsdev/metadata/constants"; -// ADR-0034 scaffold-and-own — `meta init` copies the codegen reference templates into -// the consumer's repo so they OWN them; metaobjects.config.ts imports them locally. +// ADR-0034 scaffold-and-own — where `meta eject` and `meta generator new` put a generator +// the consumer OWNS. `meta init` scaffolds it empty; metaobjects.config.ts imports from it +// locally, or imports a stock generator from its package (Amendment 5). const OWNED_GENERATORS_DIR = "codegen/generators"; @@ -105,14 +106,21 @@ export default defineConfig({ // would emit, so you can see what your metadata is already asking for. Group by // \`layer\`: model / persistence / api / client / docs / capability. // - // Then take the ones you want: + // Every row carries a \`wire.import\` line and a \`wire.entry\`. To run a generator as + // shipped, add its import at the top of this file and its entry below: // - // meta eject entity queries routes barrel + // import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; // - // That copies each generator into ./codegen/generators/ — YOURS to edit, and what - // \`meta gen\` runs — and prints the import line to add here, the entry to add below, - // what to install, and any config keys those generators read (\`dbImport\`, - // \`apiPrefix\`, \`extStyle\`, …). Add them here as you go. + // then call each one in the array below: entityFile(), queriesFile(), barrel(). + // The row also lists what to install and the config keys the generator reads + // (\`dbImport\`, \`apiPrefix\`, \`extStyle\`, …). Add them here as you go. + // + // Need one to emit something different? Own it: + // + // meta eject entity + // + // copies that generator into ./codegen/generators/ — YOURS to edit — and prints the + // local import to swap in for the package one. \`meta gen\` then runs your copy. // // Need an output nothing in that catalog emits — OpenAPI, JSON Schema, a client, a // service layer, docs? Write your own generator; that is the normal path: @@ -148,9 +156,11 @@ Next steps: 1. Author entities under metaobjects/ (start from the scaffolded meta.common.json) 2. meta gen --list --probe # the catalog: every generator, grouped by layer, with # how many files each would emit for YOUR model - 3. meta eject ... # take the ones you want — copies them into + 3. Wire the ones you want in metaobjects.config.ts: each catalog row prints the + import that runs it as shipped. + meta eject ... # need to change what one emits? copies it into # codegen/generators/ (yours to edit) and prints the - # import, the entry to wire, and what to install + # import to swap in and what to install meta generator new # an output no reference emits? write your own — this # scaffolds a working one and wires it 4. meta gen # generate from exactly what you wired diff --git a/server/typescript/packages/cli/src/lib/catalog-listing.ts b/server/typescript/packages/cli/src/lib/catalog-listing.ts index 97cb0a5d3..725d411d1 100644 --- a/server/typescript/packages/cli/src/lib/catalog-listing.ts +++ b/server/typescript/packages/cli/src/lib/catalog-listing.ts @@ -29,7 +29,7 @@ import { import * as coreTpl from "@metaobjectsdev/codegen-ts"; import * as reactTpl from "@metaobjectsdev/codegen-ts-react"; import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack"; -import { composeCatalog, listCatalog, packageOf } from "./catalog.js"; +import { composeCatalog, listCatalog, packageOf, packagedWiring, type GeneratorWiring } from "./catalog.js"; import { buildLibraryRows, renderLibraryText, type LibraryCatalogRow, type LibraryProjectContext, @@ -69,6 +69,12 @@ export interface GeneratorCatalogRow { requires?: readonly string[]; configKeys?: readonly string[]; install: { dev: string[]; runtime: string[] }; + /** + * How to run this generator AS SHIPPED: the import line for its package factory and the + * entry for `generators: [...]`. No copy involved; `meta eject` prints the same pair for + * an owned one. Absent on the `docs` layer, whose door is `meta docs`. + */ + wire?: GeneratorWiring; source: { kind: "reference-template" | "package-only"; ejectable: boolean; @@ -290,6 +296,7 @@ export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promis ? installSetFor([entry]) : installSetFor([entry], [], { generators: new Set([entry.name]), ...runtimeInstall(owned.packages) }); const pkg = packageOf(entry.name) ?? ""; + const wire = packagedWiring(entry); const row: GeneratorCatalogRow = { name: entry.name, @@ -308,6 +315,7 @@ export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promis ...(entry.requires !== undefined ? { requires: entry.requires } : {}), ...(entry.configKeys !== undefined ? { configKeys: entry.configKeys } : {}), install: { dev: install.dev, runtime: install.runtime }, + ...(wire !== undefined ? { wire } : {}), source: { kind: entry.ejectable ? "reference-template" : "package-only", ejectable: entry.ejectable, @@ -428,11 +436,13 @@ const LAYER_BLURB: Record = { /** * ADR-0034 Amendment 3: every generator is a reference helper, not a guarantee. Said at * the top of the catalog because this is where an adopter decides what to depend on. + * Amendment 5: both ways to run one are named here — import it, or own a copy. */ export const REFERENCE_HELPER_NOTE: readonly string[] = [ - "Each generator is a reference helper, not a guarantee: `meta eject ` copies it into", - "your repo and the copy is yours to change. What MetaObjects guarantees (migrate, verify,", - "render) is not a generator and is not listed here.", + "Each generator is a reference helper, not a guarantee. Import it from its package to run", + "it as shipped, or `meta eject ` to copy it into your repo and change what it emits.", + "What MetaObjects guarantees (migrate, verify, render) is not a generator and is not", + "listed here.", "Your database, store or framework is not listed (MongoDB, DynamoDB, Express, NestJS, Vue, …)?", "Model it anyway and write or retarget a generator: the metaobjects-codegen skill's", "references/any-stack.md has the procedure. MySQL is `dialect: \"mysql\"`.", @@ -440,7 +450,7 @@ export const REFERENCE_HELPER_NOTE: readonly string[] = [ /** The footer line explaining the `package-only` row mark. */ export const PACKAGE_ONLY_NOTE = - "package-only = no reference template ships for it yet, so `meta eject` cannot copy it; " + + "package-only = no reference template ships for it, so `meta eject` cannot copy it; " + "import it from its package instead."; /** The human rendering: grouped by layer, because layer is the axis you select by. */ @@ -490,6 +500,12 @@ export function renderCatalogText(rows: CatalogRow[], probed: boolean): string { if (r.requires !== undefined && r.requires.length > 0) { lines.push(` ${" ".repeat(width)} requires: ${r.requires.join(", ")}`); } + // The import that runs it as shipped. Not shown once the project owns a copy: its + // wiring is then the local import `meta eject` printed, and showing the package's + // beside an `owned` mark invites wiring both. + if (r.wire !== undefined && r.source.owned !== true) { + lines.push(` ${" ".repeat(width)} ${r.wire.import}`); + } } // The library section sits between the generators and the footer: the footer's @@ -498,8 +514,9 @@ export function renderCatalogText(rows: CatalogRow[], probed: boolean): string { lines.push(...renderLibraryText(rows.filter((r): r is LibraryCatalogRow => r.kind === "library"))); lines.push(""); - lines.push("`meta eject ` copies a generator into codegen/generators/ and prints"); - lines.push("the import to add, the entry to wire, and what to install."); + lines.push("To run a generator as shipped, add the import under its row and call it in"); + lines.push("`generators: [...]`. To change what one emits, `meta eject ` copies it into"); + lines.push("codegen/generators/ and prints the import to swap in, the entry to wire, and what to install."); if (generators.some((r) => !r.source.ejectable)) lines.push(PACKAGE_ONLY_NOTE); if (!probed) { lines.push("Add --probe to see how many files each would emit for YOUR model."); diff --git a/server/typescript/packages/cli/src/lib/catalog.ts b/server/typescript/packages/cli/src/lib/catalog.ts index 2637a33bc..b11f8f966 100644 --- a/server/typescript/packages/cli/src/lib/catalog.ts +++ b/server/typescript/packages/cli/src/lib/catalog.ts @@ -44,6 +44,63 @@ export function catalogPackages(): string[] { return SLICES.map(([pkg]) => pkg); } + +/** The engine package. Its factories live on a `/generators` subpath, not at the root. */ +const CORE_CODEGEN_PACKAGE = "@metaobjectsdev/codegen-ts"; + +/** + * The module a catalog entry's factory is imported from to run it as shipped. + * + * The engine serves its factories from `/generators` (its root is the engine and the + * primitives an owned generator composes); the framework packages serve theirs from the + * root. Undefined for a name no slice registers. + */ +export function factoryModuleOf(name: string): string | undefined { + const pkg = packageOf(name); + if (pkg === undefined) return undefined; + return pkg === CORE_CODEGEN_PACKAGE ? `${pkg}/generators` : pkg; +} + +/** How one generator reaches `generators: [...]` — the import line and the entry. */ +export interface GeneratorWiring { + import: string; + entry: string; +} + +/** + * The option names an entry's `options` summary lists WITHOUT a trailing `?` — the ones + * its factory cannot be called without (`template`, `shared-model`). + */ +function requiredOptionNames(entry: GeneratorRegistryEntry): string[] { + return (entry.options ?? "") + .split(",") + .map((o) => o.trim()) + .filter((o) => o !== "" && !o.endsWith("?")); +} + +/** + * How to wire a catalog entry AS SHIPPED: import its factory from the package, no copy. + * + * This is the half of "import by default, eject to customize" the catalog owns. The + * other half is `meta eject`, which prints the same two facts for the owned copy — the + * symbol is the same in both (gated), so moving between them is one import line. + * + * Undefined for the `docs` layer: those generators are the engine behind `meta docs`, + * which is their door, and wiring one into `generators: [...]` is the deprecated route. + */ +export function packagedWiring(entry: GeneratorRegistryEntry): GeneratorWiring | undefined { + if (entry.layer === "docs") return undefined; + const from = factoryModuleOf(entry.name); + if (from === undefined) return undefined; + const required = requiredOptionNames(entry); + return { + import: `import { ${entry.factoryExport} } from "${from}";`, + entry: required.length === 0 + ? `${entry.factoryExport}()` + : `${entry.factoryExport}({ ${required.join(", ")} })`, + }; +} + /** * The composed catalog, keyed by stable name. * diff --git a/server/typescript/packages/cli/src/lib/load-metaobjects-config.ts b/server/typescript/packages/cli/src/lib/load-metaobjects-config.ts index 5b5ee3532..430ae0145 100644 --- a/server/typescript/packages/cli/src/lib/load-metaobjects-config.ts +++ b/server/typescript/packages/cli/src/lib/load-metaobjects-config.ts @@ -131,92 +131,6 @@ function resolveCliPkg(specifier: string): string { * import of an aliased package falls back to normal module resolution (and to * jiti's alias map on non-Bun runtimes). */ -/** - * 1.0 removed `entityFile` / `queriesFile` / `routesFile` / `barrel` from - * `@metaobjectsdev/codegen-ts/generators` (ADR-0035 A3; ADR-0034 scaffold-and-own). Without - * this check the removal surfaces as a bare runtime TypeError from the transpiled config — - * `(0, _index2.entityFile) is not a function` — which names neither MetaObjects, nor the - * removal, nor the one-command remedy. A breaking change an adopter WILL hit deserves a - * diagnostic, and this is the only place that can give one: the subpath still exists (it is - * the supported home of the generators with no ownable copy), so nothing upstream of here - * can tell a correct import from a removed one. - * - * It reads the config SOURCE because that is the arm that can name the FILE and the exact - * import. `removedGeneratorRuntimeError` below is the second arm, and an adopter estate is - * why there are two: the import does not have to be in the config. This project's config - * imported an OWNED generator — `./codegen/generators/entityFileTyped`, the shape `meta - * eject` itself produces — and THAT file carried the stale - * `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"`. The source scan saw - * a clean config and the adopter got exactly the bare TypeError this check exists to - * prevent: `(0 , _generators.entityFile) is not a function`, exit 2, no file, no remedy. - */ -const REMOVED_GENERATOR_EXPORTS = ["entityFile", "queriesFile", "routesFile", "barrel"] as const; -const GENERATORS_SUBPATH = "@metaobjectsdev/codegen-ts/generators"; - -export function removedGeneratorImportError(source: string): string | undefined { - const re = new RegExp( - `import\\s*(?:type\\s+)?\\{([^}]*)\\}\\s*from\\s*['"]${GENERATORS_SUBPATH.replace(/[/\\]/g, "\\$&")}['"]`, - "g", - ); - const hits = new Set(); - for (const m of source.matchAll(re)) { - for (const raw of (m[1] ?? "").split(",")) { - const name = raw.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim(); - if (name && (REMOVED_GENERATOR_EXPORTS as readonly string[]).includes(name)) hits.add(name); - } - } - if (hits.size === 0) return undefined; - const names = [...hits].sort(); - const templateOf = (n: string) => n.replace(/File$/, ""); - // One command per name: `meta eject` takes at most one generator name. - const commands = names.map((n) => ` meta eject ${templateOf(n)}`).join("\n"); - const example = names[0] ?? ""; - return ( - `metaobjects.config.ts imports ${names.join(", ")} from "${GENERATORS_SUBPATH}". ` + - `1.0 REMOVED ${names.length === 1 ? "that export" : "those exports"} (ADR-0034 scaffold-and-own): ` + - `these generators are yours to own, so the package no longer ships them as an import.\n` + - ` Fix — copy ${names.length === 1 ? "it" : "each of them"} into your repo:\n${commands}\n` + - ` then import the copies, e.g. ` + - `import { ${example} } from "./codegen/generators/${templateOf(example)}";\n` + - ` The subpath itself is fine — promptRender, outputParser, routesFileHono and the rest ` + - `still live there. Only ${names.length === 1 ? "this one" : `these ${names.length}`} moved.\n` + - ` Guide: docs/features/migrations/0.x-to-1.0.md §11` - ); -} - -/** - * The same diagnostic, recovered from the THROW instead of the source — for the case the - * source scan structurally cannot see: a removed export imported by a module the config - * imports, rather than by the config itself. - * - * It cannot name the file (the TypeError carries no reference back to the import that - * produced it — that limitation is real, it is just not a reason to say nothing), so it - * names what it does know: the symbol, the removal, and the one-command remedy. Matching is - * on the SYMBOL, not the message shape, because the shape is the transpiler's: Bun says - * `(0 , _generators.entityFile) is not a function` and Node ESM says - * `entityFile is not a function`. - */ -export function removedGeneratorRuntimeError(err: unknown): string | undefined { - const message = err instanceof Error ? err.message : String(err ?? ""); - if (!/is not a function/.test(message)) return undefined; - const hit = REMOVED_GENERATOR_EXPORTS.find((n) => new RegExp(`\\b${n}\\b`).test(message)); - if (hit === undefined) return undefined; - const template = hit.replace(/File$/, ""); - return ( - `${hit}() is not a function — 1.0 REMOVED it from "${GENERATORS_SUBPATH}" ` + - `(ADR-0034 scaffold-and-own): these generators are yours to own, so the package no ` + - `longer ships them as an import. Your metaobjects.config.ts does not import it, so ` + - `the import is in a module the config imports — most often an OWNED generator under ` + - `codegen/generators/, which is exactly what 'meta eject' writes.\n` + - ` Fix — copy it into your repo:\n meta eject ${template}\n` + - ` then change that module's import to: ` + - `import { ${hit} } from "./codegen/generators/${template}";\n` + - ` The subpath itself is fine — promptRender, outputParser, routesFileHono and the ` + - `rest still live there.\n` + - ` Guide: docs/features/migrations/0.x-to-1.0.md §11` - ); -} - function rewriteImportSpecifiers(source: string, aliasMap: Record): string { let result = source; for (const [specifier, resolvedPath] of Object.entries(aliasMap)) { @@ -450,8 +364,6 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise` copies one into your repo to change what it emits"] : ["author entities in this project's metadata sources then re-run `meta gen`"]; // An absent section is STATED, never omitted: a reader must be able to tell // "the scan found nothing" from "the scan never ran". diff --git a/server/typescript/packages/cli/test/catalog-factory-exports.test.ts b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts new file mode 100644 index 000000000..c22543c4a --- /dev/null +++ b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts @@ -0,0 +1,147 @@ +// Every catalog entry's `factoryExport` is RESOLVED, not trusted. +// +// "Import it from its package to run it as shipped" is only true if the package exports +// the factory under the name the catalog prints. `entityFile` / `queriesFile` / +// `routesFile` / `barrel` were NOT exported between 1.0 and 1.1 while the catalog still +// listed them, and nothing held the two together — so this gate imports each package's +// public factory module the way an adopter's `metaobjects.config.ts` does and asks for +// the function by name. +// +// The second half ties the two ownership paths to one symbol. `meta eject` reads the +// import line it prints out of the reference template's header; if that symbol differed +// from the package's, moving between "import it" and "own a copy" would stop being a +// one-line change, and the eject message telling an adopter to REPLACE the package +// import would name a binding their config does not have. + +import { describe, test, expect } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as coreFactories from "@metaobjectsdev/codegen-ts/generators"; +import * as reactFactories from "@metaobjectsdev/codegen-ts-react"; +import * as tanstackFactories from "@metaobjectsdev/codegen-ts-tanstack"; +import { factoryModuleOf, listCatalog, packagedWiring } from "../src/lib/catalog.js"; +import { buildCatalogListing, renderCatalogText, type GeneratorCatalogRow } from "../src/lib/catalog-listing.js"; +import { ejectGenerator } from "../src/commands/eject.js"; + +/** The public factory module per import path — what a config's `import { … } from` resolves. */ +const MODULES: Record> = { + "@metaobjectsdev/codegen-ts/generators": coreFactories, + "@metaobjectsdev/codegen-ts-react": reactFactories, + "@metaobjectsdev/codegen-ts-tanstack": tanstackFactories, +}; + +/** + * Entries whose factory cannot be called with no options, so the name check below cannot + * construct one through the public export. Each is a primitive that takes its identity + * from its caller; the registry's own `factory` supplies placeholders for the same reason. + */ +const NEEDS_OPTIONS = new Set(["template", "shared-model"]); + +function publicFactory(name: string, exportName: string): unknown { + const from = factoryModuleOf(name); + if (from === undefined) throw new Error(`no package registers "${name}"`); + const mod = MODULES[from]; + if (mod === undefined) throw new Error(`"${name}" is served from ${from}, which this gate does not import`); + return mod[exportName]; +} + +describe("catalog factory exports", () => { + test("every entry's factory is exported, under the name the catalog prints", () => { + const missing: string[] = []; + for (const entry of listCatalog()) { + if (typeof publicFactory(entry.name, entry.factoryExport) !== "function") { + missing.push(`${entry.name}: ${entry.factoryExport} is not exported from ${factoryModuleOf(entry.name)}`); + } + } + expect(missing).toEqual([]); + }); + + test("the exported factory builds the SAME generator the catalog constructs", () => { + const mismatched: string[] = []; + for (const entry of listCatalog()) { + if (NEEDS_OPTIONS.has(entry.name)) continue; + const build = publicFactory(entry.name, entry.factoryExport) as () => { name: string }; + const viaExport = build().name; + const viaCatalog = entry.factory().name; + if (viaExport !== viaCatalog) { + mismatched.push(`${entry.name}: export builds "${viaExport}", catalog builds "${viaCatalog}"`); + } + } + expect(mismatched).toEqual([]); + }); + + test("the four generators 1.0 removed are importable again", () => { + for (const name of ["entityFile", "queriesFile", "routesFile", "barrel"]) { + expect(typeof (coreFactories as Record)[name]).toBe("function"); + } + }); + + test("an owned copy and the package export the factory under one symbol", async () => { + const dir = mkdtempSync(join(tmpdir(), "mo-wire-")); + try { + const differing: string[] = []; + for (const entry of listCatalog()) { + if (!entry.ejectable) continue; + const ejected = await ejectGenerator({ cwd: dir, name: entry.name, force: false }); + if (ejected.exportName !== entry.factoryExport) { + differing.push(`${entry.name}: owned copy exports ${ejected.exportName}, package exports ${entry.factoryExport}`); + } + } + expect(differing).toEqual([]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + +describe("meta gen --list — how to wire a generator as shipped", () => { + test("every row outside the docs layer carries its package import and entry", async () => { + const rows = (await buildCatalogListing()).filter( + (r): r is GeneratorCatalogRow => r.kind === "generator", + ); + for (const row of rows) { + if (row.layer === "docs") { + // `meta docs` is that layer's door; a wiring line would teach the deprecated route. + expect({ name: row.name, wire: row.wire }).toEqual({ name: row.name, wire: undefined }); + continue; + } + expect(row.wire?.import).toMatch(/^import \{ \w+ \} from "@metaobjectsdev\/codegen-ts[\w/-]*";$/); + expect(row.wire?.entry).toMatch(/^\w+\((\{ [\w, ]+ \})?\)$/); + } + }); + + test("the engine's factories are served from /generators, the framework packages' from the root", async () => { + const byName = new Map( + (await buildCatalogListing()) + .filter((r): r is GeneratorCatalogRow => r.kind === "generator") + .map((r) => [r.name, r] as const), + ); + expect(byName.get("entity")?.wire).toEqual({ + import: `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`, + entry: "entityFile()", + }); + expect(byName.get("hooks")?.wire).toEqual({ + import: `import { tanstackQuery } from "@metaobjectsdev/codegen-ts-tanstack";`, + entry: "tanstackQuery()", + }); + }); + + test("a factory that cannot be called bare names the options it needs", () => { + const entry = listCatalog().find((e) => e.name === "shared-model"); + if (entry === undefined) throw new Error("shared-model left the catalog"); + expect(packagedWiring(entry)?.entry).toBe("sharedModelFile({ name, include })"); + }); + + test("the text listing prints the import under a row, and drops it once the copy is owned", async () => { + const rows = await buildCatalogListing(); + const text = renderCatalogText(rows, false); + expect(text).toContain(`import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`); + expect(text).toContain("Import it from its package to run"); + + const owned = rows.map((r) => + r.kind === "generator" && r.name === "entity" ? { ...r, source: { ...r.source, owned: true } } : r, + ); + expect(renderCatalogText(owned, false)).not.toContain(`import { entityFile } from`); + }); +}); diff --git a/server/typescript/packages/cli/test/eject-multi.test.ts b/server/typescript/packages/cli/test/eject-multi.test.ts index bb1311610..102af8627 100644 --- a/server/typescript/packages/cli/test/eject-multi.test.ts +++ b/server/typescript/packages/cli/test/eject-multi.test.ts @@ -75,15 +75,15 @@ describe("meta eject takes many names", () => { }); test("a package-only generator is named as such, not as unknown", async () => { - // `callable` is a real catalog entry with no reference template. Calling it + // `shared-model` is a real catalog entry with no reference template. Calling it // "unknown" sends the reader looking for a typo that is not there. const dir = tmp(); try { - expect(await ejectCommand(["callable"], dir, "text")).toBe(2); + expect(await ejectCommand(["shared-model"], dir, "text")).toBe(2); const err = erred.join("\n"); expect(err).toContain("package-only"); - expect(err).toContain("callable"); - expect(err).not.toContain("unknown name(s): callable"); + expect(err).toContain("shared-model"); + expect(err).not.toContain("unknown name(s): shared-model"); expect(err).toContain("@metaobjectsdev/codegen-ts"); expect(err).toContain("Nothing was ejected"); } finally { diff --git a/server/typescript/packages/cli/test/gen-list.test.ts b/server/typescript/packages/cli/test/gen-list.test.ts index 8a525d88f..2383f1676 100644 --- a/server/typescript/packages/cli/test/gen-list.test.ts +++ b/server/typescript/packages/cli/test/gen-list.test.ts @@ -100,9 +100,12 @@ describe("meta gen --list (ADR-0021 D3)", () => { await genCommand(["--list"], tmp); const text = out(); const line = (name: string) => text.split("\n").find((l) => l.startsWith(` ${name} `)) ?? ""; - expect(line("callable")).toContain("package-only"); + expect(line("shared-model")).toContain("package-only"); expect(line("template")).toContain("package-only"); - // An ejectable entry is never marked. + // An ejectable entry is never marked. `callable` and `trace-helper` were marked + // until they gained reference templates. + expect(line("callable")).not.toContain("package-only"); + expect(line("trace-helper")).not.toContain("package-only"); expect(line("prompt-render")).not.toContain("package-only"); expect(line("entity")).not.toContain("package-only"); expect(text).toContain("package-only = no reference template ships"); diff --git a/server/typescript/packages/cli/test/integration/stock-generators-importable.test.ts b/server/typescript/packages/cli/test/integration/stock-generators-importable.test.ts new file mode 100644 index 000000000..8a52dc3ea --- /dev/null +++ b/server/typescript/packages/cli/test/integration/stock-generators-importable.test.ts @@ -0,0 +1,183 @@ +/** + * The two ways to run a stock generator, end to end, through the BUILT CLI. + * + * 1.0 made an owned copy the only path for `entity` / `queries` / `routes` / `barrel`; + * 1.1 restored the package import (ADR-0034 Amendment 5). Both are supported, so both + * are driven here the way an adopter drives them — `metaobjects.config.ts`, `meta gen`, + * `meta verify --codegen` — rather than by calling the factories directly, because the + * failure 1.0 shipped lived in the config LOADER and no factory-level test could see it. + * + * Three claims, one test each: + * + * 1. Importing the four from the package generates, and the drift gate is clean. + * 2. An owned copy bound to the package emits what the import emits. This is the + * migration note's promise in both directions: an adopter can drop an unmodified + * owned copy for the import (or eject one they were importing) and regenerate + * nothing they did not expect. + * 3. What DOES change on a default eject is only where the HTTP-adapter import points, + * because `meta eject` hands over that source too. Pinned so the migration note + * can say "these lines and no others" and stay true. + * + * Subprocess-per-command for the reason `verify-codegen-ejected-generator` gives: the + * config is re-read under a fresh name each load but the modules it imports are cached. + */ +import { describe, test, expect, beforeAll } from "bun:test"; +import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { meta, requireFreshDist } from "./support/built-cli.js"; + +const FIXTURES = resolve(import.meta.dirname, "..", "fixtures"); +// Inside the workspace, so an ejected template's `@metaobjectsdev/codegen-ts` import +// resolves by walking up to the workspace root. +const WORKSPACE_TMP = join(FIXTURES, "__tmp__"); +const OUT = "generated"; +const STOCK = ["entity", "queries", "routes", "barrel"] as const; +const RUNTIME_PACKAGE = "@metaobjectsdev/runtime-ts"; + +function config(imports: string, generators: string): string { + return [ + `import { defineConfig } from "@metaobjectsdev/codegen-ts";`, + imports, + `export default defineConfig({`, + ` outDir: ${JSON.stringify(OUT)},`, + ` dialect: "sqlite",`, + ` dbImport: "~/db",`, + ` extStyle: "none",`, + ` generators: [${generators}],`, + `});`, + ].join("\n"); +} + +/** As shipped: the package's factories, no copy in the repo. */ +const IMPORTED = config( + `import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators";`, + `entityFile(), queriesFile(), routesFile(), barrel()`, +); + +const OWNED_IMPORTS = [ + `import { entityFile } from "./codegen/generators/entity.js";`, + `import { queriesFile } from "./codegen/generators/queries.js";`, + `import { routesFile } from "./codegen/generators/routes.js";`, + `import { barrel } from "./codegen/generators/barrel.js";`, +].join("\n"); + +/** Owned copies, with the two that import the HTTP adapter pointed back at the package. */ +const OWNED_ON_PACKAGE_RUNTIME = config( + OWNED_IMPORTS, + `entityFile({ runtimeImport: ${JSON.stringify(RUNTIME_PACKAGE)} }), queriesFile(), ` + + `routesFile({ runtimeImport: ${JSON.stringify(RUNTIME_PACKAGE)} }), barrel()`, +); + +/** Owned copies exactly as `meta eject` wires them: the adapter is the local copy. */ +const OWNED_DEFAULT = config(OWNED_IMPORTS, `entityFile(), queriesFile(), routesFile(), barrel()`); + +function setupRepo(configSource: string): string { + mkdirSync(WORKSPACE_TMP, { recursive: true }); + const root = mkdtempSync(join(WORKSPACE_TMP, "stock-importable-")); + cpSync(join(FIXTURES, "trainer-website-meta"), root, { recursive: true }); + writeFileSync(join(root, "metaobjects.config.ts"), configSource); + return root; +} + +/** Every generated file, path → contents. */ +function generated(root: string): Map { + const out = new Map(); + const dir = join(root, OUT); + for (const f of readdirSync(dir, { recursive: true, withFileTypes: true })) { + if (!f.isFile()) continue; + const abs = join(f.parentPath, f.name); + out.set(abs.slice(dir.length + 1), readFileSync(abs, "utf8")); + } + return out; +} + +// Precondition once per FILE, not per test: this gate runs the BUILT CLI. +beforeAll(requireFreshDist); + +describe("stock generators — imported from the package, or owned", () => { + test("importing entity, queries, routes and barrel from the package generates and verifies clean", async () => { + const root = setupRepo(IMPORTED); + try { + const gen = await meta(root, "gen"); + expect(gen).toMatchObject({ exit: 0 }); + + const files = [...generated(root).keys()]; + expect(files).toContain("User.ts"); + expect(files).toContain("User.queries.ts"); + expect(files).toContain("User.routes.ts"); + expect(files).toContain("index.ts"); + + expect(await meta(root, "verify", "--codegen")).toMatchObject({ exit: 0 }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, 120_000); + + test("an owned copy bound to the package emits exactly what the import emits", async () => { + const root = setupRepo(IMPORTED); + try { + expect(await meta(root, "gen")).toMatchObject({ exit: 0 }); + const imported = generated(root); + + // Take ownership of all four, changing nothing about them. + expect(await meta(root, "eject", ...STOCK)).toMatchObject({ exit: 0 }); + writeFileSync(join(root, "metaobjects.config.ts"), OWNED_ON_PACKAGE_RUNTIME); + + // The committed output was written by the PACKAGE's generators. If the owned + // copies would write anything else, this is where it shows. + expect(await meta(root, "verify", "--codegen")).toMatchObject({ exit: 0 }); + + expect(await meta(root, "gen")).toMatchObject({ exit: 0 }); + expect(generated(root)).toEqual(imported); + + // And back: dropping the unmodified copies for the import is equally a no-op. + writeFileSync(join(root, "metaobjects.config.ts"), IMPORTED); + expect(await meta(root, "verify", "--codegen")).toMatchObject({ exit: 0 }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, 180_000); + + test("a default eject moves only the HTTP-adapter import lines", async () => { + const root = setupRepo(IMPORTED); + try { + expect(await meta(root, "gen")).toMatchObject({ exit: 0 }); + const imported = generated(root); + + expect(await meta(root, "eject", ...STOCK)).toMatchObject({ exit: 0 }); + writeFileSync(join(root, "metaobjects.config.ts"), OWNED_DEFAULT); + expect(await meta(root, "gen")).toMatchObject({ exit: 0 }); + const owned = generated(root); + + expect([...owned.keys()].sort()).toEqual([...imported.keys()].sort()); + + const changed: string[] = []; + for (const [path, before] of imported) { + const after = owned.get(path) ?? ""; + if (after === before) continue; + changed.push(path); + // Compared as line SETS: the import block is sorted by specifier, so moving one + // import from a package name to a relative path also moves the line it sits on. + const a = new Set(before.split("\n")); + const b = new Set(after.split("\n")); + const onlyImported = [...a].filter((l) => !b.has(l)); + const onlyOwned = [...b].filter((l) => !a.has(l)); + expect({ path, lines: onlyOwned.length }).toEqual({ path, lines: onlyImported.length }); + // The package line names the runtime package; the owned line names the copy. + for (const line of onlyImported) { + expect({ path, line }).toEqual({ path, line: expect.stringContaining(RUNTIME_PACKAGE) }); + } + for (const line of onlyOwned) { + expect({ path, line }).toEqual({ path, line: expect.stringContaining("codegen/runtime/") }); + } + } + // Only the two generators whose output imports the adapter are affected: the + // entity module (allowlist types) and the routes module (mount helpers). + expect(changed.sort()).toEqual( + [...imported.keys()].filter((p) => !p.endsWith(".queries.ts") && p !== "index.ts").sort(), + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }, 180_000); +}); diff --git a/server/typescript/packages/cli/test/load-metaobjects-config.test.ts b/server/typescript/packages/cli/test/load-metaobjects-config.test.ts index bf2e99687..aa10aa908 100644 --- a/server/typescript/packages/cli/test/load-metaobjects-config.test.ts +++ b/server/typescript/packages/cli/test/load-metaobjects-config.test.ts @@ -33,6 +33,38 @@ describe("loadMetaobjectsConfig", () => { } }); + // 1.0 removed these four from the package and made an owned copy the only path; 1.1 + // restored them (ADR-0034 Amendment 5). The loader refused this exact config with a + // migration message in between, so it is the config to pin: it must LOAD, and every + // entry must be a generator the runner can call. + test("loads a config importing the stock entity, queries, routes and barrel factories", async () => { + const dir = mkdtempSync(join(tmpdir(), "mo-cfg-stock-")); + try { + writeFileSync( + join(dir, "metaobjects.config.ts"), + [ + `import { defineConfig } from "@metaobjectsdev/codegen-ts";`, + `import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators";`, + `export default defineConfig({`, + ` outDir: "out",`, + ` dialect: "sqlite",`, + ` dbImport: "../db",`, + ` generators: [entityFile(), queriesFile(), routesFile(), barrel()],`, + `});`, + ].join("\n"), + ); + const cfg = await loadMetaobjectsConfig(dir); + expect(cfg.generators.map((g) => (typeof g === "string" ? g : g.name))).toEqual([ + "entity-file", "queries-file", "routes-file", "barrel", + ]); + for (const g of cfg.generators) { + expect(typeof g === "string" ? undefined : typeof g.generate).toBe("function"); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + // Regression for the launch-B3 nodenext fix: `meta init` now scaffolds the // ADR-0034 owned-generator imports with a `.js` extension (nodenext-safe). The // jiti-based loader must still resolve those relative `.js` specifiers to the 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 4f7824f89..f7968edee 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 @@ -124,11 +124,11 @@ describe("metaobjects.config.ts wiring still scaffolded", () => { const configTs = readFileSync(join(cwd, "metaobjects.config.ts"), "utf8"); expect(configTs).toContain("defineConfig"); // ADR-0034 Amendment 2 — the scaffolded config wires nothing and imports nothing. - // What it must NOT do is reach for the deprecated package `/generators` export, - // which is the one import path 1.0 removed. + // The package's `/generators` path may appear in the comment that shows how to wire + // one; an uncommented import of it would be the default suite that amendment removed. expect(configTs).toContain("generators: []"); expect(configTs).not.toMatch(/^import .* from "\.\/codegen\/generators\//m); - expect(configTs).not.toContain("@metaobjectsdev/codegen-ts/generators"); + expect(configTs).not.toMatch(/^import .* from "@metaobjectsdev\/codegen-ts\/generators"/m); }); }); diff --git a/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts b/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts index 6665bd08b..593c7dabe 100644 --- a/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts +++ b/server/typescript/packages/cli/test/unit/init-scaffold-config.test.ts @@ -92,7 +92,10 @@ describe("meta init scaffolds the owned-codegen tier, empty (ADR-0034 Amendment await init({ cwd: tmp, quiet: true }); const body = readFileSync(join(tmp, "metaobjects.config.ts"), "utf-8"); expect(body).not.toMatch(/^import .* from "\.\/codegen\/generators\//m); - expect(body).not.toContain("@metaobjectsdev/codegen-ts/generators"); + // The package import appears ONLY as a commented example of how to wire one; an + // uncommented line would be the default suite Amendment 2 removed. + expect(body).not.toMatch(/^import .* from "@metaobjectsdev\/codegen-ts\/generators"/m); + expect(body).toMatch(/^\s*\/\/\s+import \{ entityFile, queriesFile, barrel \} from "@metaobjectsdev\/codegen-ts\/generators";$/m); }); test("an ejected generator is the copyable reference template, and re-init preserves it", async () => { @@ -103,7 +106,8 @@ describe("meta init scaffolds the owned-codegen tier, empty (ADR-0034 Amendment const entityPath = join(tmp, "codegen/generators/entity.ts"); const entity = readFileSync(entityPath, "utf-8"); expect(entity).toContain("REFERENCE TEMPLATE"); - // It imports the stable engine, never the deprecated `/generators` export. + // It imports the stable engine, never the packaged factory it is a copy of: an owned + // generator that called the package's would not own what it emits. expect(entity).toContain('from "@metaobjectsdev/codegen-ts"'); expect(entity).not.toContain("@metaobjectsdev/codegen-ts/generators"); diff --git a/server/typescript/packages/cli/test/unit/removed-generator-diagnostic.test.ts b/server/typescript/packages/cli/test/unit/removed-generator-diagnostic.test.ts deleted file mode 100644 index ee3e6fe78..000000000 --- a/server/typescript/packages/cli/test/unit/removed-generator-diagnostic.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// ADR-0034's removal has TWO doors, and only one of them was covered. -// -// `removedGeneratorImportError` reads the config SOURCE, so it fires when the config itself -// imports `entityFile` / `barrel` / … from "@metaobjectsdev/codegen-ts/generators". A -// polyglot adopter estate showed the other door: its config imported an OWNED generator — -// `./codegen/generators/entityFileTyped`, the exact shape `meta eject` writes — and THAT -// file carried the stale import. The source scan saw a clean config, and what the adopter -// got was `(0 , _generators.entityFile) is not a function`, exit 2, naming no file, no -// symbol origin and no remedy: the precise failure the first door exists to prevent. - -import { describe, test, expect } from "bun:test"; -import { - removedGeneratorImportError, - removedGeneratorRuntimeError, -} from "../../src/lib/load-metaobjects-config.js"; - -describe("removedGeneratorRuntimeError", () => { - test("recognises Bun's transpiled shape", () => { - const msg = removedGeneratorRuntimeError( - new TypeError("(0 , _generators.entityFile) is not a function"), - ); - expect(msg).toBeDefined(); - expect(msg).toContain("meta eject entity"); - expect(msg).toContain("ADR-0034"); - expect(msg).toContain("0.x-to-1.0.md"); - }); - - test("recognises Node's plain shape", () => { - // Matching is on the SYMBOL, not the message shape — the shape belongs to whichever - // transpiler ran, and an adopter's runtime is not ours to choose. - const msg = removedGeneratorRuntimeError(new TypeError("barrel is not a function")); - expect(msg).toContain("meta eject barrel"); - }); - - test("says WHERE to look, since the throw cannot name the file", () => { - const msg = removedGeneratorRuntimeError(new TypeError("entityFile is not a function"))!; - expect(msg).toContain("codegen/generators/"); - expect(msg).toMatch(/module (the|your) config imports/); - }); - - test("stays out of the way of unrelated TypeErrors", () => { - expect(removedGeneratorRuntimeError(new TypeError("foo is not a function"))).toBeUndefined(); - expect(removedGeneratorRuntimeError(new Error("entityFile exploded"))).toBeUndefined(); - expect(removedGeneratorRuntimeError(undefined)).toBeUndefined(); - }); - - test("a non-Error throw does not crash the diagnostic", () => { - expect(removedGeneratorRuntimeError("entityFile is not a function")).toContain("meta eject entity"); - }); -}); - -describe("the two doors agree", () => { - test("both name the removal, the remedy and the guide", () => { - const fromSource = removedGeneratorImportError( - `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`, - )!; - const fromThrow = removedGeneratorRuntimeError(new TypeError("entityFile is not a function"))!; - for (const needle of ["ADR-0034", "meta eject entity", "0.x-to-1.0.md §11"]) { - expect(fromSource).toContain(needle); - expect(fromThrow).toContain(needle); - } - }); -}); diff --git a/server/typescript/packages/cli/test/unit/removed-generator-imports.test.ts b/server/typescript/packages/cli/test/unit/removed-generator-imports.test.ts deleted file mode 100644 index 5d864a78a..000000000 --- a/server/typescript/packages/cli/test/unit/removed-generator-imports.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 1.0 removed entityFile / queriesFile / routesFile / barrel from -// `@metaobjectsdev/codegen-ts/generators`. Without a diagnostic the removal reaches an -// adopter as `(0, _index2.entityFile) is not a function` from the transpiled config — -// naming neither MetaObjects, nor the removal, nor the remedy. Observed on a real adopter -// estate before this check existed. -// -// BOTH arms matter. That subpath is the supported home of every generator with no ownable -// copy (promptRender, outputParser, routesFileHono, …); a check that fires on the PATH -// rather than the four NAMES would refuse to load five of the six estates surveyed at the -// cut, every one of them correct. -import { describe, test, expect } from "bun:test"; -import { removedGeneratorImportError } from "../../src/lib/load-metaobjects-config.js"; - -const SUB = "@metaobjectsdev/codegen-ts/generators"; - -describe("removed generator imports", () => { - test("names every removed import, and one eject command per name", () => { - const msg = removedGeneratorImportError( - `import { entityFile, queriesFile, routesFile, barrel } from "${SUB}";`, - ); - expect(msg).toBeDefined(); - for (const n of ["entityFile", "queriesFile", "routesFile", "barrel"]) { - expect(msg).toContain(n); - } - // `meta eject` takes at most ONE name, so a single four-name command would not run. - for (const t of ["meta eject entity", "meta eject queries", "meta eject routes", "meta eject barrel"]) { - expect(msg).toContain(t); - } - expect(msg).toContain("0.x-to-1.0.md"); - }); - - test("stays silent on the non-ownable generators that legitimately live there", () => { - expect(removedGeneratorImportError( - `import { promptRender, outputParser, renderHelper } from "${SUB}";`, - )).toBeUndefined(); - expect(removedGeneratorImportError( - `import { routesFileHono, namesFile, callableFile } from "${SUB}";`, - )).toBeUndefined(); - }); - - test("does not fire on a same-named import from somewhere else", () => { - // An owned copy is exactly this, and it must load. - expect(removedGeneratorImportError( - `import { entityFile } from "./codegen/generators/entity";`, - )).toBeUndefined(); - }); - - test("catches a mixed import — the removed half is still removed", () => { - const msg = removedGeneratorImportError( - `import { promptRender, entityFile } from "${SUB}";`, - ); - expect(msg).toBeDefined(); - expect(msg).toContain("entityFile"); - expect(msg).toContain("meta eject entity"); - }); - - test("catches renamed and type-only forms", () => { - expect(removedGeneratorImportError( - `import { entityFile as ef } from "${SUB}";`, - )).toBeDefined(); - expect(removedGeneratorImportError( - `import type { EntityFileOpts } from "${SUB}";`, - )).toBeUndefined(); // a type-only opts import is not a call site; it fails at typecheck - }); -}); diff --git a/server/typescript/packages/codegen-ts-react/src/generator-registry.ts b/server/typescript/packages/codegen-ts-react/src/generator-registry.ts index 2432d4971..880cec224 100644 --- a/server/typescript/packages/codegen-ts-react/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts-react/src/generator-registry.ts @@ -24,6 +24,7 @@ export const reactGeneratorRegistry: Record = { description: "Per-entity React form component over the generated Zod schema.", tier: "native", factory: () => formFile(), + factoryExport: "formFile", options: "filter?, target?", // `react`, not "the client framework": `@metaobjectsdev/tanstack` declares `react` // as a peer, so form (react) + hooks/grid (tanstack) is the intended composition, diff --git a/server/typescript/packages/codegen-ts-tanstack/src/generator-registry.ts b/server/typescript/packages/codegen-ts-tanstack/src/generator-registry.ts index a16ea1e1b..fa0e70a4b 100644 --- a/server/typescript/packages/codegen-ts-tanstack/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts-tanstack/src/generator-registry.ts @@ -21,6 +21,7 @@ export const tanstackGeneratorRegistry: Record = "Per-entity TanStack Query hooks (useEntity / useEntities / useCreate / useUpdate / useDelete).", tier: "native", factory: () => tanstackQuery(), + factoryExport: "tanstackQuery", options: "filter?, target?", framework: "tanstack", requires: ["entity"], @@ -37,6 +38,7 @@ export const tanstackGeneratorRegistry: Record = "Per-entity TanStack Table column definitions, from a layout.dataGrid declaration.", tier: "native", factory: () => tanstackGrid(), + factoryExport: "tanstackGrid", options: "filter?, tphSubtypeGrids?, target?", framework: "tanstack", requires: ["entity"], @@ -52,6 +54,7 @@ export const tanstackGeneratorRegistry: Record = "Per-entity server-driven grid state hook (sort/filter/page) over the generated columns.", tier: "native", factory: () => tanstackGridHook(), + factoryExport: "tanstackGridHook", options: "filter?, tphSubtypeGrids?, target?", framework: "tanstack", // `grid` is here because the emitted hook imports the columns module's filter diff --git a/server/typescript/packages/codegen-ts/README.md b/server/typescript/packages/codegen-ts/README.md index d66522bae..bbb6535d6 100644 --- a/server/typescript/packages/codegen-ts/README.md +++ b/server/typescript/packages/codegen-ts/README.md @@ -84,21 +84,19 @@ dependency tree entirely: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ generators: [entityFile({ allowlists: false }), queriesFile(), barrel()], }); ``` -The `entityFile` / `queriesFile` / `routesFile` / `barrel` factories are imported -from the **owned local copies** that `meta init` scaffolds into -`codegen/generators/` (ADR-0034 scaffold-and-own). Importing them from -`@metaobjectsdev/codegen-ts/generators` still works but is **deprecated** — own a -copy instead; the package export will be removed in a future major. The engine and -primitives (`runGen`, the scope helpers `perEntity` / `perPackage` / `perModel` +Every generator factory is imported from `@metaobjectsdev/codegen-ts/generators` to run +it as shipped. To change what one emits, `meta eject ` copies its reference template +into `codegen/generators/` (ADR-0034 scaffold-and-own), and the config imports that local +copy instead; the factory keeps its name. (`entityFile` / `queriesFile` / `routesFile` / +`barrel` were removed from that subpath at 1.0 and restored in 1.1, ADR-0034 Amendment 5.) +The engine and primitives (`runGen`, the scope helpers `perEntity` / `perPackage` / `perModel` (`oncePerRun` is a soft-deprecated alias of `perModel`), `RenderContext`, the loader and render helpers) remain the stable, versioned import from `@metaobjectsdev/codegen-ts`, and an owned generator imports them from there. @@ -191,11 +189,9 @@ For every `template.prompt` that declares `@responseRef` (ADR-0052 — the inbou ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators scaffolded by `meta init` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; -import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators"; +import { + entityFile, queriesFile, barrel, promptRender, outputParser, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ // entityFile() is required: it emits the value-object interfaces the parser returns. diff --git a/server/typescript/packages/codegen-ts/src/generator-registry.ts b/server/typescript/packages/codegen-ts/src/generator-registry.ts index bf446b9e5..a0bbb4ea2 100644 --- a/server/typescript/packages/codegen-ts/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts/src/generator-registry.ts @@ -27,14 +27,11 @@ import type { Generator } from "./generator.js"; import type { MetaobjectsGenConfig } from "./metaobjects-config.js"; import { REFERENCE_GENERATOR_NAMES } from "./reference-templates.js"; -// The four ADR-0034 ownable generators are no longer exported from ./generators/index.js -// (1.0 removed them — see that file's header). They remain the engine's internal composers -// and the stable-name registry still constructs them, so import them from their own modules. -import { entityFile } from "./generators/entity-file.js"; -import { queriesFile } from "./generators/queries-file.js"; -import { routesFile } from "./generators/routes-file.js"; -import { barrel } from "./generators/barrel.js"; import { + entityFile, + queriesFile, + routesFile, + barrel, callableFile, routesFileHono, namesFile, @@ -105,6 +102,16 @@ export interface GeneratorRegistryEntry { tier: GeneratorTier; /** Constructs the generator with sensible defaults. Calling it must not throw. */ factory: () => Generator; + /** + * The name this generator's factory is exported under from its package's public + * factory module — `@metaobjectsdev/codegen-ts/generators` for this package, the + * package root for the framework packages. It is what an adopter imports to run the + * generator as shipped, so `meta gen --list` prints it. + * + * Resolved, not trusted: a gate imports that module and requires a function under this + * name whose generator carries the same implementation name as {@link factory}'s. + */ + factoryExport: string; /** Optional one-line options summary for `--list`. */ options?: string; /** The framework this generator's OUTPUT targets. Absent = framework-neutral. */ @@ -178,6 +185,7 @@ export const generatorRegistry: Record = { description: "Per-entity Drizzle table + typed model module (the entity module).", tier: "native", factory: () => entityFile(), + factoryExport: "entityFile", options: "filter?, target?", requires: [], runtimePackages: ["@metaobjectsdev/runtime-ts"], @@ -193,6 +201,7 @@ export const generatorRegistry: Record = { description: "Per-entity physical database name constants (table/view, schema, columns).", tier: "native", factory: () => namesFile(), + factoryExport: "namesFile", options: "filter?, target?", configKeys: ["columnNamingStrategy"], ejectable: ejectable("names"), @@ -204,6 +213,7 @@ export const generatorRegistry: Record = { description: "Single index.ts re-exporting every generated entity module.", tier: "native", factory: () => barrel(), + factoryExport: "barrel", options: "target?", requires: ["entity"], configKeys: ["extStyle"], @@ -218,6 +228,7 @@ export const generatorRegistry: Record = { description: "Per-entity typed query helpers (findById/create/...).", tier: "native", factory: () => queriesFile(), + factoryExport: "queriesFile", options: "filter?, target?", requires: ["entity"], // zod: a TYPE-only import — the create inputs are `z.inputInsertSchema>`. @@ -234,6 +245,7 @@ export const generatorRegistry: Record = { description: "Per-entity Fastify CRUD routes (drizzle-fastify mountCrudRoutes).", tier: "native", factory: () => routesFile(), + factoryExport: "routesFile", options: "filter?, target?", framework: "fastify", requires: ["entity"], @@ -249,6 +261,7 @@ export const generatorRegistry: Record = { description: "Per-entity Hono CRUD routes (runtime-ts/hono mountCrudRoutes).", tier: "native", factory: () => routesFileHono(), + factoryExport: "routesFileHono", options: "filter?, target?", framework: "hono", requires: ["entity"], @@ -267,6 +280,7 @@ export const generatorRegistry: Record = { "Per-entity/template SDK API reference (the generated code's API, human + agent forms).", tier: "native", factory: () => apiDocsFile(), + factoryExport: "apiDocsFile", options: "filter?, target?", ejectable: ejectable("api-docs"), }, @@ -277,6 +291,7 @@ export const generatorRegistry: Record = { description: "Neutral per-entity / per-template Markdown documentation pages.", tier: "neutral", factory: () => docsFile(), + factoryExport: "docsFile", ejectable: ejectable("docs"), note: "neutral artifact — use `meta docs` (the single docs door, ADR-0021 D1); not part of the recommended `meta gen` native suite.", }, @@ -287,6 +302,7 @@ export const generatorRegistry: Record = { description: "Mermaid ER diagram of the entity/relationship model.", tier: "neutral", factory: () => mermaidErDiagram(), + factoryExport: "mermaidErDiagram", ejectable: ejectable("mermaid-er"), note: "neutral artifact owned by the docs engine (ADR-0020); surfaced via `meta docs`, not the recommended `meta gen` native suite.", }, @@ -301,6 +317,7 @@ export const generatorRegistry: Record = { description: "Per-entity callable/service surface wrapping the query helpers.", tier: "native", factory: () => callableFile(), + factoryExport: "callableFile", options: "filter?, target?", requires: ["entity"], runtimePeers: ["drizzle-orm"], @@ -314,6 +331,7 @@ export const generatorRegistry: Record = { description: "Per-template prompt-render helper over the render engine.", tier: "native", factory: () => promptRender(), + factoryExport: "promptRender", options: "filter?, target?", // ADR-0056: the handles take each payload value object's OWN interface, which entityFile() emits. requires: ["entity"], @@ -327,6 +345,7 @@ export const generatorRegistry: Record = { description: "Per-template response parser: a strict parse / safeParse for a reply that is bare JSON in the @responseRef shape, plus a tolerant extractLenientWithLoader(root, text) for a raw model reply (prose, code fences) that reports per field instead of throwing on a bad reply; it throws only when root does not declare the response value object.", tier: "native", factory: () => outputParser(), + factoryExport: "outputParser", options: "filter?, target?", // ADR-0056: the strict parse returns the response value object's OWN interface (entityFile()). requires: ["entity"], @@ -341,6 +360,7 @@ export const generatorRegistry: Record = { description: "Per-template typed extract helper: tolerant recovery of the typed response from dirty model text; throws only when a @required field is lost.", tier: "native", factory: () => extractor(), + factoryExport: "extractor", options: "filter?, target?", requires: ["entity", "output-parser"], runtimePackages: ["@metaobjectsdev/metadata", "@metaobjectsdev/render"], @@ -353,6 +373,7 @@ export const generatorRegistry: Record = { description: "Per-template output-format prompt fragment generator.", tier: "native", factory: () => outputPrompt(), + factoryExport: "outputPrompt", options: "filter?, target?", runtimePackages: ["@metaobjectsdev/render"], ejectable: ejectable("output-prompt"), @@ -364,6 +385,7 @@ export const generatorRegistry: Record = { description: "Per-template.output render helper (document/email typed wrappers).", tier: "native", factory: () => renderHelper(), + factoryExport: "renderHelper", options: "filter?, target?", // ADR-0056: the helper takes the payload value object's OWN interface (entityFile()). requires: ["entity"], @@ -378,6 +400,7 @@ export const generatorRegistry: Record = { description: "Generic Mustache template primitive (walk + template → files).", tier: "native", factory: () => templatePrimitive(), + factoryExport: "templateGenerator", options: "name, walk, template, format?, filter?, provider?, target?", ejectable: ejectable("template"), }, @@ -389,9 +412,18 @@ export const generatorRegistry: Record = { "Per-entity typed record/call trace helpers (extract + buildLlmCallRow + persist; LlmCallBase-derived entities only).", tier: "native", factory: () => traceHelperFile(), + factoryExport: "traceHelperFile", options: "outDir?, target?", // ADR-0056: typed by the request/response value objects' OWN interfaces (entityFile()). requires: ["entity"], + // What the emitted `.trace.ts` imports. `ai-runtime` is reached only by the + // `call` half, which every loadable prompt gets (a prompt requires @textRef). + runtimePackages: [ + "@metaobjectsdev/ai-runtime", + "@metaobjectsdev/metadata", + "@metaobjectsdev/render", + "@metaobjectsdev/runtime-ts", + ], ejectable: ejectable("trace-helper"), }, "requirement-tests": { @@ -401,6 +433,7 @@ export const generatorRegistry: Record = { description: "Per-requirement test stub, one per requirement.functional claim in the ledger.", tier: "native", factory: () => requirementTests(), + factoryExport: "requirementTests", options: "filter?, grain?, target?", ejectable: ejectable("requirement-tests"), }, @@ -411,6 +444,7 @@ export const generatorRegistry: Record = { description: "Cube data model files (model/cubes/*.yml, model/views/*.yml) for the reporting vocabulary: cubes, joins, dimensions, measures, segments, a rollup per served report and a view per served @spine report.", tier: "native", factory: () => cubeModel(), + factoryExport: "cubeModel", options: "dialect?, filter?, target?", // The emitted YAML imports nothing: Cube reads it from its own project. requires: [], @@ -430,6 +464,7 @@ export const generatorRegistry: Record = { // everything, so a placeholder here constructs without throwing — real use // always supplies both, same as `template`'s templatePrimitive() above). factory: () => sharedModelFile({ name: "shared-model", include: [] }), + factoryExport: "sharedModelFile", options: "name, include, exclude?, files?, version?, target?", ejectable: ejectable("shared-model"), }, diff --git a/server/typescript/packages/codegen-ts/src/generators/index.ts b/server/typescript/packages/codegen-ts/src/generators/index.ts index 0d468eb47..ce0a0d1ae 100644 --- a/server/typescript/packages/codegen-ts/src/generators/index.ts +++ b/server/typescript/packages/codegen-ts/src/generators/index.ts @@ -1,21 +1,26 @@ -// Built-in generator factories. +// Built-in generator factories — the public home of every stock generator. // -// ADR-0034 scaffold-and-own — `entityFile`, `queriesFile`, `routesFile` and `barrel` were -// exported here and DEPRECATED; 1.0 REMOVED them (ADR-0035 A3, `docs/1.0-readiness.md` G2, -// `docs/features/migrations/0.x-to-1.0.md` §11). Own a copy instead: `meta init` scaffolds -// them into `codegen/generators/*.ts` and `meta eject ` copies one at any time. They -// remain in this directory as the engine's internal composers and as the oracle the -// byte-identity gate holds each reference template to — they are no longer public API. +// Importing a factory from here runs the generator AS SHIPPED: nothing to copy, and an +// upgrade of the package is how its fixes arrive. `meta eject ` is the other path — +// it copies the reference template into `codegen/generators/` so the adopter can change +// what it emits (ADR-0034, Amendment 5). Both are supported; neither is deprecated. // -// The factories BELOW are not deprecated and this subpath is a supported public home for -// them. The prompt/output tier (`promptRender`, `outputParser`, `outputPrompt`, -// `extractor`, `renderHelper`) ALSO ships a reference template, so `meta eject ` -// can copy one to own (ADR-0034 Amendment 3); importing it from here is the un-owned -// path, and the CLI's prompt-gate warning names it. `traceHelperFile`, `callableFile` and -// the docs/primitive entries are package-only: no reference copy ships, so this subpath -// is their only home. `routesFileHono` / `namesFile` are wired directly or ejected. +// `entityFile`, `queriesFile`, `routesFile` and `barrel` were exported here, deprecated, +// and REMOVED at 1.0 so that an owned copy was the only path. 1.1 restored them: an +// adopter who changes nothing about a generator should not have to carry a copy of it +// that falls behind the reference. The byte-identity gate +// (`test/reference-byte-identical.test.ts`) holds each reference template to the factory +// of the same name exported here, so the two paths start from the same output. +// +// `traceHelperFile`, `callableFile` and the docs/primitive entries are package-only: no +// reference copy ships, so this subpath is their only home (`meta gen --list` marks them). // See spec/decisions/ADR-0034-codegen-scaffold-and-own.md. +export { entityFile, type EntityFileOpts } from "./entity-file.js"; +export { queriesFile, type QueriesFileOpts } from "./queries-file.js"; +export { routesFile, type RoutesFileOpts } from "./routes-file.js"; +export { barrel, type BarrelOpts } from "./barrel.js"; + export { callableFile, type CallableFileOpts } from "./callable-file.js"; export { routesFileHono, type RoutesFileHonoOpts } from "./routes-file-hono.js"; export { namesFile } from "./names-file.js"; diff --git a/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts b/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts index 5dad8dc5c..fee5ffb9e 100644 --- a/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts +++ b/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts @@ -1,82 +1,22 @@ // server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts // // Stock generator that emits one .trace.ts helper file for each concrete -// entity that (a) extends LlmCallBase (directly or transitively) and (b) nests a -// template.prompt with @payloadRef and/or @responseRef. -// -// The emitted helper exports an async function `record(om, responseMo, input)` -// that EXTRACTS the typed response VO itself and persists ONE row = base envelope + -// raw I/O (via buildLlmCallRow) PLUS the typed voRequest/voResponse columns. The -// helper is typed against the request and response value objects' OWN interfaces -// (ADR-0056 — imported from the modules entityFile() declares them in, never inlined), -// so call-sites get compile-time checks. -// -// NOTE: ObjectManager does not expose its loaded metadata root, so the caller must -// pass the resolved `responseMo: MetaObject` explicitly. The generated helper -// documents this in its JSDoc. +// entity that (a) extends a library's declared trace anchor (directly or transitively) +// and (b) nests a template.prompt with @payloadRef and @responseRef. The module body is +// `renderTraceHelper` (templates/trace-helper.ts); this file is the selection and the +// output path — the same split the reference template `meta eject trace-helper` copies. // // Consumer wiring (metaobjects.config.ts): // generators: [..., entityFile(), queriesFile(), traceHelperFile(), barrel()] +import type { MetaData } from "@metaobjectsdev/metadata"; +import { type Generator, type GeneratorFactory, perEntity } from "../generator.js"; import { - TYPE_OBJECT, - TYPE_TEMPLATE, - TEMPLATE_SUBTYPE_PROMPT, - TEMPLATE_ATTR_PAYLOAD_REF, - TEMPLATE_ATTR_RESPONSE_REF, - TEMPLATE_ATTR_FORMAT, - RESPONSE_FORMAT_XML, - TEMPLATE_ATTR_TEXT_REF, - resolveObjectRef, -} from "@metaobjectsdev/metadata"; -import { responseFormatOf } from "../templates/find-inbound.js"; -import type { MetaData, MetaObject } from "@metaobjectsdev/metadata"; -import { libraryManifests } from "@metaobjectsdev/metadata/library"; -import { - type EmittedFile, - type GenContext, - type Generator, - type GeneratorFactory, - perEntity, -} from "../generator.js"; -import { valueObjectImport, valueObjectImportLines } from "../templates/value-object-import.js"; -import { GENERATED_HEADER, GENERATED_EDIT_NOTE } from "../constants.js"; -import { tphDiscriminatorPin } from "../templates/zod-validators.js"; - -/** This generator's stable name — the key a library manifest declares its anchor under. */ -const STABLE_NAME = "trace-helper"; - -/** - * The FQNs this generator keys on, from the LIBRARY MANIFESTS rather than a constant - * here (FR-043 §6). - * - * What it replaces: `const LLM_CALL_BASE = "LlmCallBase"`, compared against `.name` - * anywhere in the super chain — so any adopter entity called `LlmCallBase`, in any - * package, triggered the generator. An anchor is a fully-qualified node the library - * declares, and it is resolved to a node and compared by identity below. - * - * `ctx.libraries` undefined means the caller never said which libraries are selected - * (a programmatic `runGen()`); every shipped manifest's anchor is then a candidate, - * which is still FQN-anchored. An EMPTY array is the opposite and is honoured: the - * caller looked, this project opted into none, and the generator matches nothing. - */ -function anchorFqns(ctx: GenContext): string[] { - const manifests = ctx.libraries ?? Object.values(libraryManifests()); - const out: string[] = []; - for (const manifest of manifests) { - for (const g of manifest.generators ?? []) { - if (g.name === STABLE_NAME && g.anchor !== undefined) out.push(g.anchor); - } - } - return out; -} - -/** Resolve each anchor FQN to the node it names, skipping any the model does not hold. */ -function anchorNodes(ctx: GenContext): MetaData[] { - const wanted = new Set(anchorFqns(ctx)); - if (wanted.size === 0) return []; - return ctx.loadedRoot.children().filter((n) => wanted.has(n.resolutionKey())); -} + TRACE_HELPER_STABLE_NAME, + extendsTraceAnchor, + renderTraceHelper, + traceAnchorNodes, +} from "../templates/trace-helper.js"; export interface TraceHelperOpts { /** Output directory prefix relative to the target's outDir. Default: "" (root). */ @@ -85,291 +25,21 @@ export interface TraceHelperOpts { target?: string; } -/** - * Walk the super chain looking for one of the anchor NODES. - * - * Node identity, not `.name` and not even the FQN string: the anchors were resolved - * against this run's own loaded root, so an entity whose chain reaches one reaches - * exactly the node the library declared. - */ -function extendsAnchor(obj: MetaObject, anchors: readonly MetaData[]): boolean { - if (anchors.length === 0) return false; - for (let cur = obj.superResolved; cur !== undefined; cur = cur.superResolved) { - if (anchors.includes(cur)) return true; - } - return false; -} - -/** Capitalise the first character. */ -function pascal(s: string): string { - return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s; -} - export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts): Generator { const dirPrefix = opts?.outDir ? `${opts.outDir.replace(/\/$/, "")}/` : ""; // Resolved once per run, on first use: `perEntity` calls back per entity and the // anchor set is a property of the run, not of the entity. let anchors: MetaData[] | undefined; const generator: Generator = { - name: STABLE_NAME, + name: TRACE_HELPER_STABLE_NAME, generate: perEntity((entity, ctx) => { - anchors ??= anchorNodes(ctx); + anchors ??= traceAnchorNodes(ctx); // Only concrete entities derived from a library's declared anchor. if (entity.isAbstract) return []; - if (!extendsAnchor(entity, anchors)) return []; - - // Find the nested template.prompt. - // ADR-0039: resolving — a concrete trace entity may inherit its - // template.prompt from an abstract base (it extendsBase); own-only would - // miss the inherited prompt. - const prompt = entity.children().find( - (c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT, - ); - if (prompt === undefined) return []; - - // ADR-0039: resolving — a prompt may inherit @payloadRef/@responseRef via extends. - const payloadRef = prompt.attr(TEMPLATE_ATTR_PAYLOAD_REF); - const responseRef = prompt.attr(TEMPLATE_ATTR_RESPONSE_REF); - - // @responseRef types the result; @payloadRef types the request. Both gate - // the helper: the entity must declare voRequest/voResponse field.object - // columns (authored) for these writes to land, and the prompt's refs name - // the VOs to render/extract into. - if (typeof responseRef !== "string") return []; - if (typeof payloadRef !== "string") return []; - - const entityName = entity.name; - const fnName = `record${pascal(entityName)}`; - const outPath = `${dirPrefix}${entityName}.trace.ts`; - - // ADR-0056: the request and response types are the value objects' OWN interfaces. ADR-0042: - // a bare ref resolves in the prompt's package first. - const promptPkg = prompt.package ?? prompt.fileDefaultPackage ?? ""; - const requestVo = resolveObjectRef(ctx.loadedRoot, payloadRef, promptPkg).node; - const responseVo = resolveObjectRef(ctx.loadedRoot, responseRef, promptPkg).node; - if (requestVo === undefined || responseVo === undefined) return []; - const extStyle = ctx.config.extStyle ?? "js"; - const requestImport = valueObjectImport(ctx.renderContext, requestVo, outPath, extStyle); - const responseImport = valueObjectImport(ctx.renderContext, responseVo, outPath, extStyle); - const requestType = requestImport.name; - const responseType = responseImport.name; - - // STI: a trace entity that is a TPH subtype stamps its declared - // discriminator value as callType and drops callType from the caller input - // (industry-standard STI — the discriminator is framework-managed). - const tphPin = tphDiscriminatorPin(entity); - const sti = tphPin !== undefined; - const callTypeValue = sti ? tphPin.value : entityName; - - // Emitted record fragments. The caller never supplies the derived - // status/errorDetail (the helper computes them from extraction), and an STI - // subtype additionally drops the framework-managed callType discriminator. - const recordInputOmit = sti - ? `"llmRequest" | "status" | "errorDetail" | "callType"` - : `"llmRequest" | "status" | "errorDetail"`; - // The object spread passed to buildLlmCallRow: STI subtypes stamp their - // discriminator value, all helpers fold in the derived status/errorDetail. - const recordBuildArg = sti - ? `{ ...input, callType: ${JSON.stringify(callTypeValue)}, status, errorDetail }` - : `{ ...input, status, errorDetail }`; - - // ADR-0053: the REPLY's syntax is @responseFormat, not @format. - // - // This site used to read @format twice — once as the reply's syntax and once - // as the prompt body's — under a comment calling them "two intentionally - // different shapes". They are two different FACTS, not two shapes of one: - // a plain-text prompt can elicit an XML reply, and the shipped docs-site - // fixture is exactly that. Reading @format here mis-parsed every prompt whose - // body format differed from its reply's. - // - // The same rule the output-parser / extractor / response-format-fragment - // generators use — all four now go through responseFormatOf(). - const formatLiteral = - responseFormatOf(prompt) === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON"; - - // A renderable prompt (carries @textRef) gets an additional call helper - // that renders the prompt text, calls the LLM, then parses + persists a trace row. - // ADR-0039: resolving — a prompt may inherit @textRef via extends. - const textRef = prompt.attr(TEMPLATE_ATTR_TEXT_REF); - const renderable = typeof textRef === "string"; - // render() takes the raw format string of the prompt BODY — @format, which is - // genuinely this attribute's job. Distinct from formatLiteral above, which is - // the REPLY's syntax and comes from @responseFormat (ADR-0053). - // ADR-0039: resolving — a prompt may inherit @format via extends. - const promptFormat = prompt.attr(TEMPLATE_ATTR_FORMAT); - const renderFormat = typeof promptFormat === "string" ? promptFormat : "text"; - - // Build the import block — all imports MUST stay at the top of the emitted file. - // `extract` + `render` both live in @metaobjectsdev/render: import them together - // (one de-duplicated statement) when the prompt is renderable, else import only - // `extract`. - const importLines: string[] = [ - `import type { ObjectManager } from "@metaobjectsdev/runtime-ts";`, - `import {`, - ` LlmCallDbRecorder,`, - ` buildLlmCallRow,`, - ` persistLlmCallRow,`, - ` extractSchemaFor,`, - ` Format,`, - ` type LlmCallInput,`, - ` type LlmCallRow,`, - `} from "@metaobjectsdev/runtime-ts";`, - renderable - ? `import { extract, render, type ExtractionReport, type Provider } from "@metaobjectsdev/render";` - : `import { extract, type ExtractionReport } from "@metaobjectsdev/render";`, - `import type { MetaObject } from "@metaobjectsdev/metadata";`, - ...valueObjectImportLines([requestImport, responseImport]), - ]; - if (renderable) { - importLines.push( - `import {`, - ` runLlmCall,`, - ` type RunLlmCallInput,`, - ` type RunLlmCallDeps,`, - ` type LlmClient,`, - ` type LlmRequest,`, - ` type CostFn,`, - ` type Clock,`, - ` type IdGen,`, - `} from "@metaobjectsdev/ai-runtime";`, - ); - } - - const lines: string[] = [ - `// ${GENERATED_HEADER} — ${GENERATED_EDIT_NOTE}`, - ``, - ...importLines, - ``, - `// ---- Typed result -----------------------------------------------------------`, - ``, - `export interface ${entityName}TraceResult {`, - ` status: "ok" | "error";`, - ` errorDetail: string | null;`, - ` /** Parsed response VO, or null when a required field was lost or malformed. */`, - ` /** Note: voResponse is the plain extracted record typed as the response shape (structural, not an instance). */`, - ` voResponse: ${responseType} | null;`, - `}`, - ``, - `/**`, - ` * The failure detail when a required response field is unusable — lost (absent) or`, - ` * malformed (present but not coercible) — else null. Either way its value is missing,`, - ` * so the response must not be handed back typed as \`${responseType}\`.`, - ` */`, - `function unusableRequired(report: ExtractionReport): string | null {`, - ` const parts: string[] = [];`, - ' if (report.hasLostRequired()) parts.push(`lost required: ${report.lostRequired().join(", ")}`);', - ' if (report.hasMalformedRequired()) parts.push(`malformed required: ${report.malformedRequired().join(", ")}`);', - ` return parts.length > 0 ? parts.join("; ") : null;`, - `}`, - ``, - `// ---- Record helper ----------------------------------------------------------`, - ``, - `/**`, - ` * Record a single ${entityName} LLM call: extract the response VO and persist a`, - ` * trace row via ObjectManager regardless of whether extraction succeeded.`, - ` *`, - ` * @param om - ObjectManager wired to the application database.`, - ` * @param responseMo - MetaObject for \`${responseRef}\` (resolve via the loaded`, - ` * metadata root: \`root.findObject("${responseRef}")\`).`, - ` * Passed explicitly because ObjectManager does not expose`, - ` * its loaded metadata root.`, - ` * @param input - LLM call inputs; type \`llmRequest\` as \`${requestType}\`.`, - ` * @param opts - Optional \`redact\` hook applied to the row before persist`, - ` * (scrub PII/secrets on the typed path, same as the generic`, - ` * recordLlmCall/callLlm helpers).`, - ` */`, - `export async function ${fnName}(`, - ` om: ObjectManager,`, - ` responseMo: MetaObject,`, - ` input: Omit & { llmRequest: ${requestType} },`, - ` opts?: { redact?: (row: LlmCallRow) => LlmCallRow },`, - `): Promise<${entityName}TraceResult> {`, - ` const schema = extractSchemaFor(responseMo, ${formatLiteral});`, - ` const outcome = extract(input.llmResponseText, schema);`, - ` const errorDetail = unusableRequired(outcome.report);`, - ` const failed = errorDetail !== null;`, - ` const status = failed ? ("error" as const) : ("ok" as const);`, - ` const base = buildLlmCallRow(${recordBuildArg});`, - ` const row = { ...base, voRequest: input.llmRequest, voResponse: failed ? null : outcome.data };`, - ` await persistLlmCallRow(new LlmCallDbRecorder(om, "${entityName}"), row, opts?.redact ? { redact: opts.redact } : undefined);`, - ` return { status, errorDetail, voResponse: failed ? null : (outcome.data as ${responseType}) };`, - `}`, - ``, - ]; - - if (renderable) { - const callFn = `call${pascal(entityName)}`; - lines.push( - ``, - `// ---- Call helper (GENERATE -> CALL -> record) -------------------------------`, - ``, - `export interface ${entityName}CallDeps {`, - ` om: ObjectManager;`, - ` responseMo: MetaObject;`, - ` client: LlmClient;`, - ` /** Prompt-TEXT resolver for render() (NOT the LLM client). */`, - ` provider: Provider;`, - ` model: string;`, - ` system?: string;`, - ` params?: Record;`, - ` cost?: CostFn;`, - ` clock?: Clock;`, - ` ids?: IdGen;`, - ` traceId?: string;`, - ` parentSpanId?: string;`, - ` sessionId?: string;`, - ` /** Optional row-redaction hook applied before persist (scrub PII/secrets). */`, - ` redact?: (row: LlmCallRow) => LlmCallRow;`, - `}`, - ``, - `/**`, - ` * Render the ${entityName} prompt, call the LLM, then parse + persist a trace`, - ` * row (finally-style: a call/parse failure still writes a row).`, - ` */`, - `export async function ${callFn}(`, - ` payload: ${requestType},`, - ` deps: ${entityName}CallDeps,`, - `): Promise<${entityName}TraceResult> {`, - ` const prompt = render({ ref: ${JSON.stringify(textRef)}, payload, format: ${JSON.stringify(renderFormat)}, provider: deps.provider });`, - // Build request/input/deps conditionally so that an absent optional (T | - // undefined) is never assigned to an optional T? property — required for - // exactOptionalPropertyTypes-strict consumer projects. - ` const request: LlmRequest = { prompt, model: deps.model };`, - ` if (deps.system !== undefined) request.system = deps.system;`, - ` if (deps.params !== undefined) request.params = deps.params;`, - ` const runInput: RunLlmCallInput = { callType: ${JSON.stringify(callTypeValue)}, request };`, - ` if (deps.traceId !== undefined) runInput.traceId = deps.traceId;`, - ` if (deps.parentSpanId !== undefined) runInput.parentSpanId = deps.parentSpanId;`, - ` if (deps.sessionId !== undefined) runInput.sessionId = deps.sessionId;`, - ` const runDeps: RunLlmCallDeps = { client: deps.client };`, - ` if (deps.cost !== undefined) runDeps.cost = deps.cost;`, - ` if (deps.clock !== undefined) runDeps.clock = deps.clock;`, - ` if (deps.ids !== undefined) runDeps.ids = deps.ids;`, - ` const { input: recInput, completion } = await runLlmCall(runInput, runDeps);`, - ` let voResponse: ${responseType} | null = null;`, - ` let status = recInput.status;`, - ` let errorDetail = recInput.errorDetail;`, - ` if (completion !== undefined) {`, - ` const outcome = extract(completion.body, extractSchemaFor(deps.responseMo, ${formatLiteral}));`, - ` const unusable = unusableRequired(outcome.report);`, - ` if (unusable !== null) {`, - ` status = "error";`, - ` errorDetail = unusable;`, - ` } else {`, - ` voResponse = outcome.data as ${responseType};`, - ` }`, - ` }`, - ` const row = { ...buildLlmCallRow({ ...recInput, status, errorDetail }), voRequest: payload, voResponse };`, - ` await persistLlmCallRow(new LlmCallDbRecorder(deps.om, ${JSON.stringify(entityName)}), row, deps.redact ? { redact: deps.redact } : undefined);`, - ` return { status, errorDetail, voResponse };`, - `}`, - ); - } - - return [{ - path: outPath, - content: lines.join("\n"), - }]; + if (!extendsTraceAnchor(entity, anchors)) return []; + const path = `${dirPrefix}${entity.name}.trace.ts`; + const content = renderTraceHelper(entity, ctx, path); + return content === undefined ? [] : [{ path, content }]; }), }; if (opts?.target) { diff --git a/server/typescript/packages/codegen-ts/src/index.ts b/server/typescript/packages/codegen-ts/src/index.ts index b8ac7c608..2e254f310 100644 --- a/server/typescript/packages/codegen-ts/src/index.ts +++ b/server/typescript/packages/codegen-ts/src/index.ts @@ -293,6 +293,10 @@ export { renderOutputParser } from "./templates/output-parser.js"; export { renderExtractor } from "./templates/extractor.js"; export { renderOutputPrompt } from "./templates/output-prompt.js"; export { renderRenderHelper } from "./templates/render-helper.js"; +// The same split for the two capability generators that carry no template of their own: +// `callable` (src/reference/callable.ts) and `trace-helper` (src/reference/trace-helper.ts). +export { renderCallableFile, isCallableEntity } from "./templates/callable-file.js"; +export { renderTraceHelper, traceAnchorNodes, extendsTraceAnchor } from "./templates/trace-helper.js"; // Template-driven codegen (rc.12). Factory + framework Provider for adopters // who want to wire their own templateGenerator instances. The default diff --git a/server/typescript/packages/codegen-ts/src/reference-templates.ts b/server/typescript/packages/codegen-ts/src/reference-templates.ts index 1cfaa7fff..d4c8d4611 100644 --- a/server/typescript/packages/codegen-ts/src/reference-templates.ts +++ b/server/typescript/packages/codegen-ts/src/reference-templates.ts @@ -25,6 +25,10 @@ export const REFERENCE_GENERATOR_NAMES = [ // The Cube exporter (FR-044 Plan 4): the owned part is which entities get a cube, the // file layout and the YAML call; `buildCubeModel` and `renderCubeYaml` stay in the package. "cube-model", + // The stored-procedure / table-function wrapper and the LLM-call trace helper: thin + // generators over `renderCallableFile` / `renderTraceHelper`, so the owned part is which + // objects get a module and where it lands. + "callable", "trace-helper", ] as const; export type ReferenceGeneratorName = (typeof REFERENCE_GENERATOR_NAMES)[number]; diff --git a/server/typescript/packages/codegen-ts/src/reference/callable.ts b/server/typescript/packages/codegen-ts/src/reference/callable.ts new file mode 100644 index 000000000..960182abe --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/reference/callable.ts @@ -0,0 +1,69 @@ +// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/callable.ts) and own it. +// Then import it LOCALLY in metaobjects.config.ts: +// import { callableFile } from "./codegen/generators/callable.js"; +// +// RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's +// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not +// reach for `Bun.*` globals here; they are undefined and take the whole run down with +// `Bun is not defined`. Use `node:` builtins instead. +// targets: Drizzle on Postgres. The emitted wrapper takes `db` as a PARAMETER and +// runs a parameterised `SELECT * FROM (…)` through `db.execute`. +// use-when: an object is backed by a stored procedure or a table function +// (`source.rdb` with `@kind: storedProc` or `tableFunction`) and you want a +// typed `call(db, args)` instead of hand-writing the SQL call. +// emits: /.callable.ts per object with a callable source — typed +// args from the source's `@parameterRef` value object (none for a +// zero-argument proc) and rows parsed by the entity module's Zod schema. +// customize: which objects get a wrapper (the filter below), the file name and where +// it lands. The module BODY comes from `renderCallableFile`; to change the +// emitted code itself, copy `renderCallableFile`'s source out of the package +// and call your copy here instead. +// composes-with: entity.ts (the wrapper imports the row schema and type it emits, and the +// `@parameterRef` value object's interface). +// +// Everything below imports ONLY from `@metaobjectsdev/codegen-ts` (the stable engine). + +import type { MetaObject } from "@metaobjectsdev/metadata"; +import { + perEntity, + type Generator, + type GeneratorFactory, + isCallableEntity, + renderCallableFile, + formatTs, + entityOutputPath, + effectivePackage, +} from "@metaobjectsdev/codegen-ts"; + +export interface CallableFileOpts { + filter?: (entity: MetaObject) => boolean; + target?: string; +} + +export const callableFile = function callableFile(opts?: CallableFileOpts): Generator { + const userFilter = opts?.filter; + // OWNED: a vanilla table or view never emits a wrapper. A filter of your own narrows + // further (it is ANDed), so it cannot make a non-callable object emit one. + const filter: (e: MetaObject) => boolean = userFilter + ? (e) => isCallableEntity(e) && userFilter(e) + : isCallableEntity; + + const generator: Generator = { + name: "callable-file", + filter, + generate: perEntity(async (entity, ctx) => { + return { + path: entityOutputPath( + ctx.config.outputLayout ?? "flat", + effectivePackage(entity), + `${entity.name}.callable.ts`, + ), + content: await formatTs(renderCallableFile(entity, ctx.renderContext)), + }; + }), + }; + if (opts?.target) { + generator.target = opts.target; + } + return generator; +} as GeneratorFactory; diff --git a/server/typescript/packages/codegen-ts/src/reference/trace-helper.ts b/server/typescript/packages/codegen-ts/src/reference/trace-helper.ts new file mode 100644 index 000000000..a09fd8067 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/reference/trace-helper.ts @@ -0,0 +1,68 @@ +// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/trace-helper.ts) and own it. +// Then import it LOCALLY in metaobjects.config.ts: +// import { traceHelperFile } from "./codegen/generators/trace-helper.js"; +// +// RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's +// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not +// reach for `Bun.*` globals here; they are undefined and take the whole run down with +// `Bun is not defined`. Use `node:` builtins instead. +// targets: the `ai` library's LLM-call trace. The emitted module calls +// `@metaobjectsdev/runtime-ts` (ObjectManager, the trace row builders), +// `@metaobjectsdev/render` (extract, render) and, for a renderable prompt, +// `@metaobjectsdev/ai-runtime` (runLlmCall). +// use-when: an entity extends a library's trace anchor (the `ai` library's +// `LlmCallBase`) and nests a `template.prompt` with @payloadRef and +// @responseRef, and you want a typed `record` — plus `call` +// when the prompt carries @textRef — instead of hand-wiring extract + +// buildLlmCallRow + persist at every call site. +// emits: /.trace.ts per concrete trace entity whose nested prompt +// names both value objects; nothing for an entity that does not. +// customize: which entities get a helper (the two gates below), the output directory +// (`outDir`) and the file name. The module BODY comes from +// `renderTraceHelper`; to change the emitted code itself, copy +// `renderTraceHelper`'s source out of the package and call your copy here +// instead. +// composes-with: entity.ts (the helper is typed by the request and response value +// objects' own interfaces, imported from the modules entity.ts emits). +// +// Everything below imports ONLY from `@metaobjectsdev/codegen-ts` (the stable engine). + +import type { MetaData } from "@metaobjectsdev/metadata"; +import { + perEntity, + type Generator, + type GeneratorFactory, + extendsTraceAnchor, + renderTraceHelper, + traceAnchorNodes, +} from "@metaobjectsdev/codegen-ts"; + +export interface TraceHelperOpts { + /** Output directory prefix relative to the target's outDir. Default: "" (root). */ + outDir?: string; + /** Optional named output target (registry key). Defaults to "default". */ + target?: string; +} + +export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts): Generator { + const dirPrefix = opts?.outDir ? `${opts.outDir.replace(/\/$/, "")}/` : ""; + // Resolved once per run, on first use: `perEntity` calls back per entity and the + // anchor set is a property of the run, not of the entity. + let anchors: MetaData[] | undefined; + const generator: Generator = { + name: "trace-helper", + generate: perEntity((entity, ctx) => { + anchors ??= traceAnchorNodes(ctx); + // OWNED: only concrete entities derived from a library's declared anchor. + if (entity.isAbstract) return []; + if (!extendsTraceAnchor(entity, anchors)) return []; + const path = `${dirPrefix}${entity.name}.trace.ts`; + const content = renderTraceHelper(entity, ctx, path); + return content === undefined ? [] : [{ path, content }]; + }), + }; + if (opts?.target) { + generator.target = opts.target; + } + return generator; +} as GeneratorFactory; diff --git a/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts b/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts new file mode 100644 index 000000000..7a2b05159 --- /dev/null +++ b/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts @@ -0,0 +1,358 @@ +// The `.trace.ts` composer behind the `trace-helper` generator. +// +// One module per concrete entity that (a) extends a library's declared trace anchor +// (directly or transitively) and (b) nests a template.prompt carrying @payloadRef and +// @responseRef. It exports `record(om, responseMo, input)`, which EXTRACTS the +// typed response VO itself and persists ONE row = base envelope + raw I/O (via +// buildLlmCallRow) PLUS the typed voRequest/voResponse columns, and for a renderable +// prompt (one carrying @textRef) a `call` that renders, calls and records. +// +// The helper is typed against the request and response value objects' OWN interfaces +// (ADR-0056 — imported from the modules entityFile() declares them in, never inlined), +// so call-sites get compile-time checks. +// +// NOTE: ObjectManager does not expose its loaded metadata root, so the caller must +// pass the resolved `responseMo: MetaObject` explicitly. The generated helper +// documents this in its JSDoc. +// +// Lives here, apart from the generator, so the built-in factory and the reference +// template `meta eject trace-helper` copies call ONE composer: an adopter owns which +// entities get a helper and where it lands, and the module body stays here. + +import { + TYPE_TEMPLATE, + TEMPLATE_SUBTYPE_PROMPT, + TEMPLATE_ATTR_PAYLOAD_REF, + TEMPLATE_ATTR_RESPONSE_REF, + TEMPLATE_ATTR_FORMAT, + RESPONSE_FORMAT_XML, + TEMPLATE_ATTR_TEXT_REF, + resolveObjectRef, +} from "@metaobjectsdev/metadata"; +import type { MetaData, MetaObject } from "@metaobjectsdev/metadata"; +import { libraryManifests } from "@metaobjectsdev/metadata/library"; +import type { GenContext } from "../generator.js"; +import { responseFormatOf } from "./find-inbound.js"; +import { valueObjectImport, valueObjectImportLines } from "./value-object-import.js"; +import { GENERATED_HEADER, GENERATED_EDIT_NOTE } from "../constants.js"; +import { tphDiscriminatorPin } from "./zod-validators.js"; + +/** The generator's stable name — the key a library manifest declares its anchor under. */ +export const TRACE_HELPER_STABLE_NAME = "trace-helper"; + +/** + * The FQNs the trace-helper generator keys on, from the LIBRARY MANIFESTS rather than a + * constant here (FR-043 §6). + * + * What it replaces: `const LLM_CALL_BASE = "LlmCallBase"`, compared against `.name` + * anywhere in the super chain — so any adopter entity called `LlmCallBase`, in any + * package, triggered the generator. An anchor is a fully-qualified node the library + * declares, and it is resolved to a node and compared by identity below. + * + * `ctx.libraries` undefined means the caller never said which libraries are selected + * (a programmatic `runGen()`); every shipped manifest's anchor is then a candidate, + * which is still FQN-anchored. An EMPTY array is the opposite and is honoured: the + * caller looked, this project opted into none, and the generator matches nothing. + */ +function anchorFqns(ctx: GenContext): string[] { + const manifests = ctx.libraries ?? Object.values(libraryManifests()); + const out: string[] = []; + for (const manifest of manifests) { + for (const g of manifest.generators ?? []) { + if (g.name === TRACE_HELPER_STABLE_NAME && g.anchor !== undefined) out.push(g.anchor); + } + } + return out; +} + +/** + * The trace anchors of this run, as NODES: each anchor FQN resolved against the loaded + * root, skipping any the model does not hold. A property of the run, not of an entity — + * resolve it once and pass it to {@link extendsTraceAnchor}. + */ +export function traceAnchorNodes(ctx: GenContext): MetaData[] { + const wanted = new Set(anchorFqns(ctx)); + if (wanted.size === 0) return []; + return ctx.loadedRoot.children().filter((n) => wanted.has(n.resolutionKey())); +} + +/** + * Walk the super chain looking for one of the anchor NODES. + * + * Node identity, not `.name` and not even the FQN string: the anchors were resolved + * against this run's own loaded root, so an entity whose chain reaches one reaches + * exactly the node the library declared. + */ +export function extendsTraceAnchor(obj: MetaObject, anchors: readonly MetaData[]): boolean { + if (anchors.length === 0) return false; + for (let cur = obj.superResolved; cur !== undefined; cur = cur.superResolved) { + if (anchors.includes(cur)) return true; + } + return false; +} + +/** Capitalise the first character. */ +function pascal(s: string): string { + return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s; +} + +/** + * Render the `.trace.ts` module for one trace entity, or `undefined` when the + * entity carries nothing to type a helper by: no nested template.prompt, a prompt + * missing @payloadRef or @responseRef, or a ref that resolves to no value object. + * + * `outPath` is the path the module will be written to, relative to its target's outDir; + * the value-object imports are spelled relative to it. Whether the entity is a trace + * entity at all (concrete, extends an anchor) is the CALLER's question — see + * {@link traceAnchorNodes} / {@link extendsTraceAnchor}. + */ +export function renderTraceHelper(entity: MetaObject, ctx: GenContext, outPath: string): string | undefined { + // Find the nested template.prompt. + // ADR-0039: resolving — a concrete trace entity may inherit its + // template.prompt from an abstract base (it extendsBase); own-only would + // miss the inherited prompt. + const prompt = entity.children().find( + (c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT, + ); + if (prompt === undefined) return undefined; + + // ADR-0039: resolving — a prompt may inherit @payloadRef/@responseRef via extends. + const payloadRef = prompt.attr(TEMPLATE_ATTR_PAYLOAD_REF); + const responseRef = prompt.attr(TEMPLATE_ATTR_RESPONSE_REF); + + // @responseRef types the result; @payloadRef types the request. Both gate + // the helper: the entity must declare voRequest/voResponse field.object + // columns (authored) for these writes to land, and the prompt's refs name + // the VOs to render/extract into. + if (typeof responseRef !== "string") return undefined; + if (typeof payloadRef !== "string") return undefined; + + const entityName = entity.name; + const fnName = `record${pascal(entityName)}`; + + // ADR-0056: the request and response types are the value objects' OWN interfaces. ADR-0042: + // a bare ref resolves in the prompt's package first. + const promptPkg = prompt.package ?? prompt.fileDefaultPackage ?? ""; + const requestVo = resolveObjectRef(ctx.loadedRoot, payloadRef, promptPkg).node; + const responseVo = resolveObjectRef(ctx.loadedRoot, responseRef, promptPkg).node; + if (requestVo === undefined || responseVo === undefined) return undefined; + const extStyle = ctx.config.extStyle ?? "js"; + const requestImport = valueObjectImport(ctx.renderContext, requestVo, outPath, extStyle); + const responseImport = valueObjectImport(ctx.renderContext, responseVo, outPath, extStyle); + const requestType = requestImport.name; + const responseType = responseImport.name; + + // STI: a trace entity that is a TPH subtype stamps its declared + // discriminator value as callType and drops callType from the caller input + // (industry-standard STI — the discriminator is framework-managed). + const tphPin = tphDiscriminatorPin(entity); + const sti = tphPin !== undefined; + const callTypeValue = sti ? tphPin.value : entityName; + + // Emitted record fragments. The caller never supplies the derived + // status/errorDetail (the helper computes them from extraction), and an STI + // subtype additionally drops the framework-managed callType discriminator. + const recordInputOmit = sti + ? `"llmRequest" | "status" | "errorDetail" | "callType"` + : `"llmRequest" | "status" | "errorDetail"`; + // The object spread passed to buildLlmCallRow: STI subtypes stamp their + // discriminator value, all helpers fold in the derived status/errorDetail. + const recordBuildArg = sti + ? `{ ...input, callType: ${JSON.stringify(callTypeValue)}, status, errorDetail }` + : `{ ...input, status, errorDetail }`; + + // ADR-0053: the REPLY's syntax is @responseFormat, not @format. + // + // This site used to read @format twice — once as the reply's syntax and once + // as the prompt body's — under a comment calling them "two intentionally + // different shapes". They are two different FACTS, not two shapes of one: + // a plain-text prompt can elicit an XML reply, and the shipped docs-site + // fixture is exactly that. Reading @format here mis-parsed every prompt whose + // body format differed from its reply's. + // + // The same rule the output-parser / extractor / response-format-fragment + // generators use — all four now go through responseFormatOf(). + const formatLiteral = + responseFormatOf(prompt) === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON"; + + // A renderable prompt (carries @textRef) gets an additional call helper + // that renders the prompt text, calls the LLM, then parses + persists a trace row. + // ADR-0039: resolving — a prompt may inherit @textRef via extends. + const textRef = prompt.attr(TEMPLATE_ATTR_TEXT_REF); + const renderable = typeof textRef === "string"; + // render() takes the raw format string of the prompt BODY — @format, which is + // genuinely this attribute's job. Distinct from formatLiteral above, which is + // the REPLY's syntax and comes from @responseFormat (ADR-0053). + // ADR-0039: resolving — a prompt may inherit @format via extends. + const promptFormat = prompt.attr(TEMPLATE_ATTR_FORMAT); + const renderFormat = typeof promptFormat === "string" ? promptFormat : "text"; + + // Build the import block — all imports MUST stay at the top of the emitted file. + // `extract` + `render` both live in @metaobjectsdev/render: import them together + // (one de-duplicated statement) when the prompt is renderable, else import only + // `extract`. + const importLines: string[] = [ + `import type { ObjectManager } from "@metaobjectsdev/runtime-ts";`, + `import {`, + ` LlmCallDbRecorder,`, + ` buildLlmCallRow,`, + ` persistLlmCallRow,`, + ` extractSchemaFor,`, + ` Format,`, + ` type LlmCallInput,`, + ` type LlmCallRow,`, + `} from "@metaobjectsdev/runtime-ts";`, + renderable + ? `import { extract, render, type ExtractionReport, type Provider } from "@metaobjectsdev/render";` + : `import { extract, type ExtractionReport } from "@metaobjectsdev/render";`, + `import type { MetaObject } from "@metaobjectsdev/metadata";`, + ...valueObjectImportLines([requestImport, responseImport]), + ]; + if (renderable) { + importLines.push( + `import {`, + ` runLlmCall,`, + ` type RunLlmCallInput,`, + ` type RunLlmCallDeps,`, + ` type LlmClient,`, + ` type LlmRequest,`, + ` type CostFn,`, + ` type Clock,`, + ` type IdGen,`, + `} from "@metaobjectsdev/ai-runtime";`, + ); + } + + const lines: string[] = [ + `// ${GENERATED_HEADER} — ${GENERATED_EDIT_NOTE}`, + ``, + ...importLines, + ``, + `// ---- Typed result -----------------------------------------------------------`, + ``, + `export interface ${entityName}TraceResult {`, + ` status: "ok" | "error";`, + ` errorDetail: string | null;`, + ` /** Parsed response VO, or null when a required field was lost or malformed. */`, + ` /** Note: voResponse is the plain extracted record typed as the response shape (structural, not an instance). */`, + ` voResponse: ${responseType} | null;`, + `}`, + ``, + `/**`, + ` * The failure detail when a required response field is unusable — lost (absent) or`, + ` * malformed (present but not coercible) — else null. Either way its value is missing,`, + ` * so the response must not be handed back typed as \`${responseType}\`.`, + ` */`, + `function unusableRequired(report: ExtractionReport): string | null {`, + ` const parts: string[] = [];`, + ' if (report.hasLostRequired()) parts.push(`lost required: ${report.lostRequired().join(", ")}`);', + ' if (report.hasMalformedRequired()) parts.push(`malformed required: ${report.malformedRequired().join(", ")}`);', + ` return parts.length > 0 ? parts.join("; ") : null;`, + `}`, + ``, + `// ---- Record helper ----------------------------------------------------------`, + ``, + `/**`, + ` * Record a single ${entityName} LLM call: extract the response VO and persist a`, + ` * trace row via ObjectManager regardless of whether extraction succeeded.`, + ` *`, + ` * @param om - ObjectManager wired to the application database.`, + ` * @param responseMo - MetaObject for \`${responseRef}\` (resolve via the loaded`, + ` * metadata root: \`root.findObject("${responseRef}")\`).`, + ` * Passed explicitly because ObjectManager does not expose`, + ` * its loaded metadata root.`, + ` * @param input - LLM call inputs; type \`llmRequest\` as \`${requestType}\`.`, + ` * @param opts - Optional \`redact\` hook applied to the row before persist`, + ` * (scrub PII/secrets on the typed path, same as the generic`, + ` * recordLlmCall/callLlm helpers).`, + ` */`, + `export async function ${fnName}(`, + ` om: ObjectManager,`, + ` responseMo: MetaObject,`, + ` input: Omit & { llmRequest: ${requestType} },`, + ` opts?: { redact?: (row: LlmCallRow) => LlmCallRow },`, + `): Promise<${entityName}TraceResult> {`, + ` const schema = extractSchemaFor(responseMo, ${formatLiteral});`, + ` const outcome = extract(input.llmResponseText, schema);`, + ` const errorDetail = unusableRequired(outcome.report);`, + ` const failed = errorDetail !== null;`, + ` const status = failed ? ("error" as const) : ("ok" as const);`, + ` const base = buildLlmCallRow(${recordBuildArg});`, + ` const row = { ...base, voRequest: input.llmRequest, voResponse: failed ? null : outcome.data };`, + ` await persistLlmCallRow(new LlmCallDbRecorder(om, "${entityName}"), row, opts?.redact ? { redact: opts.redact } : undefined);`, + ` return { status, errorDetail, voResponse: failed ? null : (outcome.data as ${responseType}) };`, + `}`, + ``, + ]; + + if (renderable) { + const callFn = `call${pascal(entityName)}`; + lines.push( + ``, + `// ---- Call helper (GENERATE -> CALL -> record) -------------------------------`, + ``, + `export interface ${entityName}CallDeps {`, + ` om: ObjectManager;`, + ` responseMo: MetaObject;`, + ` client: LlmClient;`, + ` /** Prompt-TEXT resolver for render() (NOT the LLM client). */`, + ` provider: Provider;`, + ` model: string;`, + ` system?: string;`, + ` params?: Record;`, + ` cost?: CostFn;`, + ` clock?: Clock;`, + ` ids?: IdGen;`, + ` traceId?: string;`, + ` parentSpanId?: string;`, + ` sessionId?: string;`, + ` /** Optional row-redaction hook applied before persist (scrub PII/secrets). */`, + ` redact?: (row: LlmCallRow) => LlmCallRow;`, + `}`, + ``, + `/**`, + ` * Render the ${entityName} prompt, call the LLM, then parse + persist a trace`, + ` * row (finally-style: a call/parse failure still writes a row).`, + ` */`, + `export async function ${callFn}(`, + ` payload: ${requestType},`, + ` deps: ${entityName}CallDeps,`, + `): Promise<${entityName}TraceResult> {`, + ` const prompt = render({ ref: ${JSON.stringify(textRef)}, payload, format: ${JSON.stringify(renderFormat)}, provider: deps.provider });`, + // Build request/input/deps conditionally so that an absent optional (T | + // undefined) is never assigned to an optional T? property — required for + // exactOptionalPropertyTypes-strict consumer projects. + ` const request: LlmRequest = { prompt, model: deps.model };`, + ` if (deps.system !== undefined) request.system = deps.system;`, + ` if (deps.params !== undefined) request.params = deps.params;`, + ` const runInput: RunLlmCallInput = { callType: ${JSON.stringify(callTypeValue)}, request };`, + ` if (deps.traceId !== undefined) runInput.traceId = deps.traceId;`, + ` if (deps.parentSpanId !== undefined) runInput.parentSpanId = deps.parentSpanId;`, + ` if (deps.sessionId !== undefined) runInput.sessionId = deps.sessionId;`, + ` const runDeps: RunLlmCallDeps = { client: deps.client };`, + ` if (deps.cost !== undefined) runDeps.cost = deps.cost;`, + ` if (deps.clock !== undefined) runDeps.clock = deps.clock;`, + ` if (deps.ids !== undefined) runDeps.ids = deps.ids;`, + ` const { input: recInput, completion } = await runLlmCall(runInput, runDeps);`, + ` let voResponse: ${responseType} | null = null;`, + ` let status = recInput.status;`, + ` let errorDetail = recInput.errorDetail;`, + ` if (completion !== undefined) {`, + ` const outcome = extract(completion.body, extractSchemaFor(deps.responseMo, ${formatLiteral}));`, + ` const unusable = unusableRequired(outcome.report);`, + ` if (unusable !== null) {`, + ` status = "error";`, + ` errorDetail = unusable;`, + ` } else {`, + ` voResponse = outcome.data as ${responseType};`, + ` }`, + ` }`, + ` const row = { ...buildLlmCallRow({ ...recInput, status, errorDetail }), voRequest: payload, voResponse };`, + ` await persistLlmCallRow(new LlmCallDbRecorder(deps.om, ${JSON.stringify(entityName)}), row, deps.redact ? { redact: deps.redact } : undefined);`, + ` return { status, errorDetail, voResponse };`, + `}`, + ); + } + + return lines.join("\n"); +} diff --git a/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts b/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts index 7f69fd65d..868a7bdca 100644 --- a/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts +++ b/server/typescript/packages/codegen-ts/test/reference-byte-identical.test.ts @@ -37,6 +37,11 @@ import { import { renderRequirementTest as builtinRenderRequirementTest } from "../src/templates/requirement-test.js"; import { cubeModel as builtinCubeModel } from "../src/generators/cube-model.js"; import { cubeModel as refCubeModel } from "../src/reference/cube-model.js"; +import { callableFile as builtinCallable } from "../src/generators/callable-file.js"; +import { callableFile as refCallable } from "../src/reference/callable.js"; +import { traceHelperFile as builtinTraceHelper } from "../src/generators/trace-helper-file.js"; +import { traceHelperFile as refTraceHelper } from "../src/reference/trace-helper.js"; +import { librarySources } from "@metaobjectsdev/metadata/library"; import { CANONICAL_CONFIG, CANONICAL_MODEL, @@ -136,6 +141,11 @@ const PAIRS: Record Generator; ref: () // vocabulary), so the pair is compared over two empty sets there; the describe at the end // of this file runs both over models that DO, under both Cube dialects. "cube-model": { builtin: builtinCubeModel, ref: refCubeModel }, + // Both emit nothing over the entity-shaped fixtures above (none declares a callable + // source or a trace entity); the describe at the end of this file runs each pair over + // a model that does. + callable: { builtin: builtinCallable, ref: refCallable }, + "trace-helper": { builtin: builtinTraceHelper, ref: refTraceHelper }, }; // The prompt tier emits nothing for the entity-shaped fixtures above — none declares a @@ -839,3 +849,128 @@ describe("ADR-0034 — the cube-model reference is byte-identical to the built-i expect(shape(refCubeModel(opts))).toEqual(builtin); }); }); + +// `callable` and `trace-helper` key on shapes no fixture above carries: a source that is a +// stored procedure or a table function, and an entity extending the `ai` library's trace +// anchor with a nested prompt. Without these models each pair would be compared over two +// empty sets, which is how a reference template ships having lost a branch. +const CALLABLE_MODEL = { + "metadata.root": { + package: "demo", + children: [ + { + "object.value": { + name: "PhaseSummaryArgs", + children: [ + { "field.int": { name: "caseId", "@required": true } }, + { "field.timestamp": { name: "asOfDate" } }, + ], + }, + }, + { + // A stored procedure taking typed arguments. + "object.projection": { + name: "PhaseSummary", + children: [ + { "source.rdb": { "@kind": "storedProc", "@proc": "fn_phase_summary", "@parameterRef": "PhaseSummaryArgs" } }, + { "field.long": { name: "phaseId" } }, + { "field.string": { name: "phaseName" } }, + ], + }, + }, + { + // A zero-argument table function: no `args` parameter, an empty call list. + "object.projection": { + name: "OpenCase", + children: [ + { "source.rdb": { "@kind": "tableFunction", "@function": "fn_open_cases" } }, + { "field.long": { name: "caseId" } }, + ], + }, + }, + { + // An ordinary table: must emit no wrapper on either side. + "object.entity": { + name: "Case", + children: [ + { "field.long": { name: "id" } }, + { "source.rdb": { "@table": "cases" } }, + { "identity.primary": { name: "pk", "@fields": ["id"] } }, + ], + }, + }, + ], + }, +}; + +/** One trace entity over the shipped `ai` anchor; `prompt` varies the branch under test. */ +function traceEntity(name: string, table: string, prompt: Record) { + return { + "object.entity": { + name, + extends: "metaobjects::ai::LlmCallBase", + children: [ + { "source.rdb": { "@table": table, "@role": "primary" } }, + { "identity.primary": { name: "id", "@fields": ["traceId"] } }, + { "template.prompt": { name: `${name}Prompt`, "@payloadRef": "AskVO", "@responseRef": "AnswerVO", ...prompt } }, + ], + }, + }; +} + +const TRACE_MODEL = { + "metadata.root": { + package: "app::ops", + children: [ + { "object.value": { name: "AskVO", children: [{ "field.string": { name: "question" } }] } }, + { "object.value": { name: "AnswerVO", children: [{ "field.string": { name: "answer" } }] } }, + // A JSON prompt body and a JSON reply. + traceEntity("ApiCall", "api_call", { "@textRef": "p/ask", "@format": "json" }), + // A plain-text body eliciting an XML reply: the other format literal, and the case + // where the body's @format and the reply's @responseFormat differ (ADR-0053). + traceEntity("AuditCall", "audit_call", { "@textRef": "p/audit", "@format": "text", "@responseFormat": "xml" }), + ], + }, +}; + +describe("ADR-0034 — the callable and trace-helper reference templates over models that carry them", () => { + async function compare( + root: Parameters[0]["metadata"], + names: readonly ReferenceGeneratorName[], + emitted: RegExp, + expected: number, + ) { + const aDir = mkdtempSync(join(tmpdir(), "codegen-builtin-")); + const bDir = mkdtempSync(join(tmpdir(), "codegen-reference-")); + try { + const a = await gen(aDir, names.map((n) => PAIRS[n].builtin()), root); + const b = await gen(bDir, names.map((n) => PAIRS[n].ref()), root); + const aKeys = Object.keys(a).sort(); + // Vacuity guard: exactly the modules the model asks for, no more and no fewer. + expect(aKeys.filter((k) => emitted.test(k)).length).toBe(expected); + expect(Object.keys(b).sort()).toEqual(aKeys); + for (const k of aKeys) expect(`${k}:\n${b[k]}`).toBe(`${k}:\n${a[k]}`); + } finally { + rmSync(aDir, { recursive: true, force: true }); + rmSync(bDir, { recursive: true, force: true }); + } + } + + test("callable — a stored procedure with arguments, a zero-argument table function, and a plain table", async () => { + const result = await new MetaDataLoader().load([ + new InMemoryStringSource(JSON.stringify(CALLABLE_MODEL), { id: "callable.json", format: "json" }), + ]); + expect(result.errors).toEqual([]); + await compare(result.root, ["entity", "callable"], /\.callable\.ts$/, 2); + }); + + test("trace-helper — a JSON reply and an XML one, over the shipped anchor", async () => { + const result = await new MetaDataLoader().load([ + ...librarySources(["ai"]), + new InMemoryStringSource(JSON.stringify(TRACE_MODEL), { id: "trace.json", format: "json" }), + ]); + expect(result.errors).toEqual([]); + await compare(result.root, ["entity", "trace-helper"], /\.trace\.ts$/, 2); + }); +}); + diff --git a/server/typescript/packages/codegen-ts/test/reference-templates.test.ts b/server/typescript/packages/codegen-ts/test/reference-templates.test.ts index 760b4a1e3..a1d44cf8c 100644 --- a/server/typescript/packages/codegen-ts/test/reference-templates.test.ts +++ b/server/typescript/packages/codegen-ts/test/reference-templates.test.ts @@ -15,7 +15,7 @@ describe("reference-templates reader", () => { expect([...REFERENCE_GENERATOR_NAMES]).toEqual([ "entity", "queries", "routes", "routes-hono", "barrel", "names", "prompt-render", "output-parser", "extractor", "output-prompt", "render-helper", - "requirement-tests", "cube-model", + "requirement-tests", "cube-model", "callable", "trace-helper", ]); }); @@ -31,7 +31,8 @@ describe("reference-templates reader", () => { const src = readReferenceTemplate(name); expect(src).toContain("REFERENCE TEMPLATE"); expect(src).toContain('from "@metaobjectsdev/codegen-ts"'); - // Never the deprecated package `/generators` export. + // Never the packaged factory it is a copy of — an owned generator that called + // the package's would not own what it emits. expect(src).not.toContain("@metaobjectsdev/codegen-ts/generators"); } }); diff --git a/server/typescript/packages/test-generators/README.md b/server/typescript/packages/test-generators/README.md index c36c250da..420b572e9 100644 --- a/server/typescript/packages/test-generators/README.md +++ b/server/typescript/packages/test-generators/README.md @@ -3,10 +3,13 @@ This repo's **owned copies** of the ADR-0034 reference generators — the same files `meta init` scaffolds into a consumer's `codegen/generators/`. -1.0 removes `entityFile` / `queriesFile` / `routesFile` / `barrel` from -`@metaobjectsdev/codegen-ts/generators` (readiness G2 / ADR-0035 A3). Suites that need a -generator set to drive `runGen` import them from here instead, which means they exercise -the artifact an adopter actually runs rather than an engine internal. +Suites that need a generator set to drive `runGen` import them from here, which means they +exercise the artifact an adopter who EJECTS actually runs. (This package dates from 1.0, +which removed `entityFile` / `queriesFile` / `routesFile` / `barrel` from +`@metaobjectsdev/codegen-ts/generators`; 1.1 restored that import, ADR-0034 Amendment 5. +The package stays, because the owned path is still a supported path and still needs +exercising. The imported path is gated by the byte-identity test in `codegen-ts` and by +`cli/test/integration/stock-generators-importable.test.ts`.) `test/owned-copies-current.test.ts` byte-compares each copy against `readReferenceTemplate(name)`, so a drifted copy fails loudly. Refresh with: diff --git a/server/typescript/packages/test-generators/src/index.ts b/server/typescript/packages/test-generators/src/index.ts index dce258fe0..579d734b4 100644 --- a/server/typescript/packages/test-generators/src/index.ts +++ b/server/typescript/packages/test-generators/src/index.ts @@ -1,9 +1,10 @@ -// The repo's OWN copies of the ADR-0034 reference templates — the same move `meta init` -// scaffolds into a consumer's `codegen/generators/*.ts`. They exist because 1.0 removes +// The repo's OWN copies of the ADR-0034 reference templates — what `meta eject` writes +// into a consumer's `codegen/generators/*.ts`. They date from 1.0, which removed // `entityFile` / `queriesFile` / `routesFile` / `barrel` from -// `@metaobjectsdev/codegen-ts/generators` (readiness G2, ADR-0035 A3), and the suites that -// drive `runGen` need a generator set. Taking the OWNED copies rather than reaching for an -// engine internal means this repo's tests exercise the artifact an adopter actually runs. +// `@metaobjectsdev/codegen-ts/generators`; 1.1 restored that import (ADR-0034 Amendment 5), +// and these stay because the owned path is still supported and still needs exercising. +// Taking the OWNED copies means this repo's suites run the artifact an adopter who ejects +// actually runs. // // The copies are byte-gated against `readReferenceTemplate()` by `test/owned-copies-current.test.ts`, // so they cannot drift from the templates `meta init` ships. Refresh with: diff --git a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md index bccaa235e..5722e6c1b 100644 --- a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md +++ b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md @@ -347,6 +347,76 @@ the guide says to name an instance or a factory function. Guide: `docs/recipes/write-your-own-generator.md`. +### Amendment 5 (2026-10-11) — the stock generators are importable again; eject is how you customize + +**Ruling (maintainer).** Asked whether adopters may import the stock entity, queries and +barrel generators again instead of having to own copies: *"Yes, everything should be +ejectable."* Two things follow, and both are the rule from here on: + +- **Every stock generator can be run as shipped**, by importing its factory from its + package. Nothing is copied, and an upgrade of the package is how its fixes arrive. +- **Every stock generator can be ejected**, when the adopter wants to change what it emits. + Ejecting is the customization route; it is not a precondition for generating. + +**Supersedes** Decision 3 ("you cannot `import { entityFile } from +"@metaobjectsdev/codegen-ts/generators"`") and the clause of Decision 2 that makes +scaffold-and-own "the default and only taught path". Decisions 1, 4 and 5 and Amendments 1 +to 4 stand. + +**Why.** An assessment of an adopter estate found its owned copies of these generators were +almost pure duplication of the reference: a few lines of the adopter's own, and the copies +had already fallen behind the reference they were taken from. Decision 3 removed the +import so that there would be "no black-box to fight". What it also removed was the option +of not fighting at all. An adopter who needed nothing changed still had to carry a copy, +and a copy does not receive fixes: this ADR's own Consequences list "owned forks don't +auto-receive fixes" as a cost that diff and update "mitigate, not eliminate". + +**What Decision 3 was solving, and why it stays solved.** The failure it addressed was an +adopter discovering mid-build that changing generated code meant forking the package or +fighting the three-way merge. `meta eject` (Amendment 1) did not exist when Decision 3 was +written. It now copies any reference at any time, with its helper runtime (Amendment 3, +2026-09-24), and the catalog names it beside every generator. The exit is one command away +whichever path a project started on, so restoring the import does not bring the old +failure back. + +**What changed with this amendment.** + +1. `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from + `@metaobjectsdev/codegen-ts/generators` again. The config loader's two diagnostics for + the removed imports went with the removal. +2. The catalog says how to import. Each registry entry carries `factoryExport`, and + `meta gen --list` prints the import line that runs the generator as shipped (`wire` on + the JSON row). It is resolved, not trusted: a gate imports each package's public factory + module and requires that name, and requires the reference template of an ejectable + generator to export the same symbol, so moving between the two paths is one import line. +3. `callable` and `trace-helper` gained reference templates and left the package-only list. +4. `meta init` is unchanged in what it wires, which is nothing (Amendment 2 stands). Its + scaffolded comment now shows the import first and eject as the way to change output. +5. The order of guidance in Amendment 4 gains a first step: **import a stock generator + when it already emits what you want; write the generator you need when nothing does; + eject a reference when one is close; hand-write only what the model cannot express.** + +**What does not change.** + +- **The core and helper line of Amendment 3.** An imported generator is still a reference + helper. Its output is not covered by `docs/compatibility-policy.md` and may change in a + MINOR; after an upgrade `meta verify --codegen` reports the difference as drift until the + project regenerates. That is the trade for receiving fixes without a merge. An adopter + who needs output to hold still across upgrades ejects. +- **Existing owned copies.** They keep running exactly as before, and `meta eject --list` + keeps marking each one identical to, or differing from, the installed reference. +- **The byte-identity gate.** It holds every reference template to the factory of the same + name, so the two paths start from the same output. The one difference is where the + HTTP-adapter import points, because eject hands over that source too (Amendment 3). + +**Still not ejectable, with the reason for each.** In TypeScript: `template`, which is the +Mustache primitive and has no emit logic of its own (the C#, Java and Python ports rule the +same way); `shared-model`, whose output is a hash-pinned artifact that a consumer's +`meta deps sync` verifies, so it is a contract rather than a helper; and `docs`, `api-docs` +and `mermaid-er`, which are the engine behind `meta docs` and have no seam to run an owned +copy through. Outside TypeScript, the Java `entity` and `extractor` generators became +ejectable in Amendment 6. Every port's generators run as shipped, selected by name. + ### Amendment 6 (2026-10-11) — the Java `entity` and `extractor` generators are ejectable **Ruling (maintainer).** Asked whether adopters may import the stock generators again: diff --git a/spec/roadmap.md b/spec/roadmap.md index 1ddba2923..273e9b17f 100644 --- a/spec/roadmap.md +++ b/spec/roadmap.md @@ -198,7 +198,8 @@ _(FR-032 was developed under the working number "FR-026" — see commit history; `routesFile`/`barrel` exports from `@metaobjectsdev/codegen-ts/generators` — scoped by NAME, not by path, because that subpath carries ~20 exports and only those four were deprecated; the owned copies written by `meta init` / `meta eject` are the path now - (ADR-0034). FR-024's declared-API surface was ratified as DEFERRED rather than rushed in, + (ADR-0034). (1.1 restored the four exports: import to run a generator as shipped, eject + to change it. ADR-0034 Amendment 5.) FR-024's declared-API surface was ratified as DEFERRED rather than rushed in, which is what removed the last candidate for a breaking round. - **Standing caveat on the evidence.** All nine estates are maintainer-owned, which the G3d From 341d2facacff33506f4faa8ea858f592cb1af435 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 08:38:04 -0400 Subject: [PATCH 02/10] refactor(cli,codegen-ts): simplifier pass over the stock-generator change - One CORE_CODEGEN_PACKAGE definition in the CLI; each catalog slice declares the module it serves factories from, so factoryModuleOf is a lookup. - trace-helper uses the shared toPascalCase instead of a local copy (same body, output unchanged). - The package-only footer names meta docs as the docs layer's door. - A gate asserts the package wiring and the eject wiring print the same call for every ejectable generator. - Header comments corrected where they contradicted the change. Verification: scripts/ci-local.sh --quick passed locally on the feature commit (36 steps: hygiene and drift gates, ts build + typecheck, TypeScript conformance, ts unit suites). The codegen-ts and cli suites were rerun after this pass and are green. The hosted ci step is skipped for this run, so that local run is the CI evidence. The C#, Java, Kotlin and Python lanes were not run; no file under those ports changed. --- .../packages/cli/src/commands/eject.ts | 4 +- .../packages/cli/src/lib/catalog-listing.ts | 9 +++- .../packages/cli/src/lib/catalog.ts | 46 +++++++++++-------- .../cli/test/catalog-factory-exports.test.ts | 9 ++++ .../codegen-ts/src/generator-registry.ts | 7 ++- .../codegen-ts/src/generators/index.ts | 5 +- .../src/generators/trace-helper-file.ts | 10 ++-- .../packages/codegen-ts/src/index.ts | 6 ++- .../codegen-ts/src/templates/trace-helper.ts | 10 ++-- 9 files changed, 65 insertions(+), 41 deletions(-) diff --git a/server/typescript/packages/cli/src/commands/eject.ts b/server/typescript/packages/cli/src/commands/eject.ts index 892b80751..5e85484bd 100644 --- a/server/typescript/packages/cli/src/commands/eject.ts +++ b/server/typescript/packages/cli/src/commands/eject.ts @@ -24,7 +24,7 @@ import { parseEjectArgs } from "../lib/args.js"; import { log } from "../lib/log.js"; import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js"; import { compareOwnedCopy, type OwnedComparison } from "../lib/owned-copy.js"; -import { composeCatalog, factoryModuleOf, packageOf } from "../lib/catalog.js"; +import { composeCatalog, CORE_CODEGEN_PACKAGE, factoryModuleOf, packageOf } from "../lib/catalog.js"; import { installSetFor, type InstallSet, type OwnedRuntimeInstall } from "../lib/install-set.js"; import { ejectRuntime, ejectsRuntime, runtimeCopyStatus, runtimeDiffCommand, runtimeInstall, @@ -61,8 +61,6 @@ interface TemplateSource { // runtime by selecting this entry via `names.includes(name)`. Reading from the root // deletes that machinery without weakening anything: membership is still checked, once, // where the untrusted value enters. -const CORE_CODEGEN_PACKAGE = "@metaobjectsdev/codegen-ts"; - const SOURCES: TemplateSource[] = [ { packageName: CORE_CODEGEN_PACKAGE, diff --git a/server/typescript/packages/cli/src/lib/catalog-listing.ts b/server/typescript/packages/cli/src/lib/catalog-listing.ts index 725d411d1..239a01ba3 100644 --- a/server/typescript/packages/cli/src/lib/catalog-listing.ts +++ b/server/typescript/packages/cli/src/lib/catalog-listing.ts @@ -296,6 +296,10 @@ export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promis ? installSetFor([entry]) : installSetFor([entry], [], { generators: new Set([entry.name]), ...runtimeInstall(owned.packages) }); const pkg = packageOf(entry.name) ?? ""; + // The row keeps the stock wiring even when this project owns the copy: the JSON + // consumer reads `source.owned` to know what it runs today, and the package line is + // the fact it needs to go back to stock. `renderCatalogText` deliberately hides that + // line under an owned row — two imports in front of a human invite wiring both. const wire = packagedWiring(entry); const row: GeneratorCatalogRow = { @@ -450,8 +454,9 @@ export const REFERENCE_HELPER_NOTE: readonly string[] = [ /** The footer line explaining the `package-only` row mark. */ export const PACKAGE_ONLY_NOTE = - "package-only = no reference template ships for it, so `meta eject` cannot copy it; " + - "import it from its package instead."; + "package-only = no reference template ships for it, so `meta eject` cannot copy it. " + + "Run one as shipped by importing its factory — the import sits under its row, except " + + "for the docs layer, whose door is `meta docs`."; /** The human rendering: grouped by layer, because layer is the axis you select by. */ export function renderCatalogText(rows: CatalogRow[], probed: boolean): string { diff --git a/server/typescript/packages/cli/src/lib/catalog.ts b/server/typescript/packages/cli/src/lib/catalog.ts index b11f8f966..35b685d65 100644 --- a/server/typescript/packages/cli/src/lib/catalog.ts +++ b/server/typescript/packages/cli/src/lib/catalog.ts @@ -21,11 +21,24 @@ import { import { reactGeneratorRegistry } from "@metaobjectsdev/codegen-ts-react"; import { tanstackGeneratorRegistry } from "@metaobjectsdev/codegen-ts-tanstack"; -/** The slices, in install-boundary order: the engine, then its framework packages. */ -const SLICES: ReadonlyArray]> = [ - ["@metaobjectsdev/codegen-ts", generatorRegistry], - ["@metaobjectsdev/codegen-ts-react", reactGeneratorRegistry], - ["@metaobjectsdev/codegen-ts-tanstack", tanstackGeneratorRegistry], +/** + * The engine package. Its factories are served from a `/generators` subpath rather than + * its root (the root is the engine plus the primitives an owned generator composes); the + * framework packages serve theirs from the root. Exported because `eject.ts`'s source + * table names the same package — one definition per CLI package, not two. + */ +export const CORE_CODEGEN_PACKAGE = "@metaobjectsdev/codegen-ts"; + +/** + * The slices, in install-boundary order: the engine, then its framework packages. The + * third element is the module that package serves factories from — a per-package fact + * declared at registration, so {@link factoryModuleOf} is a lookup and a new slice + * carries its own fact instead of adding a case to a name-keyed branch. + */ +const SLICES: ReadonlyArray, string]> = [ + [CORE_CODEGEN_PACKAGE, generatorRegistry, `${CORE_CODEGEN_PACKAGE}/generators`], + ["@metaobjectsdev/codegen-ts-react", reactGeneratorRegistry, "@metaobjectsdev/codegen-ts-react"], + ["@metaobjectsdev/codegen-ts-tanstack", tanstackGeneratorRegistry, "@metaobjectsdev/codegen-ts-tanstack"], ]; /** @@ -44,21 +57,12 @@ export function catalogPackages(): string[] { return SLICES.map(([pkg]) => pkg); } - -/** The engine package. Its factories live on a `/generators` subpath, not at the root. */ -const CORE_CODEGEN_PACKAGE = "@metaobjectsdev/codegen-ts"; - /** - * The module a catalog entry's factory is imported from to run it as shipped. - * - * The engine serves its factories from `/generators` (its root is the engine and the - * primitives an owned generator composes); the framework packages serve theirs from the - * root. Undefined for a name no slice registers. + * The module a catalog entry's factory is imported from to run it as shipped — the third + * element of the slice that registers the name. Undefined for a name no slice registers. */ export function factoryModuleOf(name: string): string | undefined { - const pkg = packageOf(name); - if (pkg === undefined) return undefined; - return pkg === CORE_CODEGEN_PACKAGE ? `${pkg}/generators` : pkg; + return SLICES.find(([, slice]) => name in slice)?.[2]; } /** How one generator reaches `generators: [...]` — the import line and the entry. */ @@ -69,7 +73,11 @@ export interface GeneratorWiring { /** * The option names an entry's `options` summary lists WITHOUT a trailing `?` — the ones - * its factory cannot be called without (`template`, `shared-model`). + * its factory cannot be called without. `options` is the option surface's only + * declaration and this is its only reader: `--list` prints no option list of its own, so + * the string IS the wiring data, not a caption beside it. The `?`-suffix convention is + * documented on the field in `generator-registry.ts`, and + * `catalog-factory-exports.test.ts` pins the two call sites that depend on it. */ function requiredOptionNames(entry: GeneratorRegistryEntry): string[] { return (entry.options ?? "") @@ -111,7 +119,7 @@ export function packagedWiring(entry: GeneratorRegistryEntry): GeneratorWiring | * another. */ export function composeCatalog(): Record { - return composeSlices(SLICES); + return composeSlices(SLICES.map(([pkg, slice]) => [pkg, slice] as const)); } /** diff --git a/server/typescript/packages/cli/test/catalog-factory-exports.test.ts b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts index c22543c4a..233760bb2 100644 --- a/server/typescript/packages/cli/test/catalog-factory-exports.test.ts +++ b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts @@ -87,6 +87,15 @@ describe("catalog factory exports", () => { if (ejected.exportName !== entry.factoryExport) { differing.push(`${entry.name}: owned copy exports ${ejected.exportName}, package exports ${entry.factoryExport}`); } + // One symbol is only half the claim: the two paths must print the SAME call. + // `packagedWiring` spells required options off the entry's `options` string, + // while the eject line is built bare — so equality here also holds the rule + // that no ejectable entry has a required option. + const packaged = packagedWiring(entry); + const ejectEntry = `${ejected.exportName}()`; + if (packaged !== undefined && packaged.entry !== ejectEntry) { + differing.push(`${entry.name}: package prints "${packaged.entry}", eject prints "${ejectEntry}"`); + } } expect(differing).toEqual([]); } finally { diff --git a/server/typescript/packages/codegen-ts/src/generator-registry.ts b/server/typescript/packages/codegen-ts/src/generator-registry.ts index a0bbb4ea2..62b016da3 100644 --- a/server/typescript/packages/codegen-ts/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts/src/generator-registry.ts @@ -112,7 +112,12 @@ export interface GeneratorRegistryEntry { * name whose generator carries the same implementation name as {@link factory}'s. */ factoryExport: string; - /** Optional one-line options summary for `--list`. */ + /** + * The factory's option surface: comma-separated option names, each carrying a trailing + * `?` when optional. Nothing renders this string — the CLI's `packagedWiring` derives + * the `generators: [...]` call advice from it, so rewording it is a wiring change, not + * a copy edit. + */ options?: string; /** The framework this generator's OUTPUT targets. Absent = framework-neutral. */ framework?: GeneratorFramework; diff --git a/server/typescript/packages/codegen-ts/src/generators/index.ts b/server/typescript/packages/codegen-ts/src/generators/index.ts index ce0a0d1ae..add750044 100644 --- a/server/typescript/packages/codegen-ts/src/generators/index.ts +++ b/server/typescript/packages/codegen-ts/src/generators/index.ts @@ -12,8 +12,9 @@ // (`test/reference-byte-identical.test.ts`) holds each reference template to the factory // of the same name exported here, so the two paths start from the same output. // -// `traceHelperFile`, `callableFile` and the docs/primitive entries are package-only: no -// reference copy ships, so this subpath is their only home (`meta gen --list` marks them). +// Which entries have a reference copy is declared once, in `REFERENCE_GENERATOR_NAMES` +// (reference-templates.ts); `meta gen --list` marks every other entry `package-only` — +// today that is `template`, `shared-model` and the docs tier. // See spec/decisions/ADR-0034-codegen-scaffold-and-own.md. export { entityFile, type EntityFileOpts } from "./entity-file.js"; diff --git a/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts b/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts index fee5ffb9e..91f9ecebf 100644 --- a/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts +++ b/server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts @@ -1,10 +1,10 @@ // server/typescript/packages/codegen-ts/src/generators/trace-helper-file.ts // -// Stock generator that emits one .trace.ts helper file for each concrete -// entity that (a) extends a library's declared trace anchor (directly or transitively) -// and (b) nests a template.prompt with @payloadRef and @responseRef. The module body is -// `renderTraceHelper` (templates/trace-helper.ts); this file is the selection and the -// output path — the same split the reference template `meta eject trace-helper` copies. +// Stock generator that emits one .trace.ts helper per trace entity. This file is +// the selection and the output path; what a trace entity IS, and the module body, live in +// templates/trace-helper.ts (anchor walk: `traceAnchorNodes` / `extendsTraceAnchor`; +// prompt gating and rendering: `renderTraceHelper`) — the same split the reference +// template `meta eject trace-helper` copies. // // Consumer wiring (metaobjects.config.ts): // generators: [..., entityFile(), queriesFile(), traceHelperFile(), barrel()] diff --git a/server/typescript/packages/codegen-ts/src/index.ts b/server/typescript/packages/codegen-ts/src/index.ts index 2e254f310..2de57c3c4 100644 --- a/server/typescript/packages/codegen-ts/src/index.ts +++ b/server/typescript/packages/codegen-ts/src/index.ts @@ -293,8 +293,10 @@ export { renderOutputParser } from "./templates/output-parser.js"; export { renderExtractor } from "./templates/extractor.js"; export { renderOutputPrompt } from "./templates/output-prompt.js"; export { renderRenderHelper } from "./templates/render-helper.js"; -// The same split for the two capability generators that carry no template of their own: -// `callable` (src/reference/callable.ts) and `trace-helper` (src/reference/trace-helper.ts). +// The same split for the two capability generators that gained reference templates in +// 1.1: an owned copy of `callable` (src/reference/callable.ts) or `trace-helper` +// (src/reference/trace-helper.ts) chooses which files to emit and where; the module body +// stays in these composers. export { renderCallableFile, isCallableEntity } from "./templates/callable-file.js"; export { renderTraceHelper, traceAnchorNodes, extendsTraceAnchor } from "./templates/trace-helper.js"; diff --git a/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts b/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts index 7a2b05159..c4e800c93 100644 --- a/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts +++ b/server/typescript/packages/codegen-ts/src/templates/trace-helper.ts @@ -35,6 +35,7 @@ import type { GenContext } from "../generator.js"; import { responseFormatOf } from "./find-inbound.js"; import { valueObjectImport, valueObjectImportLines } from "./value-object-import.js"; import { GENERATED_HEADER, GENERATED_EDIT_NOTE } from "../constants.js"; +import { toPascalCase } from "../naming.js"; import { tphDiscriminatorPin } from "./zod-validators.js"; /** The generator's stable name — the key a library manifest declares its anchor under. */ @@ -91,11 +92,6 @@ export function extendsTraceAnchor(obj: MetaObject, anchors: readonly MetaData[] return false; } -/** Capitalise the first character. */ -function pascal(s: string): string { - return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s; -} - /** * Render the `.trace.ts` module for one trace entity, or `undefined` when the * entity carries nothing to type a helper by: no nested template.prompt, a prompt @@ -128,7 +124,7 @@ export function renderTraceHelper(entity: MetaObject, ctx: GenContext, outPath: if (typeof payloadRef !== "string") return undefined; const entityName = entity.name; - const fnName = `record${pascal(entityName)}`; + const fnName = `record${toPascalCase(entityName)}`; // ADR-0056: the request and response types are the value objects' OWN interfaces. ADR-0042: // a bare ref resolves in the prompt's package first. @@ -286,7 +282,7 @@ export function renderTraceHelper(entity: MetaObject, ctx: GenContext, outPath: ]; if (renderable) { - const callFn = `call${pascal(entityName)}`; + const callFn = `call${toPascalCase(entityName)}`; lines.push( ``, `// ---- Call helper (GENERATE -> CALL -> record) -------------------------------`, From 0d0801201e8e4df99dbd784ae1769d59fc4844c1 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 08:51:55 -0400 Subject: [PATCH 03/10] no-mistakes(review): Drop derived wire entry; align eject wording and docs Assisted-by: no-mistakes:claude:claude-opus-5-5 --- CHANGELOG.md | 11 ++++--- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../references/typescript.md | 6 ++-- docs/features/own-your-codegen.md | 6 ++-- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../references/typescript.md | 6 ++-- .../skills/metaobjects-codegen/SKILL.md | 5 +-- .../references/owning-generators.md | 2 +- .../references/typescript.md | 6 ++-- .../packages/cli/src/commands/gen.ts | 8 ++--- .../packages/cli/src/commands/init.ts | 11 ++++--- server/typescript/packages/cli/src/index.ts | 11 ++++--- .../packages/cli/src/lib/catalog-listing.ts | 6 ++-- .../packages/cli/src/lib/catalog.ts | 33 ++++--------------- .../cli/test/catalog-factory-exports.test.ts | 23 ++----------- .../packages/codegen-ts/src/catalog-gates.ts | 3 +- .../codegen-ts/src/generator-registry.ts | 7 +--- .../ADR-0034-codegen-scaffold-and-own.md | 29 ++++++++-------- 26 files changed, 94 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bf6b4313..118c82319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,15 +35,18 @@ here.** until you regenerate. Own the copy if output has to hold still across upgrades. - **`meta gen --list` prints how to import each generator.** Every row outside the `docs` layer shows the import line that runs the generator as shipped, and `--format json` - carries it as `wire.import` and `wire.entry`. A gate imports each package's factory module - and requires the name the catalog prints, and requires an ejectable generator's reference + carries it as `wire.import`. The catalog prints the import only: each generator's + documented options say how to call it. A gate imports each package's factory module and + requires the name the catalog prints, and requires an ejectable generator's reference template to export the same symbol. - **`callable` and `trace-helper` can be ejected (TypeScript).** Both gained a reference template, so `meta eject callable` and `meta eject trace-helper` copy a generator you own: which objects get a module and where it lands. The module body stays in the package (`renderCallableFile`, `renderTraceHelper`, now exported from `@metaobjectsdev/codegen-ts`), - the same split the prompt tier uses. Still package-only: the `template` primitive, the - docs tier (`docs`, `api-docs`, `mermaid-er`) and `shared-model`. + the same split the prompt tier uses. The rule is that every stock generator can be + ejected except the `template` primitive, which has no emit logic of its own to own. Not + yet ejectable in this change: `shared-model` and the docs tier (`docs`, `api-docs`, + `mermaid-er`) in TypeScript, and `entity` and `extractor` in Java. ### Changed diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/agent-context/skills/metaobjects-codegen/references/typescript.md b/agent-context/skills/metaobjects-codegen/references/typescript.md index 62e7aa00c..f904e4313 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript.md @@ -127,8 +127,10 @@ name. Import by default and eject to customize: an owned copy you never change o behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the only path for those four.) The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in -`meta gen --list`: the `template` primitive and `shared-model`. +`@metaobjectsdev/codegen-ts`. Every stock generator can be ejected except the `template` +primitive, which has no emit logic of its own to own. `shared-model` and the docs tier +(`docs`, `api-docs`, `mermaid-er`) are not yet ejectable; `meta gen --list` marks each +entry without a reference copy `package-only`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index c46940fff..d55a5d549 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -56,8 +56,8 @@ Every stock generator can be used two ways: | After a package upgrade | output can change; `meta verify --codegen` reports it until you run `meta gen` | output changes only when your copy does | Start with the import. `meta gen --list` prints the import line under each generator -(`wire.import` and `wire.entry` in `--format json`), beside what to install and which -config keys it reads: +(`wire.import` in `--format json`), beside what to install and which config keys it reads. +The call is yours to write from each generator's documented options: ```ts import { defineConfig } from "@metaobjectsdev/cli"; @@ -378,7 +378,7 @@ generator code. The 20-line programmatic shape for each port is in | Port | Invocation | Programmatic — write a `Generator` | Declarative — template + scope | |---|---|---|---| -| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To use a stock generator as shipped, import its factory from its package (`meta gen --list` prints the line) and wire it. To change one: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to put in `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. `requirement-tests` ejects as one file holding the generator and its default stub renderer, so the stub text is yours to change; the requirement walk, each test's identity and the claim digest stay in the package. `cube-model` (the Cube data-model exporter, [cube-export.md](cube-export.md)) ejects as one file as well: which entities get a cube, where the files land and the YAML call are yours, while the mapping (`buildCubeModel`, which raises every refusal) and the YAML writer (`renderCubeYaml`) stay in the package. `callable` and `trace-helper` eject the same way as the prompt tier: the copy decides which objects get a module and where it lands, and the module body comes from `renderCallableFile` / `renderTraceHelper` in the package. Three kinds of registered generator are not ejectable and stay **package-only** — `meta gen --list` marks them so and `meta eject` names them as such: the `template` primitive, which has no emit logic of its own to own (the same ruling as the C#, Java and Python ports); the docs tier (`docs`, `api-docs`, `mermaid-er`), which is the engine behind `meta docs` and is run by that command, not by `generators: [...]`; and `shared-model` (FR-023's publisher generator), which emits a hash-pinned cross-port contract artifact. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | +| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To use a stock generator as shipped, import its factory from its package (`meta gen --list` prints the line) and wire it. To change one: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to put in `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. `requirement-tests` ejects as one file holding the generator and its default stub renderer, so the stub text is yours to change; the requirement walk, each test's identity and the claim digest stay in the package. `cube-model` (the Cube data-model exporter, [cube-export.md](cube-export.md)) ejects as one file as well: which entities get a cube, where the files land and the YAML call are yours, while the mapping (`buildCubeModel`, which raises every refusal) and the YAML writer (`renderCubeYaml`) stay in the package. `callable` and `trace-helper` eject the same way as the prompt tier: the copy decides which objects get a module and where it lands, and the module body comes from `renderCallableFile` / `renderTraceHelper` in the package. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own (the same ruling in every port). Not yet ejectable: the docs tier (`docs`, `api-docs`, `mermaid-er`) and `shared-model` (FR-023's publisher generator). Until each gains a reference copy, `meta gen --list` marks it **package-only** and `meta eject` names it as such. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | | **Java / Kotlin** | `mvn metaobjects:generate` / `mvn metaobjects:verify` (`metaobjects-maven-plugin`) | **Yes.** Extend `FileEmittingGenerator` and read the model through `ModelWalk` (both in `metaobjects-codegen-base`) for one of your own. Every generator — built-in or your own — is named in `` and loaded from the project classpath: one seam, not two. There is no default suite, so `` is the complete list. Kotlin runs through the same goal. | **Yes** — `TemplateScopeGenerator` wired as an ordinary ``. No CLI flag: `` is already the seam. | | **C#** | `dotnet meta gen` / `dotnet meta verify` (.NET tool) | **Yes.** Implement `IGenerator` in the owned console project `codegen/` and list it in `codegen/Program.cs`; `dotnet meta gen` / `verify --codegen` hand off to that project whenever `codegen/Codegen.csproj` exists. `dotnet meta eject ` scaffolds the project, or write its two files by hand. An owned generator the `--generators` selection does not name still runs. | **Yes** — `dotnet meta gen --template-spec --template-root `. | | **Python** | `metaobjects gen` / `metaobjects verify` (console-script) | **Yes.** Name your generator as `module:symbol` in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one. Read the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol` registers **metamodel vocabulary**, not a generator.) | **Yes** — `metaobjects gen --template-spec --templates `. | diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 62e7aa00c..f904e4313 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -127,8 +127,10 @@ name. Import by default and eject to customize: an owned copy you never change o behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the only path for those four.) The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in -`meta gen --list`: the `template` primitive and `shared-model`. +`@metaobjectsdev/codegen-ts`. Every stock generator can be ejected except the `template` +primitive, which has no emit logic of its own to own. `shared-model` and the docs tier +(`docs`, `api-docs`, `mermaid-er`) are not yet ejectable; `meta gen --list` marks each +entry without a reference copy `package-only`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md index d0e4c90e4..a92b5352a 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -270,8 +270,9 @@ they emit. `react`, so a form generator plus the TanStack hook/grid generators is the intended composition, not a conflict. 5. **Wire what you chose.** A generator you will run unchanged is wired from its package: - on TypeScript each catalog row carries `wire.import` and `wire.entry`, plus `install` - and `configKeys`; on the other ports you select it by its stable name. Copy nothing. + on TypeScript each catalog row carries `wire.import`, plus `install` and `configKeys`, + and you write the call from the generator's documented options; on the other ports you + select it by its stable name. Copy nothing. For a generator whose output you need to change, **`meta eject --format json`** copies each into `codegen/generators/` (yours to edit), and reports the import line, the entry to add to `generators`, one diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index e4abee17d..cf7c7f473 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -10,7 +10,7 @@ it there, in the same change, and keep going: | Port | Where the fix goes | |---|---| -| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. A generator `meta gen --list` marks `package-only` (the `template` primitive, `shared-model`) has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | +| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own. `shared-model` and the docs tier (`docs`, `api-docs`, `mermaid-er`) are not yet ejectable. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. | | **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. | | **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Program.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). | diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md index 62e7aa00c..f904e4313 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -127,8 +127,10 @@ name. Import by default and eject to customize: an owned copy you never change o behind the reference. (`entityFile` / `queriesFile` / `routesFile` / `barrel` were removed from that subpath at 1.0 and restored in 1.1, so on a 1.0.x install an owned copy is the only path for those four.) The engine primitives come from the package main entry, -`@metaobjectsdev/codegen-ts`. Two entries cannot be ejected and are `package-only` in -`meta gen --list`: the `template` primitive and `shared-model`. +`@metaobjectsdev/codegen-ts`. Every stock generator can be ejected except the `template` +primitive, which has no emit logic of its own to own. `shared-model` and the docs tier +(`docs`, `api-docs`, `mermaid-er`) are not yet ejectable; `meta gen --list` marks each +entry without a reference copy `package-only`. **`requirementTests()` — one test stub per requirement the model claims.** A recommended approach, not a contract: a `live` or `partial` requirement gets a stub that fails until diff --git a/server/typescript/packages/cli/src/commands/gen.ts b/server/typescript/packages/cli/src/commands/gen.ts index 8f60c9253..515bb0f69 100644 --- a/server/typescript/packages/cli/src/commands/gen.ts +++ b/server/typescript/packages/cli/src/commands/gen.ts @@ -221,10 +221,10 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat narrate(fmt, "\nNothing is generated until you choose it — `generators: []` is what `meta init` " + "scaffolds, by design.\n" + - " meta gen --list --probe the catalog, with how many files each generator " + - "would emit for YOUR model\n" + - " meta eject ... take the ones you want; it prints the import, the " + - "entry to wire, and what to install\n" + + " meta gen --list --probe the catalog: the import that runs each generator as " + + "shipped, and how many files it would emit for YOUR model\n" + + " meta eject ... copy one into your repo to change what it emits; it " + + "prints the import to swap in, the entry to wire, and what to install\n" + "(`meta docs` needs none of this — documentation is on by default.)", ); } diff --git a/server/typescript/packages/cli/src/commands/init.ts b/server/typescript/packages/cli/src/commands/init.ts index f6e10fa23..4209e3f5b 100644 --- a/server/typescript/packages/cli/src/commands/init.ts +++ b/server/typescript/packages/cli/src/commands/init.ts @@ -106,14 +106,15 @@ export default defineConfig({ // would emit, so you can see what your metadata is already asking for. Group by // \`layer\`: model / persistence / api / client / docs / capability. // - // Every row carries a \`wire.import\` line and a \`wire.entry\`. To run a generator as - // shipped, add its import at the top of this file and its entry below: + // Every row carries the import line that runs its generator as shipped + // (\`wire.import\`). Add it at the top of this file: // // import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; // - // then call each one in the array below: entityFile(), queriesFile(), barrel(). - // The row also lists what to install and the config keys the generator reads - // (\`dbImport\`, \`apiPrefix\`, \`extStyle\`, …). Add them here as you go. + // then call each one in the array below: entityFile(), queriesFile(), barrel(). A + // generator's options are in its documentation. The row also lists what to install + // and the config keys the generator reads (\`dbImport\`, \`apiPrefix\`, \`extStyle\`, …). + // Add them here as you go. // // Need one to emit something different? Own it: // diff --git a/server/typescript/packages/cli/src/index.ts b/server/typescript/packages/cli/src/index.ts index 519ed3c27..4cd1cfae2 100644 --- a/server/typescript/packages/cli/src/index.ts +++ b/server/typescript/packages/cli/src/index.ts @@ -237,8 +237,9 @@ USAGE: meta eject ... Copy each generator into codegen/generators/.ts meta eject --list List every ejectable generator name, grouped by package -Codegen is opt-in — \`meta init\` wires nothing — so this is the door every generator -you run comes through. Take several at once: the install set is CONSOLIDATED, so +Eject a generator to CHANGE what it emits. To run one as shipped, import its factory +from its package instead — \`meta gen --list\` prints the import line — and copy +nothing. Take several at once: the install set is CONSOLIDATED, so \`meta eject form hooks grid\` prints one install line, not three. An unknown name refuses the whole call and copies nothing. @@ -251,9 +252,9 @@ FLAGS: --help, -h Print this help \`meta init\` copies NO generators — codegen is opt-in, and codegen/generators/ -starts empty on purpose. Everything you run comes through here (ADR-0034 -scaffold-and-own): the file is YOURS to edit, and \`meta gen\` runs your copy, not -the package's. It prints the import line and the entry to add in +starts empty on purpose. An ejected file is YOURS to edit (ADR-0034 +scaffold-and-own), and \`meta gen\` runs your copy, not the package's. Eject prints +the import line to swap in for the package one and the entry to add in metaobjects.config.ts, and never overwrites a file you already own without --force. Find names with \`meta gen --list\` — add --probe to see how many files each would diff --git a/server/typescript/packages/cli/src/lib/catalog-listing.ts b/server/typescript/packages/cli/src/lib/catalog-listing.ts index 239a01ba3..a59f9a036 100644 --- a/server/typescript/packages/cli/src/lib/catalog-listing.ts +++ b/server/typescript/packages/cli/src/lib/catalog-listing.ts @@ -70,9 +70,9 @@ export interface GeneratorCatalogRow { configKeys?: readonly string[]; install: { dev: string[]; runtime: string[] }; /** - * How to run this generator AS SHIPPED: the import line for its package factory and the - * entry for `generators: [...]`. No copy involved; `meta eject` prints the same pair for - * an owned one. Absent on the `docs` layer, whose door is `meta docs`. + * How to run this generator AS SHIPPED: the import line for its package factory. No + * copy involved; `meta eject` prints the local import for an owned one. Absent on the + * `docs` layer, whose door is `meta docs`. */ wire?: GeneratorWiring; source: { diff --git a/server/typescript/packages/cli/src/lib/catalog.ts b/server/typescript/packages/cli/src/lib/catalog.ts index 35b685d65..42e715217 100644 --- a/server/typescript/packages/cli/src/lib/catalog.ts +++ b/server/typescript/packages/cli/src/lib/catalog.ts @@ -65,33 +65,20 @@ export function factoryModuleOf(name: string): string | undefined { return SLICES.find(([, slice]) => name in slice)?.[2]; } -/** How one generator reaches `generators: [...]` — the import line and the entry. */ -export interface GeneratorWiring { - import: string; - entry: string; -} - /** - * The option names an entry's `options` summary lists WITHOUT a trailing `?` — the ones - * its factory cannot be called without. `options` is the option surface's only - * declaration and this is its only reader: `--list` prints no option list of its own, so - * the string IS the wiring data, not a caption beside it. The `?`-suffix convention is - * documented on the field in `generator-registry.ts`, and - * `catalog-factory-exports.test.ts` pins the two call sites that depend on it. + * How one generator reaches a config as shipped: the import line for its factory. The + * call is not derived here — a factory's options are documented with the generator. */ -function requiredOptionNames(entry: GeneratorRegistryEntry): string[] { - return (entry.options ?? "") - .split(",") - .map((o) => o.trim()) - .filter((o) => o !== "" && !o.endsWith("?")); +export interface GeneratorWiring { + import: string; } /** * How to wire a catalog entry AS SHIPPED: import its factory from the package, no copy. * * This is the half of "import by default, eject to customize" the catalog owns. The - * other half is `meta eject`, which prints the same two facts for the owned copy — the - * symbol is the same in both (gated), so moving between them is one import line. + * other half is `meta eject`, which prints the import for the owned copy — the symbol is + * the same in both (gated), so moving between them is one import line. * * Undefined for the `docs` layer: those generators are the engine behind `meta docs`, * which is their door, and wiring one into `generators: [...]` is the deprecated route. @@ -100,13 +87,7 @@ export function packagedWiring(entry: GeneratorRegistryEntry): GeneratorWiring | if (entry.layer === "docs") return undefined; const from = factoryModuleOf(entry.name); if (from === undefined) return undefined; - const required = requiredOptionNames(entry); - return { - import: `import { ${entry.factoryExport} } from "${from}";`, - entry: required.length === 0 - ? `${entry.factoryExport}()` - : `${entry.factoryExport}({ ${required.join(", ")} })`, - }; + return { import: `import { ${entry.factoryExport} } from "${from}";` }; } /** diff --git a/server/typescript/packages/cli/test/catalog-factory-exports.test.ts b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts index 233760bb2..b5f7fda34 100644 --- a/server/typescript/packages/cli/test/catalog-factory-exports.test.ts +++ b/server/typescript/packages/cli/test/catalog-factory-exports.test.ts @@ -20,7 +20,7 @@ import { join } from "node:path"; import * as coreFactories from "@metaobjectsdev/codegen-ts/generators"; import * as reactFactories from "@metaobjectsdev/codegen-ts-react"; import * as tanstackFactories from "@metaobjectsdev/codegen-ts-tanstack"; -import { factoryModuleOf, listCatalog, packagedWiring } from "../src/lib/catalog.js"; +import { factoryModuleOf, listCatalog } from "../src/lib/catalog.js"; import { buildCatalogListing, renderCatalogText, type GeneratorCatalogRow } from "../src/lib/catalog-listing.js"; import { ejectGenerator } from "../src/commands/eject.js"; @@ -87,15 +87,6 @@ describe("catalog factory exports", () => { if (ejected.exportName !== entry.factoryExport) { differing.push(`${entry.name}: owned copy exports ${ejected.exportName}, package exports ${entry.factoryExport}`); } - // One symbol is only half the claim: the two paths must print the SAME call. - // `packagedWiring` spells required options off the entry's `options` string, - // while the eject line is built bare — so equality here also holds the rule - // that no ejectable entry has a required option. - const packaged = packagedWiring(entry); - const ejectEntry = `${ejected.exportName}()`; - if (packaged !== undefined && packaged.entry !== ejectEntry) { - differing.push(`${entry.name}: package prints "${packaged.entry}", eject prints "${ejectEntry}"`); - } } expect(differing).toEqual([]); } finally { @@ -105,7 +96,7 @@ describe("catalog factory exports", () => { }); describe("meta gen --list — how to wire a generator as shipped", () => { - test("every row outside the docs layer carries its package import and entry", async () => { + test("every row outside the docs layer carries its package import, and no derived call", async () => { const rows = (await buildCatalogListing()).filter( (r): r is GeneratorCatalogRow => r.kind === "generator", ); @@ -116,7 +107,7 @@ describe("meta gen --list — how to wire a generator as shipped", () => { continue; } expect(row.wire?.import).toMatch(/^import \{ \w+ \} from "@metaobjectsdev\/codegen-ts[\w/-]*";$/); - expect(row.wire?.entry).toMatch(/^\w+\((\{ [\w, ]+ \})?\)$/); + expect({ name: row.name, keys: Object.keys(row.wire ?? {}) }).toEqual({ name: row.name, keys: ["import"] }); } }); @@ -128,20 +119,12 @@ describe("meta gen --list — how to wire a generator as shipped", () => { ); expect(byName.get("entity")?.wire).toEqual({ import: `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`, - entry: "entityFile()", }); expect(byName.get("hooks")?.wire).toEqual({ import: `import { tanstackQuery } from "@metaobjectsdev/codegen-ts-tanstack";`, - entry: "tanstackQuery()", }); }); - test("a factory that cannot be called bare names the options it needs", () => { - const entry = listCatalog().find((e) => e.name === "shared-model"); - if (entry === undefined) throw new Error("shared-model left the catalog"); - expect(packagedWiring(entry)?.entry).toBe("sharedModelFile({ name, include })"); - }); - test("the text listing prints the import under a row, and drops it once the copy is owned", async () => { const rows = await buildCatalogListing(); const text = renderCatalogText(rows, false); diff --git a/server/typescript/packages/codegen-ts/src/catalog-gates.ts b/server/typescript/packages/codegen-ts/src/catalog-gates.ts index 1d6b087ab..d60d02ad0 100644 --- a/server/typescript/packages/codegen-ts/src/catalog-gates.ts +++ b/server/typescript/packages/codegen-ts/src/catalog-gates.ts @@ -92,7 +92,8 @@ export function warnUnsatisfiedRequires( `"${stable}" is wired but ${list} ${missing.length === 1 ? "is" : "are"} not. ` + `The code "${stable}" emits imports modules ${list} would have emitted, so ` + `tsc will report an unresolved import. Wire ${missing.length === 1 ? "it" : "them"} ` + - `(\`meta eject ${missing.join(" ")}\` copies the reference generator${missing.length === 1 ? "" : "s"}), ` + + `(\`meta gen --list\` prints the import that runs ${missing.length === 1 ? "it" : "each"} as shipped; ` + + `\`meta eject ${missing.join(" ")}\` copies the reference generator${missing.length === 1 ? "" : "s"} to change what ${missing.length === 1 ? "it emits" : "they emit"}), ` + `or keep your own hand-written module${missing.length === 1 ? "" : "s"} at ${missing.length === 1 ? "that path" : "those paths"}.`, ); } diff --git a/server/typescript/packages/codegen-ts/src/generator-registry.ts b/server/typescript/packages/codegen-ts/src/generator-registry.ts index 62b016da3..a0bbb4ea2 100644 --- a/server/typescript/packages/codegen-ts/src/generator-registry.ts +++ b/server/typescript/packages/codegen-ts/src/generator-registry.ts @@ -112,12 +112,7 @@ export interface GeneratorRegistryEntry { * name whose generator carries the same implementation name as {@link factory}'s. */ factoryExport: string; - /** - * The factory's option surface: comma-separated option names, each carrying a trailing - * `?` when optional. Nothing renders this string — the CLI's `packagedWiring` derives - * the `generators: [...]` call advice from it, so rewording it is a wiring change, not - * a copy edit. - */ + /** Optional one-line options summary for `--list`. */ options?: string; /** The framework this generator's OUTPUT targets. Absent = framework-neutral. */ framework?: GeneratorFramework; diff --git a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md index 5722e6c1b..8a5e7fddd 100644 --- a/spec/decisions/ADR-0034-codegen-scaffold-and-own.md +++ b/spec/decisions/ADR-0034-codegen-scaffold-and-own.md @@ -355,8 +355,10 @@ ejectable."* Two things follow, and both are the rule from here on: - **Every stock generator can be run as shipped**, by importing its factory from its package. Nothing is copied, and an upgrade of the package is how its fixes arrive. -- **Every stock generator can be ejected**, when the adopter wants to change what it emits. - Ejecting is the customization route; it is not a precondition for generating. +- **Every stock generator can be ejected except `template`**, when the adopter wants to + change what it emits. Ejecting is the customization route; it is not a precondition for + generating. `template` is the one permanent exception, in every port: it is the Mustache + primitive and has no emit logic of its own to own. **Supersedes** Decision 3 ("you cannot `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"`") and the clause of Decision 2 that makes @@ -385,10 +387,12 @@ failure back. `@metaobjectsdev/codegen-ts/generators` again. The config loader's two diagnostics for the removed imports went with the removal. 2. The catalog says how to import. Each registry entry carries `factoryExport`, and - `meta gen --list` prints the import line that runs the generator as shipped (`wire` on - the JSON row). It is resolved, not trusted: a gate imports each package's public factory - module and requires that name, and requires the reference template of an ejectable - generator to export the same symbol, so moving between the two paths is one import line. + `meta gen --list` prints the import line that runs the generator as shipped + (`wire.import` on the JSON row). It is resolved, not trusted: a gate imports each + package's public factory module and requires that name, and requires the reference + template of an ejectable generator to export the same symbol, so moving between the two + paths is one import line. The catalog prints the import only. It does not derive the + call: a factory's options are documented with the generator. 3. `callable` and `trace-helper` gained reference templates and left the package-only list. 4. `meta init` is unchanged in what it wires, which is nothing (Amendment 2 stands). Its scaffolded comment now shows the import first and eject as the way to change output. @@ -409,13 +413,12 @@ failure back. name, so the two paths start from the same output. The one difference is where the HTTP-adapter import points, because eject hands over that source too (Amendment 3). -**Still not ejectable, with the reason for each.** In TypeScript: `template`, which is the -Mustache primitive and has no emit logic of its own (the C#, Java and Python ports rule the -same way); `shared-model`, whose output is a hash-pinned artifact that a consumer's -`meta deps sync` verifies, so it is a contract rather than a helper; and `docs`, `api-docs` -and `mermaid-er`, which are the engine behind `meta docs` and have no seam to run an owned -copy through. Outside TypeScript, the Java `entity` and `extractor` generators became -ejectable in Amendment 6. Every port's generators run as shipped, selected by name. +**Not yet ejectable in this change.** The rule above is ahead of the code for four +TypeScript generators: `shared-model`, `docs`, `api-docs` and `mermaid-er`. Each one runs as +shipped today and gains a reference copy through a sibling change, which updates this list +when it lands. The Java `entity` and `extractor` generators became ejectable in Amendment 6. +`template` is not on the list because it is the permanent exception. Every port's +generators run as shipped, selected by name. ### Amendment 6 (2026-10-11) — the Java `entity` and `extractor` generators are ejectable From 877e3e6c8168a2311f97b79bfbfea55cf63ce4ec Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 09:07:39 -0400 Subject: [PATCH 04/10] no-mistakes(review): Guard edited runtime adapters; teach import-first in skills Assisted-by: no-mistakes:claude:claude-opus-5-5 --- CHANGELOG.md | 8 +++-- .../skills/metaobjects-audit/SKILL.md | 5 ++- .../references/typescript.md | 12 +++++++ .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 8 +++-- .../references/typescript.md | 13 ++++--- .../references/typescript.md | 9 +++-- .../references/react.md | 7 ++-- docs/features/own-your-codegen.md | 18 ++++++++-- docs/ports/typescript.md | 35 ++++++++----------- .../document-graph-and-wide-column-stores.md | 6 ++-- .../.claude/skills/metaobjects-audit/SKILL.md | 5 ++- .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../references/react.md | 7 ++-- .../.claude/skills/metaobjects-audit/SKILL.md | 5 ++- .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../references/react.md | 7 ++-- .../.claude/skills/metaobjects-audit/SKILL.md | 5 ++- .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 5 ++- .../references/typescript.md | 12 +++++++ .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 8 +++-- .../references/typescript.md | 13 ++++--- .../references/typescript.md | 9 +++-- .../references/react.md | 7 ++-- .../.claude/skills/metaobjects-audit/SKILL.md | 5 ++- .../references/typescript.md | 12 +++++++ .../references/any-stack.md | 6 ++-- .../references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 8 +++-- .../references/typescript.md | 13 ++++--- .../references/typescript.md | 9 +++-- .../integration/document-store-recipe.test.ts | 10 +++--- 39 files changed, 189 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 118c82319..7bfd4f98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,11 @@ here.** `meta eject --list` and `meta verify --codegen` behave as before. - **To drop a copy you never changed**, replace its local import with the package import, delete the file and run `meta gen`. `queries` and `barrel` regenerate nothing if the - copy matched the reference. `entity` and `routes` also move their HTTP-adapter import - from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which then has to be - installed again. Steps and the diff to expect: + copy matched the reference. `entity`, `routes` and `routes-hono` also move their + HTTP-adapter import from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which + then has to be installed again. For those three, a copy is unchanged only when + `meta eject --list` also marks every `codegen/runtime/` file it reaches identical: if + you edited the adapter, keep the owned generator. Steps and the diff to expect: [own-your-codegen](docs/features/own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). - **An imported generator is still a reference helper.** Its output is not covered by the compatibility policy and can change in a MINOR; `meta verify --codegen` reports that diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/agent-context/skills/metaobjects-audit/references/typescript.md b/agent-context/skills/metaobjects-audit/references/typescript.md index feab2c601..6c0344cf8 100644 --- a/agent-context/skills/metaobjects-audit/references/typescript.md +++ b/agent-context/skills/metaobjects-audit/references/typescript.md @@ -95,6 +95,18 @@ its local import with the package import and deleting the file, then `meta gen` review of the diff. A copy with lines of its own is an intentional customization. Check only how far behind it is. +**`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one +also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix +is often made there with the generator file left untouched. The same listing marks each +copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For +these three, the copy is unmodified only when the generator file AND every +`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +is not in the package, the copy carries a customization: it is **not a finding**, and do +not recommend the package import. The packaged generator imports +`@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with +nothing reporting it. The owned generator is what points the output at that adapter (its +`runtimeImport` option), so it stays. + **Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, `routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, diff --git a/agent-context/skills/metaobjects-codegen/references/any-stack.md b/agent-context/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/agent-context/skills/metaobjects-codegen/references/any-stack.md +++ b/agent-context/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md b/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md index 14c77c36d..132c83867 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md @@ -14,10 +14,11 @@ enum fails Zod validation. ## Use it 1. Save the file below as `codegen/generators/mongo-repository.ts` — it is yours to edit. -2. `npx meta eject entity`, then wire both generators: +2. Wire it next to the stock entity generator, imported from its package and run as + shipped: ```ts - import { entityFile } from "./codegen/generators/entity.js"; + import { entityFile } from "@metaobjectsdev/codegen-ts/generators"; import { mongoRepositoryGenerator } from "./codegen/generators/mongo-repository.js"; export default defineConfig({ @@ -26,7 +27,8 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb`, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists + under install, then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/agent-context/skills/metaobjects-codegen/references/typescript.md b/agent-context/skills/metaobjects-codegen/references/typescript.md index f904e4313..c5903cffc 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript.md @@ -64,16 +64,15 @@ Codegen is wired in a type-checked TS config at the project root. `defineConfig` comes from `@metaobjectsdev/cli`; the generators come from their packages. `meta init` scaffolds this file with **`generators: []`** — nothing is generated until -you choose it. Each import below appears once you `meta eject` that generator, which -prints the exact line to add. +you choose it. Each import below runs a generator as shipped; `meta gen --list` prints +the exact line for every generator. Only a generator you are going to change is ejected: +`meta eject ` copies it into `codegen/generators/` and prints the local import to +swap in for the package one. ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators — copied in by `meta eject` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped: nothing copied, fixes arrive by upgrading the package. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack"; diff --git a/agent-context/skills/metaobjects-prompts/references/typescript.md b/agent-context/skills/metaobjects-prompts/references/typescript.md index 189c9a88f..287ceb5ba 100644 --- a/agent-context/skills/metaobjects-prompts/references/typescript.md +++ b/agent-context/skills/metaobjects-prompts/references/typescript.md @@ -127,11 +127,10 @@ answer like this" fragment you splice into the prompt text so the model returns shape the parser above expects: ```ts -// metaobjects.config.ts — the same owned entity trio as above, plus outputPrompt -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -import { promptRender, outputParser, outputPrompt } from "@metaobjectsdev/codegen-ts/generators"; +// metaobjects.config.ts — the same entity trio as above, plus outputPrompt +import { + entityFile, queriesFile, barrel, promptRender, outputParser, outputPrompt, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/agent-context/skills/metaobjects-runtime-ui/references/react.md b/agent-context/skills/metaobjects-runtime-ui/references/react.md index 8e4414173..4e58a77f0 100644 --- a/agent-context/skills/metaobjects-runtime-ui/references/react.md +++ b/agent-context/skills/metaobjects-runtime-ui/references/react.md @@ -33,10 +33,9 @@ validation rule) rides along automatically: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; export default defineConfig({ diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index d55a5d549..6b645ed1f 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -96,6 +96,17 @@ falls behind the reference. To drop one: `DIFFERS` with `0 line(s) of your own`: it is only behind. A copy with lines of your own is a customization; keep it, or check what the lines are with `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts`. + + For `entity`, `routes` and `routes-hono`, check the adapter too. Ejecting one also + copied the HTTP adapter into `codegen/runtime/`, and a route fix is often made there + with the generator file left untouched. The same listing marks each of those files + `identical`, `DIFFERS` or `not in the package`. The copy is unmodified only when the + generator file and every `codegen/runtime/` file it reaches + ([the table below](#what-meta-eject-hands-over-and-what-stays-core-typescript)) are + marked identical. If one differs, the copy carries your change: keep the owned + generator, because it is what points the generated code at your adapter (its + `runtimeImport` option), and the packaged generator always imports + `@metaobjectsdev/runtime-ts`. Stop here for that generator. 2. In `metaobjects.config.ts`, replace the local import with the package one. The factory name does not change: @@ -112,9 +123,10 @@ What the diff in step 4 holds: - **A copy that matched the reference** regenerates nothing for `queries` and `barrel`. - **A copy that was behind** regenerates the changes it was missing. That is the reference catching up, so read it the way you would read an upgrade. -- **`entity` and `routes` also move one import.** An ejected `entity` or `routes` points - its output at the adapter copy in `codegen/runtime/`, and the packaged generator imports - `@metaobjectsdev/runtime-ts`. So those import lines change, and the project needs +- **`entity`, `routes` and `routes-hono` also move one import.** An ejected `entity`, + `routes` or `routes-hono` points its output at the adapter copy in `codegen/runtime/`, + and the packaged generator imports `@metaobjectsdev/runtime-ts`. So those import lines + change, and the project needs `@metaobjectsdev/runtime-ts` installed again (`meta gen --list --format json` gives the range under `install.runtime`). A copy wired with `runtimeImport: "@metaobjectsdev/runtime-ts"` already imported the package and moves diff --git a/docs/ports/typescript.md b/docs/ports/typescript.md index a43f9087c..c8ffb0e3a 100644 --- a/docs/ports/typescript.md +++ b/docs/ports/typescript.md @@ -53,20 +53,18 @@ Two config files, by design: `codegen/generators/` with `generators: []` (nothing is generated until you choose — ADR-0034 Amendment 2), and the `.gitignore` entries for `.metaobjects/.gen-state/`. Choose from the catalog with `meta gen --list --probe`, which reports how many files -each generator would emit for your model, then take the ones you want: -`meta eject entity queries routes barrel` copies each reference template into -`codegen/generators/` (ADR-0034 scaffold-and-own — yours to edit) and prints the import, -the entry to wire, what to install and the config keys it reads. `meta gen` runs those -local copies, not the package's. +each generator would emit for your model and prints the import line that runs it as +shipped, beside what to install and the config keys it reads. Import the ones you want +from their package: nothing is copied, and an upgrade is how their fixes arrive. To change +what one emits, `meta eject ` copies its reference template into +`codegen/generators/` (ADR-0034 scaffold-and-own — yours to edit) and prints the local +import to swap in; `meta gen` then runs that copy, not the package's. ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` — yours to edit (ADR-0034). -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { routesFile } from "./codegen/generators/routes.js"; -import { barrel } from "./codegen/generators/barrel.js"; +// Run as shipped. Eject one (`meta eject routes`) only to change what it emits. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", @@ -83,7 +81,7 @@ export default defineConfig({ ### Declarative template-codegen (Mustache) Owning a generator is one of **two** authoring paths, and TypeScript has both. A -generator can be **programmatic** — the scaffolded `codegen/generators/*.ts` above, +generator can be **programmatic** — a `codegen/generators/*.ts` file you ejected or wrote, composing this package's `render*` functions — or **declarative**: a Mustache template plus a scope, with no generator code at all. @@ -102,7 +100,7 @@ declared anywhere else would have its output reported as drift): // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; import { templateGenerator } from "@metaobjectsdev/codegen-ts"; -import { entityFile } from "./codegen/generators/entity.js"; +import { entityFile } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", @@ -579,15 +577,10 @@ difference is the framework adapter the emitted code talks to. ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators scaffolded by `meta init` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -// Hono routes are NOT scaffolded eagerly by `meta init` (the default emit targets -// Fastify) but have the same reference template + ownership route as entity/queries/ -// routes/barrel — take it with `meta eject routes-hono`, which copies it to -// codegen/generators/routes-hono.ts. -import { routesFileHono } from "./codegen/generators/routes-hono.js"; +// Run as shipped, imported from the package. `routesFileHono` has the same reference +// template and ownership route as the others: `meta eject routes-hono` copies it to +// codegen/generators/routes-hono.ts when you need to change what it emits. +import { entityFile, queriesFile, routesFileHono, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/docs/recipes/document-graph-and-wide-column-stores.md b/docs/recipes/document-graph-and-wide-column-stores.md index 95355fb5e..6daac4930 100644 --- a/docs/recipes/document-graph-and-wide-column-stores.md +++ b/docs/recipes/document-graph-and-wide-column-stores.md @@ -66,15 +66,15 @@ repository. A test keeps the two identical. [`generators/typescript/mongo-repository.ts`](generators/typescript/mongo-repository.ts) is a complete example for the MongoDB Node driver. Copy it into `codegen/generators/`, then wire -it next to the entity generator: +it next to the stock entity generator, which is imported from its package and run as shipped +(install the packages its `meta gen --list` row lists): ```bash -meta eject entity cp /docs/recipes/generators/typescript/mongo-repository.ts codegen/generators/ ``` ```ts -import { entityFile } from "./codegen/generators/entity.js"; +import { entityFile } from "@metaobjectsdev/codegen-ts/generators"; import { mongoRepositoryGenerator } from "./codegen/generators/mongo-repository.js"; export default defineConfig({ diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md index 8e4414173..4e58a77f0 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md @@ -33,10 +33,9 @@ validation rule) rides along automatically: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; export default defineConfig({ diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/any-stack.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/react.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/react.md index 8e4414173..4e58a77f0 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/react.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-runtime-ui/references/react.md @@ -33,10 +33,9 @@ validation rule) rides along automatically: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; export default defineConfig({ diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/any-stack.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md index feab2c601..6c0344cf8 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -95,6 +95,18 @@ its local import with the package import and deleting the file, then `meta gen` review of the diff. A copy with lines of its own is an intentional customization. Check only how far behind it is. +**`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one +also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix +is often made there with the generator file left untouched. The same listing marks each +copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For +these three, the copy is unmodified only when the generator file AND every +`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +is not in the package, the copy carries a customization: it is **not a finding**, and do +not recommend the package import. The packaged generator imports +`@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with +nothing reporting it. The owned generator is what points the output at that adapter (its +`runtimeImport` option), so it stays. + **Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, `routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md index 14c77c36d..132c83867 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md @@ -14,10 +14,11 @@ enum fails Zod validation. ## Use it 1. Save the file below as `codegen/generators/mongo-repository.ts` — it is yours to edit. -2. `npx meta eject entity`, then wire both generators: +2. Wire it next to the stock entity generator, imported from its package and run as + shipped: ```ts - import { entityFile } from "./codegen/generators/entity.js"; + import { entityFile } from "@metaobjectsdev/codegen-ts/generators"; import { mongoRepositoryGenerator } from "./codegen/generators/mongo-repository.js"; export default defineConfig({ @@ -26,7 +27,8 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb`, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists + under install, then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md index f904e4313..c5903cffc 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -64,16 +64,15 @@ Codegen is wired in a type-checked TS config at the project root. `defineConfig` comes from `@metaobjectsdev/cli`; the generators come from their packages. `meta init` scaffolds this file with **`generators: []`** — nothing is generated until -you choose it. Each import below appears once you `meta eject` that generator, which -prints the exact line to add. +you choose it. Each import below runs a generator as shipped; `meta gen --list` prints +the exact line for every generator. Only a generator you are going to change is ejected: +`meta eject ` copies it into `codegen/generators/` and prints the local import to +swap in for the package one. ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators — copied in by `meta eject` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped: nothing copied, fixes arrive by upgrading the package. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack"; diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md index 189c9a88f..287ceb5ba 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-prompts/references/typescript.md @@ -127,11 +127,10 @@ answer like this" fragment you splice into the prompt text so the model returns shape the parser above expects: ```ts -// metaobjects.config.ts — the same owned entity trio as above, plus outputPrompt -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -import { promptRender, outputParser, outputPrompt } from "@metaobjectsdev/codegen-ts/generators"; +// metaobjects.config.ts — the same entity trio as above, plus outputPrompt +import { + entityFile, queriesFile, barrel, promptRender, outputParser, outputPrompt, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md index 8e4414173..4e58a77f0 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/react.md @@ -33,10 +33,9 @@ validation rule) rides along automatically: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; export default defineConfig({ diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md index 4682f2370..3567085d0 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,10 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. **An import of `entityFile` / + of its own should be replaced by the package import. For `entity`, `routes` and + `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is + also marked identical: an edited adapter is a customization, and such a copy is not a + finding. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md index feab2c601..6c0344cf8 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -95,6 +95,18 @@ its local import with the package import and deleting the file, then `meta gen` review of the diff. A copy with lines of its own is an intentional customization. Check only how far behind it is. +**`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one +also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix +is often made there with the generator file left untouched. The same listing marks each +copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For +these three, the copy is unmodified only when the generator file AND every +`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +is not in the package, the copy carries a customization: it is **not a finding**, and do +not recommend the package import. The packaged generator imports +`@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with +nothing reporting it. The owned generator is what points the output at that adapter (its +`runtimeImport` option), so it stays. + **Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, `routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md index 391bf2b39..f69c1bbdb 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md @@ -30,8 +30,10 @@ reference generators target React and TanStack. Anything else is this page. | **api** — the HTTP surface | Fastify, Hono | §4 | | **client** — hooks, forms, grids | React, TanStack | §5 | -The model layer never depends on the store or the framework. Generate it first -(`meta eject entity`, wire `entityFile()`); everything you write later imports it. +The model layer never depends on the store or the framework. Generate it first: import +`entityFile` from `@metaobjectsdev/codegen-ts/generators` and wire `entityFile()`. Nothing +is copied, and everything you write later imports its output. Eject it (`meta eject +entity`) only if you need to change what it emits. ## 3. Persistence diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index cf7c7f473..4c0892f48 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. | +| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md index 14c77c36d..132c83867 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md @@ -14,10 +14,11 @@ enum fails Zod validation. ## Use it 1. Save the file below as `codegen/generators/mongo-repository.ts` — it is yours to edit. -2. `npx meta eject entity`, then wire both generators: +2. Wire it next to the stock entity generator, imported from its package and run as + shipped: ```ts - import { entityFile } from "./codegen/generators/entity.js"; + import { entityFile } from "@metaobjectsdev/codegen-ts/generators"; import { mongoRepositoryGenerator } from "./codegen/generators/mongo-repository.js"; export default defineConfig({ @@ -26,7 +27,8 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb`, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists + under install, then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md index f904e4313..c5903cffc 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md @@ -64,16 +64,15 @@ Codegen is wired in a type-checked TS config at the project root. `defineConfig` comes from `@metaobjectsdev/cli`; the generators come from their packages. `meta init` scaffolds this file with **`generators: []`** — nothing is generated until -you choose it. Each import below appears once you `meta eject` that generator, which -prints the exact line to add. +you choose it. Each import below runs a generator as shipped; `meta gen --list` prints +the exact line for every generator. Only a generator you are going to change is ejected: +`meta eject ` copies it into `codegen/generators/` and prints the local import to +swap in for the package one. ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators — copied in by `meta eject` (ADR-0034 scaffold-and-own). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped: nothing copied, fixes arrive by upgrading the package. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack"; diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md index 189c9a88f..287ceb5ba 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md @@ -127,11 +127,10 @@ answer like this" fragment you splice into the prompt text so the model returns shape the parser above expects: ```ts -// metaobjects.config.ts — the same owned entity trio as above, plus outputPrompt -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { barrel } from "./codegen/generators/barrel.js"; -import { promptRender, outputParser, outputPrompt } from "@metaobjectsdev/codegen-ts/generators"; +// metaobjects.config.ts — the same entity trio as above, plus outputPrompt +import { + entityFile, queriesFile, barrel, promptRender, outputParser, outputPrompt, +} from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", diff --git a/server/typescript/packages/cli/test/integration/document-store-recipe.test.ts b/server/typescript/packages/cli/test/integration/document-store-recipe.test.ts index 05bd887b7..b1d66b4a4 100644 --- a/server/typescript/packages/cli/test/integration/document-store-recipe.test.ts +++ b/server/typescript/packages/cli/test/integration/document-store-recipe.test.ts @@ -4,8 +4,9 @@ * docs/recipes/generators/typescript/mongo-repository.ts. This gate does exactly that: * * - a model whose `Order` is a SOURCELESS entity (identity, no `source.*`); - * - `meta eject entity`, then the example generator copied in verbatim and wired; - * - a strict typecheck of both generators, `meta gen`, and `meta verify --codegen`. + * - the example generator copied in verbatim and wired beside the stock entity + * generator, imported from its package; + * - a strict typecheck of the copied generator, `meta gen`, and `meta verify --codegen`. * * It pins the two halves the recipe depends on: the entity module gives a sourceless entity * its create/PATCH schemas and allowlists (and no table), and the example generator picks it @@ -64,7 +65,6 @@ describe("document-store recipe", () => { root = await newShopProject("doc-store-"); writeModel(root, MODEL); - expect(await meta(root, "eject", "entity")).toMatchObject({ exit: 0 }); copyFileSync(EXAMPLE, join(root, "codegen", "generators", "mongo-repository.ts")); // The wiring the recipe documents. @@ -73,14 +73,14 @@ describe("document-store recipe", () => { .replace( 'import { defineConfig } from "@metaobjectsdev/cli";', 'import { defineConfig } from "@metaobjectsdev/cli";\n' + - 'import { entityFile } from "./codegen/generators/entity.js";\n' + + 'import { entityFile } from "@metaobjectsdev/codegen-ts/generators";\n' + 'import { mongoRepositoryGenerator } from "./codegen/generators/mongo-repository.js";', ) .replace("generators: [],", "generators: [entityFile(), mongoRepositoryGenerator()],"); expect(config).toContain("mongoRepositoryGenerator()"); writeFileSync(configPath, config); - const tsc = await typecheckGenerators(root, ["codegen/generators/entity.ts", "codegen/generators/mongo-repository.ts"]); + const tsc = await typecheckGenerators(root, ["codegen/generators/mongo-repository.ts"]); expect({ step: "tsc", ...tsc }).toMatchObject({ step: "tsc", exit: 0 }); expect(await meta(root, "gen")).toMatchObject({ exit: 0 }); From 576b2c78794af92cfe7ad8f09811ec5ca6bdf750 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 09:22:52 -0400 Subject: [PATCH 05/10] no-mistakes(review): Name format-aware listing commands for runtime adapter check Assisted-by: no-mistakes:claude:claude-opus-5-5 --- CHANGELOG.md | 7 +++- .../skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/typescript.md | 39 ++++++++++++++----- .../references/owning-generators.md | 2 +- .../references/typescript-docs.md | 5 ++- docs/features/own-your-codegen.md | 19 ++++++--- .../.claude/skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/typescript.md | 39 ++++++++++++++----- .../references/owning-generators.md | 2 +- .../references/typescript-docs.md | 5 ++- .../.claude/skills/metaobjects-audit/SKILL.md | 12 +++--- .../references/typescript.md | 39 ++++++++++++++----- .../references/owning-generators.md | 2 +- .../references/typescript-docs.md | 5 ++- .../cli/test/skill-catalog-grounding.test.ts | 5 ++- 21 files changed, 165 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bfd4f98e..c0de09220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,11 @@ here.** copy matched the reference. `entity`, `routes` and `routes-hono` also move their HTTP-adapter import from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which then has to be installed again. For those three, a copy is unchanged only when - `meta eject --list` also marks every `codegen/runtime/` file it reaches identical: if - you edited the adapter, keep the owned generator. Steps and the diff to expect: + `meta eject --list --format text` also marks every `codegen/runtime/` file it reaches + identical: if you edited the adapter, keep the owned generator. Those rows are printed + in the text form only (`meta gen --list --format json` carries them as + `source.runtimeCopy[]`), so a listing without them is not evidence that the adapter is + unchanged. Steps and the diff to expect: [own-your-codegen](docs/features/own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). - **An imported generator is still a reference helper.** Its output is not covered by the compatibility policy and can change in a MINOR; `meta verify --codegen` reports that diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/agent-context/skills/metaobjects-audit/references/typescript.md b/agent-context/skills/metaobjects-audit/references/typescript.md index 6c0344cf8..2d24e86c2 100644 --- a/agent-context/skills/metaobjects-audit/references/typescript.md +++ b/agent-context/skills/metaobjects-audit/references/typescript.md @@ -85,21 +85,40 @@ import { routesFile } from "./codegen/generators/routes"; is duplication, and it falls behind the reference: ```bash -meta eject --list +meta eject --list --format text ``` -marks each owned copy `identical to the reference`, `same content as the reference, your -formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is -identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing -its local import with the package import and deleting the file, then `meta gen` and a -review of the diff. A copy with lines of its own is an intentional customization. Check -only how far behind it is. +Pass `--format text`. Away from a terminal the default is a structured document, and +that document holds the generator rows only: it has no `codegen/runtime/` rows at all. + +The text listing marks each owned copy `identical to the reference`, `same content as the +reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those +are the `--format text` labels. In `--format json` or `toon` the same row carries +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is +identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a +**finding**: recommend replacing its local import with the package import and deleting +the file, then `meta gen` and a review of the diff. A copy with lines of its own is an +intentional customization. Check only how far behind it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix -is often made there with the generator file left untouched. The same listing marks each -copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For -these three, the copy is unmodified only when the generator file AND every +is often made there with the generator file left untouched. Read the state of those files +from one of two places: + +- `meta eject --list --format text` prints a block of `codegen/runtime/` rows after the + generators, each marked `identical to the package`, `same content as the package, your + formatting`, `DIFFERS: N line(s) only in the package, M line(s) only in yours`, or + `not in the package`. +- `meta gen --list --format json` puts them on the generator's own row, inside its + `source` object: an owned row has `owned: true` there and a `runtimeCopy` array, one + entry per adapter file that generator reaches, with `path` and `verdict` (`identical`, + `reformatted` or `differs`). + +**A listing with no runtime rows is not evidence that the adapter is unmodified.** If +`codegen/runtime/` exists and the output you read shows no row for its files, you have +not checked: run one of the two commands above. + +For these three, the copy is unmodified only when the generator file AND every `codegen/runtime/` file it reaches are marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-docs.md b/agent-context/skills/metaobjects-codegen/references/typescript-docs.md index 15584f858..356232ff2 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript-docs.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript-docs.md @@ -58,8 +58,9 @@ install what eject prints instead (`qs`, `@types/qs`, the framework, the core What stays a package import is core: `@metaobjectsdev/metadata`, `render`, the reply parser. Options on each ejected generator: `runtimeImport` moves the copy (relative to the output root, like `dbImport`, or a path alias); `runtimeImport: "@metaobjectsdev/runtime-ts"` -goes back to the package. `meta eject --list` and `meta gen --list` mark each copied file -identical to or differing from the installed package; to take an upstream fix, read +goes back to the package. `meta eject --list --format text` and `meta gen --list` mark each +copied file identical to or differing from the installed package (the structured form of +`meta eject --list` omits those rows); to take an upstream fix, read `diff -u node_modules/@metaobjectsdev/runtime-ts/src/ codegen/runtime/` and apply it (or `meta eject --force` for a file you never changed). diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index 6b645ed1f..9cf2b11b7 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -91,7 +91,9 @@ Between 1.0 and 1.1 an owned copy was the only way to run `entity`, `queries`, ` and `barrel`, so many projects carry copies they never changed. A copy like that only falls behind the reference. To drop one: -1. Run `meta eject --list`. A copy marked `identical to the reference` or +1. Run `meta eject --list --format text`. At a terminal `--format text` is already the + default; in a script or an agent's shell the default is a structured document that + lists the generators only. A copy marked `identical to the reference` or `same content as the reference, your formatting` is unmodified. So is one marked `DIFFERS` with `0 line(s) of your own`: it is only behind. A copy with lines of your own is a customization; keep it, or check what the lines are with @@ -99,8 +101,12 @@ falls behind the reference. To drop one: For `entity`, `routes` and `routes-hono`, check the adapter too. Ejecting one also copied the HTTP adapter into `codegen/runtime/`, and a route fix is often made there - with the generator file left untouched. The same listing marks each of those files - `identical`, `DIFFERS` or `not in the package`. The copy is unmodified only when the + with the generator file left untouched. The text listing marks each of those files + `identical`, `DIFFERS` or `not in the package`; `meta gen --list --format json` carries + the same comparison as `source.runtimeCopy[]` on the generator's row, with a `verdict` + per file. A listing with no `codegen/runtime/` rows is not evidence that the adapter is + unmodified: the structured form of `meta eject --list` never prints them. The copy is + unmodified only when the generator file and every `codegen/runtime/` file it reaches ([the table below](#what-meta-eject-hands-over-and-what-stays-core-typescript)) are marked identical. If one differs, the copy carries your change: keep the owned @@ -172,11 +178,12 @@ judges only files `meta gen` wrote. `meta eject --list` and `meta gen --list` compare every file under `codegen/runtime/` with the installed `@metaobjectsdev/runtime-ts` (the same formatting-blind comparison used for -owned generators) and mark it `identical`, `DIFFERS`, or `not in the package`. After -upgrading the package: +owned generators) and mark it `identical`, `DIFFERS`, or `not in the package`. `meta eject +--list` prints those rows in its text form only, so pass `--format text` when not at a +terminal. After upgrading the package: ```bash -meta eject --list # which copied files differ from the new version +meta eject --list --format text # which copied files differ from the new version diff -u node_modules/@metaobjectsdev/runtime-ts/src/drizzle-fastify/filter-parser.ts \ codegen/runtime/drizzle-fastify/filter-parser.ts ``` diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md index 6c0344cf8..2d24e86c2 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -85,21 +85,40 @@ import { routesFile } from "./codegen/generators/routes"; is duplication, and it falls behind the reference: ```bash -meta eject --list +meta eject --list --format text ``` -marks each owned copy `identical to the reference`, `same content as the reference, your -formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is -identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing -its local import with the package import and deleting the file, then `meta gen` and a -review of the diff. A copy with lines of its own is an intentional customization. Check -only how far behind it is. +Pass `--format text`. Away from a terminal the default is a structured document, and +that document holds the generator rows only: it has no `codegen/runtime/` rows at all. + +The text listing marks each owned copy `identical to the reference`, `same content as the +reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those +are the `--format text` labels. In `--format json` or `toon` the same row carries +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is +identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a +**finding**: recommend replacing its local import with the package import and deleting +the file, then `meta gen` and a review of the diff. A copy with lines of its own is an +intentional customization. Check only how far behind it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix -is often made there with the generator file left untouched. The same listing marks each -copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For -these three, the copy is unmodified only when the generator file AND every +is often made there with the generator file left untouched. Read the state of those files +from one of two places: + +- `meta eject --list --format text` prints a block of `codegen/runtime/` rows after the + generators, each marked `identical to the package`, `same content as the package, your + formatting`, `DIFFERS: N line(s) only in the package, M line(s) only in yours`, or + `not in the package`. +- `meta gen --list --format json` puts them on the generator's own row, inside its + `source` object: an owned row has `owned: true` there and a `runtimeCopy` array, one + entry per adapter file that generator reaches, with `path` and `verdict` (`identical`, + `reformatted` or `differs`). + +**A listing with no runtime rows is not evidence that the adapter is unmodified.** If +`codegen/runtime/` exists and the output you read shows no row for its files, you have +not checked: run one of the two commands above. + +For these three, the copy is unmodified only when the generator file AND every `codegen/runtime/` file it reaches are marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md index 15584f858..356232ff2 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md @@ -58,8 +58,9 @@ install what eject prints instead (`qs`, `@types/qs`, the framework, the core What stays a package import is core: `@metaobjectsdev/metadata`, `render`, the reply parser. Options on each ejected generator: `runtimeImport` moves the copy (relative to the output root, like `dbImport`, or a path alias); `runtimeImport: "@metaobjectsdev/runtime-ts"` -goes back to the package. `meta eject --list` and `meta gen --list` mark each copied file -identical to or differing from the installed package; to take an upstream fix, read +goes back to the package. `meta eject --list --format text` and `meta gen --list` mark each +copied file identical to or differing from the installed package (the structured form of +`meta eject --list` omits those rows); to take an upstream fix, read `diff -u node_modules/@metaobjectsdev/runtime-ts/src/ codegen/runtime/` and apply it (or `meta eject --force` for a file you never changed). diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md index 3567085d0..c1e1848c5 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -29,11 +29,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid `codegen/generators/*` (via `meta eject`, or `meta init` on a CLI before 1.0.4), and which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that - falls behind the reference: `meta eject --list` marks each copy, and one with no lines - of its own should be replaced by the package import. For `entity`, `routes` and - `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is - also marked identical: an edited adapter is a customization, and such a copy is not a - finding. **An import of `entityFile` / + falls behind the reference: `meta eject --list --format text` marks each copy, and one + with no lines of its own should be replaced by the package import. For `entity`, + `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy + reaches is also marked identical: an edited adapter is a customization, and such a copy + is not a finding. Those runtime rows appear only with `--format text` (or as the + `runtimeCopy` array inside the row's `source` object in `meta gen --list --format + json`); a listing with no runtime rows is not evidence that the adapter is unmodified. **An import of `entityFile` / `queriesFile` / `routesFile` / `barrel` on a 1.0.x install** fails to load, because 1.0 removed those four and 1.1 restored them: there it is an upgrade blocker, fixed by upgrading to 1.1 or by ejecting. Importing anything from that subpath is otherwise diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md index 6c0344cf8..2d24e86c2 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -85,21 +85,40 @@ import { routesFile } from "./codegen/generators/routes"; is duplication, and it falls behind the reference: ```bash -meta eject --list +meta eject --list --format text ``` -marks each owned copy `identical to the reference`, `same content as the reference, your -formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. A copy that is -identical, or behind with `0 line(s) of your own`, is a **finding**: recommend replacing -its local import with the package import and deleting the file, then `meta gen` and a -review of the diff. A copy with lines of its own is an intentional customization. Check -only how far behind it is. +Pass `--format text`. Away from a terminal the default is a structured document, and +that document holds the generator rows only: it has no `codegen/runtime/` rows at all. + +The text listing marks each owned copy `identical to the reference`, `same content as the +reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those +are the `--format text` labels. In `--format json` or `toon` the same row carries +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is +identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a +**finding**: recommend replacing its local import with the package import and deleting +the file, then `meta gen` and a review of the diff. A copy with lines of its own is an +intentional customization. Check only how far behind it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix -is often made there with the generator file left untouched. The same listing marks each -copied runtime file `identical to the package`, `DIFFERS` or `not in the package`. For -these three, the copy is unmodified only when the generator file AND every +is often made there with the generator file left untouched. Read the state of those files +from one of two places: + +- `meta eject --list --format text` prints a block of `codegen/runtime/` rows after the + generators, each marked `identical to the package`, `same content as the package, your + formatting`, `DIFFERS: N line(s) only in the package, M line(s) only in yours`, or + `not in the package`. +- `meta gen --list --format json` puts them on the generator's own row, inside its + `source` object: an owned row has `owned: true` there and a `runtimeCopy` array, one + entry per adapter file that generator reaches, with `path` and `verdict` (`identical`, + `reformatted` or `differs`). + +**A listing with no runtime rows is not evidence that the adapter is unmodified.** If +`codegen/runtime/` exists and the output you read shows no row for its files, you have +not checked: run one of the two commands above. + +For these three, the copy is unmodified only when the generator file AND every `codegen/runtime/` file it reaches are marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 4c0892f48..27a2cc70b 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when `meta eject --list` marks the generator file and every runtime file it reaches identical. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md index 15584f858..356232ff2 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md @@ -58,8 +58,9 @@ install what eject prints instead (`qs`, `@types/qs`, the framework, the core What stays a package import is core: `@metaobjectsdev/metadata`, `render`, the reply parser. Options on each ejected generator: `runtimeImport` moves the copy (relative to the output root, like `dbImport`, or a path alias); `runtimeImport: "@metaobjectsdev/runtime-ts"` -goes back to the package. `meta eject --list` and `meta gen --list` mark each copied file -identical to or differing from the installed package; to take an upstream fix, read +goes back to the package. `meta eject --list --format text` and `meta gen --list` mark each +copied file identical to or differing from the installed package (the structured form of +`meta eject --list` omits those rows); to take an upstream fix, read `diff -u node_modules/@metaobjectsdev/runtime-ts/src/ codegen/runtime/` and apply it (or `meta eject --force` for a file you never changed). diff --git a/server/typescript/packages/cli/test/skill-catalog-grounding.test.ts b/server/typescript/packages/cli/test/skill-catalog-grounding.test.ts index 5c88b4bd2..b617aac85 100644 --- a/server/typescript/packages/cli/test/skill-catalog-grounding.test.ts +++ b/server/typescript/packages/cli/test/skill-catalog-grounding.test.ts @@ -77,8 +77,9 @@ function namesInCommands(files: readonly string[]): Named[] { const tok = toks[i]!; if (tok.startsWith("--")) { // Skip the flag AND its value: `--format json` must not read `json` as a - // generator name. A `--flag=value` form carries its own value already. - if (!tok.includes("=")) i++; + // generator name. A `--flag=value` form carries its own value already, and + // a flag followed by another flag (`--list --format text`) took no value. + if (!tok.includes("=") && !(toks[i + 1] ?? "").startsWith("--")) i++; continue; } if (tok === "" || PLACEHOLDERS.has(tok)) continue; From 29de0831ae69d38c10d30ded7c24ac12a223ca40 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 09:35:59 -0400 Subject: [PATCH 06/10] no-mistakes(review): Point install set to JSON catalog; name per-port listings Assisted-by: no-mistakes:claude:claude-opus-5-5 --- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 5 +++-- docs/features/own-your-codegen.md | 6 ++++-- docs/ports/typescript.md | 6 ++++-- docs/recipes/document-graph-and-wide-column-stores.md | 3 ++- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 5 +++-- .../metaobjects-codegen/references/owning-generators.md | 2 +- .../references/typescript-document-store.md | 5 +++-- server/typescript/packages/cli/README.md | 6 +++++- 13 files changed, 30 insertions(+), 18 deletions(-) diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md b/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md index 132c83867..ade5d5063 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript-document-store.md @@ -27,8 +27,9 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists - under install, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of + `npx meta gen --list --format json` lists under `install` (the text listing does not + print them), then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index 9cf2b11b7..5dd6374a7 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -56,8 +56,10 @@ Every stock generator can be used two ways: | After a package upgrade | output can change; `meta verify --codegen` reports it until you run `meta gen` | output changes only when your copy does | Start with the import. `meta gen --list` prints the import line under each generator -(`wire.import` in `--format json`), beside what to install and which config keys it reads. -The call is yours to write from each generator's documented options: +(`wire.import` in `--format json`). What to install and which config keys a generator +reads are on the `--format json` row only, as `install.dev`, `install.runtime` and +`configKeys`; the text listing does not print them. The call is yours to write from each +generator's documented options: ```ts import { defineConfig } from "@metaobjectsdev/cli"; diff --git a/docs/ports/typescript.md b/docs/ports/typescript.md index c8ffb0e3a..9a8ad4dbb 100644 --- a/docs/ports/typescript.md +++ b/docs/ports/typescript.md @@ -54,8 +54,10 @@ Two config files, by design: ADR-0034 Amendment 2), and the `.gitignore` entries for `.metaobjects/.gen-state/`. Choose from the catalog with `meta gen --list --probe`, which reports how many files each generator would emit for your model and prints the import line that runs it as -shipped, beside what to install and the config keys it reads. Import the ones you want -from their package: nothing is copied, and an upgrade is how their fixes arrive. To change +shipped. What to install and the config keys a generator reads are in the structured +catalog only: `meta gen --list --format json` carries `install.dev`, `install.runtime` +and `configKeys` on each row, and the text listing prints neither. Import the ones you +want from their package: nothing is copied, and an upgrade is how their fixes arrive. To change what one emits, `meta eject ` copies its reference template into `codegen/generators/` (ADR-0034 scaffold-and-own — yours to edit) and prints the local import to swap in; `meta gen` then runs that copy, not the package's. diff --git a/docs/recipes/document-graph-and-wide-column-stores.md b/docs/recipes/document-graph-and-wide-column-stores.md index 6daac4930..09bb0de99 100644 --- a/docs/recipes/document-graph-and-wide-column-stores.md +++ b/docs/recipes/document-graph-and-wide-column-stores.md @@ -67,7 +67,8 @@ repository. A test keeps the two identical. [`generators/typescript/mongo-repository.ts`](generators/typescript/mongo-repository.ts) is a complete example for the MongoDB Node driver. Copy it into `codegen/generators/`, then wire it next to the stock entity generator, which is imported from its package and run as shipped -(install the packages its `meta gen --list` row lists): +(install the packages its `meta gen --list --format json` row lists under `install`; the text +listing does not print them): ```bash cp /docs/recipes/generators/typescript/mongo-repository.ts codegen/generators/ diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md index 132c83867..ade5d5063 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md @@ -27,8 +27,9 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists - under install, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of + `npx meta gen --list --format json` lists under `install` (the text listing does not + print them), then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 27a2cc70b..20197896b 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | `meta eject --list --format text` (TypeScript) or `gen --list` (other ports) marks each owned copy identical to the reference or differing from it. One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md index 132c83867..ade5d5063 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md @@ -27,8 +27,9 @@ enum fails Zod validation. }); ``` -3. `npm install mongodb` and the packages the `entity` row of `npx meta gen --list` lists - under install, then `npx meta gen`. Use the repository from your routes: +3. `npm install mongodb` and the packages the `entity` row of + `npx meta gen --list --format json` lists under `install` (the text listing does not + print them), then `npx meta gen`. Use the repository from your routes: `const orders = orderRepository(client.db("shop")); await orders.create(body);` What the repository does, built on the generated contract: diff --git a/server/typescript/packages/cli/README.md b/server/typescript/packages/cli/README.md index a3c5def43..e1b78ba4e 100644 --- a/server/typescript/packages/cli/README.md +++ b/server/typescript/packages/cli/README.md @@ -69,6 +69,10 @@ meta gen --list --probe # 4. Wire the ones you want. The catalog prints the import that runs each generator as # shipped; add it to metaobjects.config.ts and call it in `generators: [...]`. +# What to install is on the structured row only (`install.dev`, `install.runtime`), +# beside the config keys the generator reads (`configKeys`): +meta gen --list --format json + # Need to change what one emits? Copy it into your repo and own it. Prints the import # to swap in, the entry to wire, and what to install. meta eject entity @@ -103,7 +107,7 @@ Running `meta` with no arguments prints a concise status line (whether a `metaob 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. -**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. 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. +**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. Flags: - `--force` — overwrite scaffold files (memory records preserved) From 3dd737cbaab9f6f90f8ffbd946a13130da2ff80b Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 09:49:42 -0400 Subject: [PATCH 07/10] no-mistakes(review): Make unmodified-copy rule conservative; finish import-first sweep Assisted-by: no-mistakes:claude:claude-opus-5-5 --- CHANGELOG.md | 8 +++-- .../skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/typescript.md | 27 +++++++++++---- .../references/owning-generators.md | 2 +- docs/features/cube-export.md | 15 ++++----- docs/features/own-your-codegen.md | 33 +++++++++++++------ docs/llms/llms-full.txt | 21 ++++++------ docs/llms/llms.txt | 2 +- docs/ports/typescript-client.md | 24 +++++--------- .../extending-metaobjects-with-providers.md | 8 ++--- docs/recipes/nextjs-vercel.md | 16 ++++----- docs/recipes/wiring-generated-queries.md | 7 ++-- .../.claude/skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/typescript.md | 27 +++++++++++---- .../references/owning-generators.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 24 ++++++++++---- .../references/typescript.md | 27 +++++++++++---- .../references/owning-generators.md | 2 +- .../packages/cli/src/commands/eject.ts | 5 +-- .../packages/test-generators/README.md | 2 +- 26 files changed, 241 insertions(+), 137 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0de09220..e5aa7ae94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,10 @@ here.** - **Nothing changes for a project that owns its copies.** They run as before, and `meta eject --list` and `meta verify --codegen` behave as before. - **To drop a copy you never changed**, replace its local import with the package import, - delete the file and run `meta gen`. `queries` and `barrel` regenerate nothing if the + delete the file and run `meta gen`. Only a copy `meta eject --list --format text` marks + `identical to the reference` is proven unchanged: `0 line(s) of your own` rules out + added or edited lines, not removed or reordered ones, so check the file's history + before dropping a copy with any other mark. `queries` and `barrel` regenerate nothing if the copy matched the reference. `entity`, `routes` and `routes-hono` also move their HTTP-adapter import from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which then has to be installed again. For those three, a copy is unchanged only when @@ -65,7 +68,8 @@ here.** `meta eject`. That import now loads. - **The agent-context skills teach "import by default, eject to customize".** The codegen skill and the always-on context no longer send an agent to eject a generator it is not - going to change. The audit skill reports an unmodified owned copy as a finding, and reads + going to change. The audit skill reports an owned copy it can prove unmodified as a + finding, and reads an import of the four as a blocker only on a 1.0.x install. ### Fixed diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/agent-context/skills/metaobjects-audit/references/typescript.md b/agent-context/skills/metaobjects-audit/references/typescript.md index 2d24e86c2..6981e1b4e 100644 --- a/agent-context/skills/metaobjects-audit/references/typescript.md +++ b/agent-context/skills/metaobjects-audit/references/typescript.md @@ -94,11 +94,24 @@ that document holds the generator rows only: it has no `codegen/runtime/` rows a The text listing marks each owned copy `identical to the reference`, `same content as the reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those are the `--format text` labels. In `--format json` or `toon` the same row carries -`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is -identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a -**finding**: recommend replacing its local import with the package import and deleting -the file, then `meta gen` and a review of the diff. A copy with lines of its own is an -intentional customization. Check only how far behind it is. +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. + +**Report a copy as a finding only when you can prove it unmodified.** + +- `identical to the reference` (`status: identical`) is the only mark that proves it. That + copy is a **finding**: recommend replacing its local import with the package import and + deleting the file, then `meta gen` and a review of the diff. +- `same content as the reference, your formatting` (`reformatted`) and `DIFFERS` with + `0 line(s) of your own` (`differs`, `own: 0`) do not prove it. The comparison ignores + line order, and a line the adopter deleted counts as "behind" exactly like a line + upstream added, so `own: 0` rules out added or edited lines, not removed or reordered + ones. Confirm from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) or from + `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts` + showing only upstream changes. Confirmed, it is the same finding. Not confirmed, report + it as **possibly customized: check its history**, never as a copy to drop. +- A copy with lines of its own is an intentional customization. Check only how far behind + it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix @@ -118,8 +131,8 @@ from one of two places: `codegen/runtime/` exists and the output you read shows no row for its files, you have not checked: run one of the two commands above. -For these three, the copy is unmodified only when the generator file AND every -`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +For these three, the copy is unmodified only when the generator file passes the test +above AND every `codegen/runtime/` file it reaches is marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports `@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/docs/features/cube-export.md b/docs/features/cube-export.md index b9308bc08..4dabcadab 100644 --- a/docs/features/cube-export.md +++ b/docs/features/cube-export.md @@ -5,8 +5,8 @@ joins, dimensions, measures, segments, and a rollup for each served report._ **Status:** a TypeScript reference helper (FR-044 Plan 4; [ADR-0034](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md) Amendment 3). It is -listed by `meta gen --list`, copied into your repo with `meta eject cube-model`, and checked -by `meta verify --codegen`. It is not core: what MetaObjects guarantees is the vocabulary and +listed by `meta gen --list`, imported from its package to run as shipped (or copied into your +repo with `meta eject cube-model` to change it), and checked by `meta verify --codegen`. It is not core: what MetaObjects guarantees is the vocabulary and the loader's checks on it, and a generator that writes files into your repo is a helper you can copy and own. It adds no vocabulary (`metamodelVersion` stays 1.1) and has no runtime: the files it writes import nothing. @@ -55,8 +55,8 @@ that points there: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Copied in by `meta eject cube-model`: yours to edit (ADR-0034). -import { cubeModel } from "./codegen/generators/cube-model.js"; +// Run as shipped. `meta eject cube-model` copies it in when you need to change it. +import { cubeModel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", @@ -66,13 +66,12 @@ export default defineConfig({ }); ``` -`meta eject cube-model` copies one file, `codegen/generators/cube-model.ts`, and prints the -import and the entry to wire. It copies nothing into `codegen/runtime/`, because the output +To change what it emits, `meta eject cube-model` copies one file, +`codegen/generators/cube-model.ts`, and prints the local import to swap in and the entry to wire. It copies nothing into `codegen/runtime/`, because the output imports nothing. What you own in the copy is which entities get a cube, where the files land and the YAML call. The mapping (`buildCubeModel`, which raises every refusal below) and the YAML writer (`renderCubeYaml`) stay in the package, both exported from -`@metaobjectsdev/codegen-ts`. To run the package's own copy, import `cubeModel` from -`@metaobjectsdev/codegen-ts` instead. +`@metaobjectsdev/codegen-ts`. | Option | Meaning | |---|---| diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index 5dd6374a7..3f7e9db16 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -93,13 +93,24 @@ Between 1.0 and 1.1 an owned copy was the only way to run `entity`, `queries`, ` and `barrel`, so many projects carry copies they never changed. A copy like that only falls behind the reference. To drop one: -1. Run `meta eject --list --format text`. At a terminal `--format text` is already the +1. Decide whether the copy carries a change of yours. Run + `meta eject --list --format text`. At a terminal `--format text` is already the default; in a script or an agent's shell the default is a structured document that - lists the generators only. A copy marked `identical to the reference` or - `same content as the reference, your formatting` is unmodified. So is one marked - `DIFFERS` with `0 line(s) of your own`: it is only behind. A copy with lines of your - own is a customization; keep it, or check what the lines are with - `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts`. + lists the generators only. + + - **`identical to the reference` is the only mark that proves a copy unmodified.** Such + a copy can be replaced without looking further. + - **Every other mark needs a look first.** `same content as the reference, your + formatting` comes from a comparison that ignores line order, so a copy whose + statements you reordered reads the same way. `DIFFERS` with `0 line(s) of your own` + rules out lines you added or edited, not lines you removed or reordered: a line you + deleted is counted as "behind", exactly like a line upstream added. Before dropping + a copy with either mark, confirm it from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) or from + the diff against the reference + (`diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts` + shows only upstream changes). + - **A copy with lines of your own is a customization.** Keep it. For `entity`, `routes` and `routes-hono`, check the adapter too. Ejecting one also copied the HTTP adapter into `codegen/runtime/`, and a route fix is often made there @@ -108,9 +119,9 @@ falls behind the reference. To drop one: the same comparison as `source.runtimeCopy[]` on the generator's row, with a `verdict` per file. A listing with no `codegen/runtime/` rows is not evidence that the adapter is unmodified: the structured form of `meta eject --list` never prints them. The copy is - unmodified only when the - generator file and every `codegen/runtime/` file it reaches - ([the table below](#what-meta-eject-hands-over-and-what-stays-core-typescript)) are + unmodified only when the generator file passes the test above and every + `codegen/runtime/` file it reaches + ([the table below](#what-meta-eject-hands-over-and-what-stays-core-typescript)) is marked identical. If one differs, the copy carries your change: keep the owned generator, because it is what points the generated code at your adapter (its `runtimeImport` option), and the packaged generator always imports @@ -130,7 +141,9 @@ What the diff in step 4 holds: - **A copy that matched the reference** regenerates nothing for `queries` and `barrel`. - **A copy that was behind** regenerates the changes it was missing. That is the reference - catching up, so read it the way you would read an upgrade. + catching up, and you can read it the way you would read an upgrade, only once step 1 has + confirmed the copy carried no change of your own. A line you had removed from the copy + comes back in this same diff, and nothing marks it as yours. - **`entity`, `routes` and `routes-hono` also move one import.** An ejected `entity`, `routes` or `routes-hono` points its output at the adapter copy in `codegen/runtime/`, and the packaged generator imports `@metaobjectsdev/runtime-ts`. So those import lines diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt index 34a2f5aa8..41e6dbe68 100644 --- a/docs/llms/llms-full.txt +++ b/docs/llms/llms-full.txt @@ -265,18 +265,16 @@ The project config (`metaobjects.config.ts`) wires up the generators and target Codegen is **opt-in**: `meta init` writes `generators: []`, and no port ships a default suite. `meta gen --list --probe` is the catalog — it runs every generator against your -model and reports how many files each would emit — and `meta eject ...` copies the -ones you choose into your repo, printing the import line and the entry to add. +model and reports how many files each would emit, with the import line that runs it as +shipped. Import the ones you want from their package; `meta eject ...` copies one +into your repo when you need to change what it emits, printing the local import to swap in. ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators — copied in by `meta eject` (ADR-0034 scaffold-and-own). These files -// are in YOUR repo and are yours to edit. The default emit targets Fastify on Node; -// retarget it by editing these, not by switching tools. -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { routesFile } from "./codegen/generators/routes.js"; -import { barrel } from "./codegen/generators/barrel.js"; +// Run as shipped: nothing copied, fixes arrive by upgrading the package. The default +// emit targets Fastify on Node; retarget it by ejecting a generator (ADR-0034 +// scaffold-and-own) and editing your copy, not by switching tools. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "./src/db", @@ -297,8 +295,9 @@ $ meta init # The catalog: every generator by layer, with a file count for YOUR model $ meta gen --list --probe -# Take the ones you want — copies them into codegen/generators/ and says what to wire -$ meta eject entity queries routes barrel +# Wire the ones you want: add each catalog row's import line to metaobjects.config.ts. +# To change what one emits, copy it into codegen/generators/ and own it: +$ meta eject routes # Generate code from exactly what you wired $ meta gen diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt index 521cb672a..9c3c1ee31 100644 --- a/docs/llms/llms.txt +++ b/docs/llms/llms.txt @@ -82,7 +82,7 @@ MetaObjects deliberately does **not** ship one universal binary. Schema operatio ### Commands - `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts`, **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 gen [...]` — TS codegen from entities defined under `metaobjects/`. Supports `--dry-run` and `--watch`. Generators (`entityFile()`, `queriesFile()`, `routesFile()`, `formFile()`, `tanstackQuery()`, `tanstackGrid()`, `barrel()`) are reference templates you own, not framework imports: `meta init` wires none, and `meta eject ...` copies the ones you choose (`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 import and entry to add to `metaobjects.config.ts`. The shipped defaults target Fastify/Drizzle/React/TanStack and are retargeted by editing your copy, not by switching tools. Per-port codegen runs in that port's own build tool: `mvn metaobjects:generate` (Java/Kotlin), `dotnet meta gen` (C#), `metaobjects gen` (Python). +- `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), and `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. 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, and **`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. - `meta migrate --dialect --slug ` — diff metadata vs the committed schema snapshot and emit migration SQL files under `.metaobjects/migrations`; add `--db --apply` to run them. `--dialect` selects the diff pipeline, not just the SQL flavor — required offline and on `baseline`, auto-detected from the URL scheme when `--db` is given. On a brand-new database the first command is `meta migrate --from-db --db --dialect --slug init --apply` (diff against the empty database, emit `CREATE TABLE`, apply, record the snapshot) — **not** `meta migrate baseline`, which is for adopting a database that already has its schema. `meta migrate apply-pending --db --dialect ` replays committed migrations with no diff (fresh DB / CI). Supports SQLite (`file:`, `libsql:`), Postgres (`postgres:`, `postgresql:`), and Cloudflare D1 (TS only). `--dry-run` prints SQL to stdout. **Schema migration is owned by the Node `meta` CLI (ADR-0015) and used by every port regardless of backend language** — a JVM/Python/C# project needs no per-language migrate engine, but DOES need Node (or Bun) available to run it: no pre-built binary is published today. There is no Maven, .NET, or Python migrate command. diff --git a/docs/ports/typescript-client.md b/docs/ports/typescript-client.md index f61de5e21..aa27c548e 100644 --- a/docs/ports/typescript-client.md +++ b/docs/ports/typescript-client.md @@ -79,11 +79,9 @@ fetch URLs (browser-side): ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { routesFile } from "./codegen/generators/routes.js"; -import { barrel } from "./codegen/generators/barrel.js"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid, tanstackGridHook } from "@metaobjectsdev/codegen-ts-tanstack"; @@ -432,11 +430,9 @@ importBase?, outputLayout?, dbImport? }`. ```ts // metaobjects.config.ts (multi-target) import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { formFile } from "@metaobjectsdev/codegen-ts-react"; import { tanstackQuery, tanstackGrid, tanstackGridHook } from "@metaobjectsdev/codegen-ts-tanstack"; @@ -728,11 +724,9 @@ Metadata (same `Author` entity as the React examples above): ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { routesFile } from "./codegen/generators/routes"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, routesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { angularServiceFile, angularFormFile, diff --git a/docs/recipes/extending-metaobjects-with-providers.md b/docs/recipes/extending-metaobjects-with-providers.md index 124a8f3bc..643faaa44 100644 --- a/docs/recipes/extending-metaobjects-with-providers.md +++ b/docs/recipes/extending-metaobjects-with-providers.md @@ -88,11 +88,9 @@ Notable bits: ```ts // metaobjects.config.ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; -import { promptRender } from "@metaobjectsdev/codegen-ts/generators"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel, promptRender } from "@metaobjectsdev/codegen-ts/generators"; import { exampleProvider } from "./src/codegen/example-provider"; export default defineConfig({ diff --git a/docs/recipes/nextjs-vercel.md b/docs/recipes/nextjs-vercel.md index af262f289..d8a33a04a 100644 --- a/docs/recipes/nextjs-vercel.md +++ b/docs/recipes/nextjs-vercel.md @@ -30,11 +30,9 @@ config and none of them is metadata. ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (§6) — `meta init` wires none. -import { entityFile } from "./codegen/generators/entity.js"; -import { queriesFile } from "./codegen/generators/queries.js"; -import { routesFileHono } from "./codegen/generators/routes-hono.js"; -import { barrel } from "./codegen/generators/barrel.js"; +// Run as shipped, imported from the package — `meta init` wires none. §6 covers +// ejecting one when you need to change what it emits. +import { entityFile, queriesFile, routesFileHono, barrel } from "@metaobjectsdev/codegen-ts/generators"; export default defineConfig({ outDir: "src/generated", @@ -57,10 +55,10 @@ export default defineConfig({ }); ``` -The `.js` specifiers in this file's own imports are correct and are **not** what -`extStyle` governs. `metaobjects.config.ts` is loaded by the CLI under Node, never by -Turbopack; `extStyle` decides only how *generated* modules import each other. `meta eject` -prints these lines for you — paste them as given. +`extStyle` does **not** govern this file's own imports. `metaobjects.config.ts` is loaded +by the CLI under Node, never by Turbopack; `extStyle` decides only how *generated* modules +import each other. If you later eject a generator (§6), the local import `meta eject` +prints ends in `.js`: that is correct, so paste it as given. `.meta.ts` deliberately does **not** get the directive. It is plain data with no hooks and no React import — imported *by* a client component, which under RSC is exactly diff --git a/docs/recipes/wiring-generated-queries.md b/docs/recipes/wiring-generated-queries.md index b60e6ed0f..ad15832d7 100644 --- a/docs/recipes/wiring-generated-queries.md +++ b/docs/recipes/wiring-generated-queries.md @@ -267,10 +267,9 @@ Then wire it into `metaobjects.config.ts`: ```ts import { defineConfig } from "@metaobjectsdev/cli"; -// Owned generators, copied in by `meta eject` (ADR-0034 scaffold-and-own; `meta init` wires none). -import { entityFile } from "./codegen/generators/entity"; -import { queriesFile } from "./codegen/generators/queries"; -import { barrel } from "./codegen/generators/barrel"; +// Run as shipped, imported from the package (`meta init` wires none). `meta eject ` +// copies one into ./codegen/generators/ only when you need to change what it emits. +import { entityFile, queriesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; import { honoRoutesFile } from "./metaobjects-routes-hono"; export default defineConfig({ diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md index 2d24e86c2..6981e1b4e 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -94,11 +94,24 @@ that document holds the generator rows only: it has no `codegen/runtime/` rows a The text listing marks each owned copy `identical to the reference`, `same content as the reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those are the `--format text` labels. In `--format json` or `toon` the same row carries -`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is -identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a -**finding**: recommend replacing its local import with the package import and deleting -the file, then `meta gen` and a review of the diff. A copy with lines of its own is an -intentional customization. Check only how far behind it is. +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. + +**Report a copy as a finding only when you can prove it unmodified.** + +- `identical to the reference` (`status: identical`) is the only mark that proves it. That + copy is a **finding**: recommend replacing its local import with the package import and + deleting the file, then `meta gen` and a review of the diff. +- `same content as the reference, your formatting` (`reformatted`) and `DIFFERS` with + `0 line(s) of your own` (`differs`, `own: 0`) do not prove it. The comparison ignores + line order, and a line the adopter deleted counts as "behind" exactly like a line + upstream added, so `own: 0` rules out added or edited lines, not removed or reordered + ones. Confirm from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) or from + `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts` + showing only upstream changes. Confirmed, it is the same finding. Not confirmed, report + it as **possibly customized: check its history**, never as a copy to drop. +- A copy with lines of its own is an intentional customization. Check only how far behind + it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix @@ -118,8 +131,8 @@ from one of two places: `codegen/runtime/` exists and the output you read shows no row for its files, you have not checked: run one of the two commands above. -For these three, the copy is unmodified only when the generator file AND every -`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +For these three, the copy is unmodified only when the generator file passes the test +above AND every `codegen/runtime/` file it reaches is marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports `@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md index c1e1848c5..4d038bd0b 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -30,7 +30,13 @@ Actual cutovers run through the existing skills mapped per finding tier (§ Brid which does it import from `@metaobjectsdev/codegen-ts/generators`? Both are supported from 1.1. Two findings to look for. **An unmodified owned copy** is duplication that falls behind the reference: `meta eject --list --format text` marks each copy, and one - with no lines of its own should be replaced by the package import. For `entity`, + marked `identical to the reference` should be replaced by the package import. That is + the only mark that proves a copy unmodified. `same content as the reference, your + formatting` and `DIFFERS` with `0 line(s) of your own` rule out added or edited lines, + not removed or reordered ones: confirm those from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) first, and + report a copy you cannot confirm as "possibly customized: check its history", not as a + copy to drop. For `entity`, `routes` and `routes-hono` that holds only when every `codegen/runtime/` file the copy reaches is also marked identical: an edited adapter is a customization, and such a copy is not a finding. Those runtime rows appear only with `--format text` (or as the @@ -388,16 +394,20 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend ## Owned-codegen & scaffold-and-own assessment -- If config imports `entityFile` / `queriesFile` / `routesFile` / `barrel` from - `@metaobjectsdev/codegen-ts/generators` instead of owned `codegen/generators/*`, - recommend the scaffold-and-own migration (`meta init`, or `meta eject `) — on 1.0+ - those four no longer resolve. Importing the non-ownable generators from that subpath is - correct and is not a finding. +- A config may import `entityFile` / `queriesFile` / `routesFile` / `barrel` from + `@metaobjectsdev/codegen-ts/generators` or from owned `codegen/generators/*`. Both are + supported from 1.1, and the import is the right choice for a generator the project does + not change. The import is a blocker only on a 1.0.x install, where those four do not + resolve: recommend upgrading to 1.1, or `meta eject ` if the project has to stay + on 1.0.x. On 1.1 the finding is the reverse case, an owned copy proven unmodified by + the Phase 0 owned-generators check: recommend the package import. Importing any other + generator from that subpath is correct and is not a finding. - Audit owned generators: (a) regenerate clean? (b) drifted from reference templates — intentional (good) vs stale/accidental (missed upstream fix)? (c) hand-rolling a walk that a declarative `scope` + `outputPattern` could replace? (d) bespoke shape better as a `templateGenerator` than a forked generator? -- **Authoring ladder:** built-in fits → use it · close → **own + customize** (the default) +- **Authoring ladder:** built-in fits → import it as shipped · close → **eject it and + customize the copy** · new shape → **author a declarative template-spec / custom generator from the metadata** · genuinely un-modelable → hand-write (still import the generated types). - **Generator-gap check:** missing generators that block the biggest wins? Recommend per gap: diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md index 2d24e86c2..6981e1b4e 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md @@ -94,11 +94,24 @@ that document holds the generator rows only: it has no `codegen/runtime/` rows a The text listing marks each owned copy `identical to the reference`, `same content as the reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those are the `--format text` labels. In `--format json` or `toon` the same row carries -`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. A copy that is -identical or reformatted, or behind with `0 line(s) of your own` (`own: 0`), is a -**finding**: recommend replacing its local import with the package import and deleting -the file, then `meta gen` and a review of the diff. A copy with lines of its own is an -intentional customization. Check only how far behind it is. +`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. + +**Report a copy as a finding only when you can prove it unmodified.** + +- `identical to the reference` (`status: identical`) is the only mark that proves it. That + copy is a **finding**: recommend replacing its local import with the package import and + deleting the file, then `meta gen` and a review of the diff. +- `same content as the reference, your formatting` (`reformatted`) and `DIFFERS` with + `0 line(s) of your own` (`differs`, `own: 0`) do not prove it. The comparison ignores + line order, and a line the adopter deleted counts as "behind" exactly like a line + upstream added, so `own: 0` rules out added or edited lines, not removed or reordered + ones. Confirm from the file's history + (`git log -p -- codegen/generators/.ts` shows no edit after the eject) or from + `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts` + showing only upstream changes. Confirmed, it is the same finding. Not confirmed, report + it as **possibly customized: check its history**, never as a copy to drop. +- A copy with lines of its own is an intentional customization. Check only how far behind + it is. **`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix @@ -118,8 +131,8 @@ from one of two places: `codegen/runtime/` exists and the output you read shows no row for its files, you have not checked: run one of the two commands above. -For these three, the copy is unmodified only when the generator file AND every -`codegen/runtime/` file it reaches are marked identical. If any of those files differs or +For these three, the copy is unmodified only when the generator file passes the test +above AND every `codegen/runtime/` file it reaches is marked identical. If any of those files differs or is not in the package, the copy carries a customization: it is **not a finding**, and do not recommend the package import. The packaged generator imports `@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 20197896b..797499f40 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). One with no lines of your own is unmodified. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/server/typescript/packages/cli/src/commands/eject.ts b/server/typescript/packages/cli/src/commands/eject.ts index 5e85484bd..a4f5a092a 100644 --- a/server/typescript/packages/cli/src/commands/eject.ts +++ b/server/typescript/packages/cli/src/commands/eject.ts @@ -1,8 +1,9 @@ // FR-040 §4.2(a) — `meta eject ...` takes ownership of any reference-template // generator, in any package, at any time after `meta init`. // -// Under opt-in codegen this is THE copy door: `meta init` scaffolds the layout and an -// empty selection, so every generator an adopter runs arrives through here. It takes +// This is the copy door, for a generator an adopter wants to CHANGE: `meta init` +// scaffolds the layout and an empty selection, and a generator run as shipped is +// imported from its package without coming through here (ADR-0034 Amendment 5). It takes // MANY names, because a real selection is several — choosing a client tier is // `form hooks grid` — and three separate invocations produce three separate install // lines for the same package. The consolidated install set is the point. diff --git a/server/typescript/packages/test-generators/README.md b/server/typescript/packages/test-generators/README.md index 420b572e9..36c971e52 100644 --- a/server/typescript/packages/test-generators/README.md +++ b/server/typescript/packages/test-generators/README.md @@ -1,7 +1,7 @@ # @metaobjectsdev/test-generators (private, never published) This repo's **owned copies** of the ADR-0034 reference generators — the same files -`meta init` scaffolds into a consumer's `codegen/generators/`. +`meta eject` copies into a consumer's `codegen/generators/`. Suites that need a generator set to drive `runGen` import them from here, which means they exercise the artifact an adopter who EJECTS actually runs. (This package dates from 1.0, From 01eb3aa767bff628f49cbe3188f587935cd15ee4 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 09:54:56 -0400 Subject: [PATCH 08/10] no-mistakes(review): Scope identical-proves-unmodified to TypeScript in owning-generators skill Assisted-by: no-mistakes:claude:claude-opus-5-5 --- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- .../skills/metaobjects-codegen/references/owning-generators.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md +++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md index 797499f40..c34159b84 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md +++ b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md @@ -52,7 +52,7 @@ project misses fixes it would have received by upgrading. |---|---| | A stock generator's output, unchanged | Wire it from its package and copy nothing. TypeScript: the import line on its `meta gen --list` row, for example `import { entityFile } from "@metaobjectsdev/codegen-ts/generators";`. Other ports: select it by its stable name. | | To change what a stock generator emits | Eject it, edit the copy, and point the config at the copy. On TypeScript the factory keeps its name, so that is one import line. | -| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). Only a copy marked identical is proven unmodified. One marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | +| To stop carrying a copy you never changed | Each port's listing marks every owned copy identical to the reference or differing from it: `meta eject --list --format text` (TypeScript), `metaobjects gen --list` (Python), `dotnet meta gen --list` (C#), `mvn metaobjects:eject -Dlist` (Java, Kotlin). On TypeScript, only a copy marked identical is proven unmodified: there the mark means byte-for-byte equal. On Python, C# and Java/Kotlin, identical compares lines without regard to their order, so it rules out added, edited and removed lines but not reordered ones: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On every port, one marked as the same content in your formatting, or as differing with no lines of your own, may still carry lines you removed or reordered, because that count rules out added or edited lines only: confirm from the file's history (`git log -p` on the copy shows no edit after the eject) before dropping it. On TypeScript, replace its local import with the package import, delete the file, run `meta gen` and review the diff. `entity`, `routes` and `routes-hono` also own the HTTP adapter under `codegen/runtime/`: treat one as unmodified only when the generator file and every runtime file it reaches are marked identical. The runtime rows are printed by `meta eject --list --format text`, and carried as the `runtimeCopy` array (a `verdict` per file) inside the `source` object of the generator's row in `meta gen --list --format json`. Away from a terminal, `meta eject --list` without `--format text` prints generator rows only, and a listing with no runtime rows is not evidence that the adapter is unmodified. If a runtime file differs, your change lives there, so keep the owned generator, which is what points the output at that adapter. | A generator you import is still a reference helper: its output can change when you upgrade, and `verify --codegen` reports that as drift until you regenerate. If output has to hold From f8b615a680a10324447455959352876fed610cc3 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 12:45:12 -0400 Subject: [PATCH 09/10] docs(changelog): move the stock-generator entries into the 1.1.0 section 1.1.0 has not shipped: the latest public release is 1.0.13, and the dated [1.1.0] heading is the prepared release, not a published one. The entries this branch added under [Unreleased] describe work that ships in 1.1.0, and the docs, skills and ADR all say "restored in 1.1", so they belong in that section. The Added entries go to the top of 1.1.0's Added list and the Changed entries to its plain Changed list. [Unreleased] keeps the two fixes that were there before this branch. --- CHANGELOG.md | 119 ++++++++++++++++++++++++--------------------------- 1 file changed, 57 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5aa7ae94..70084a940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,68 +10,6 @@ here.** ## [Unreleased] -### Added - -- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again - (TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from - `@metaobjectsdev/codegen-ts/generators` once more, beside `namesFile`, `routesFileHono` - and the prompt tier. 1.0 had removed those four so that an owned copy was the only way to - run them. An assessment of an adopter found its owned copies were almost pure duplication - of the reference and had already fallen behind it, so the rule is now: **import a stock - generator to run it as shipped, and `meta eject` it when you need to change what it - emits.** Both paths are supported and tested, and the factory keeps its name in an - ejected copy, so moving between them is one import line. Ruling and reasoning: - ADR-0034 Amendment 5. - - **Nothing changes for a project that owns its copies.** They run as before, and - `meta eject --list` and `meta verify --codegen` behave as before. - - **To drop a copy you never changed**, replace its local import with the package import, - delete the file and run `meta gen`. Only a copy `meta eject --list --format text` marks - `identical to the reference` is proven unchanged: `0 line(s) of your own` rules out - added or edited lines, not removed or reordered ones, so check the file's history - before dropping a copy with any other mark. `queries` and `barrel` regenerate nothing if the - copy matched the reference. `entity`, `routes` and `routes-hono` also move their - HTTP-adapter import from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which - then has to be installed again. For those three, a copy is unchanged only when - `meta eject --list --format text` also marks every `codegen/runtime/` file it reaches - identical: if you edited the adapter, keep the owned generator. Those rows are printed - in the text form only (`meta gen --list --format json` carries them as - `source.runtimeCopy[]`), so a listing without them is not evidence that the adapter is - unchanged. Steps and the diff to expect: - [own-your-codegen](docs/features/own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). - - **An imported generator is still a reference helper.** Its output is not covered by the - compatibility policy and can change in a MINOR; `meta verify --codegen` reports that - until you regenerate. Own the copy if output has to hold still across upgrades. -- **`meta gen --list` prints how to import each generator.** Every row outside the `docs` - layer shows the import line that runs the generator as shipped, and `--format json` - carries it as `wire.import`. The catalog prints the import only: each generator's - documented options say how to call it. A gate imports each package's factory module and - requires the name the catalog prints, and requires an ejectable generator's reference - template to export the same symbol. -- **`callable` and `trace-helper` can be ejected (TypeScript).** Both gained a reference - template, so `meta eject callable` and `meta eject trace-helper` copy a generator you - own: which objects get a module and where it lands. The module body stays in the package - (`renderCallableFile`, `renderTraceHelper`, now exported from `@metaobjectsdev/codegen-ts`), - the same split the prompt tier uses. The rule is that every stock generator can be - ejected except the `template` primitive, which has no emit logic of its own to own. Not - yet ejectable in this change: `shared-model` and the docs tier (`docs`, `api-docs`, - `mermaid-er`) in TypeScript, and `entity` and `extractor` in Java. - -### Changed - -- **`meta init`'s scaffolded `metaobjects.config.ts` shows the import first.** The selection - is still empty (`generators: []`), and an existing project is not touched. The comment - now shows the package import as the way to wire a generator and `meta eject` as the way to - change one. `meta init`'s next steps and `meta gen`'s "no generators are wired" help say - the same. -- **The config loader no longer rejects an import of the four generators.** 1.0.x answered - `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"` with a message naming - `meta eject`. That import now loads. -- **The agent-context skills teach "import by default, eject to customize".** The codegen - skill and the always-on context no longer send an agent to eject a generator it is not - going to change. The audit skill reports an owned copy it can prove unmodified as a - finding, and reads - an import of the four as a blocker only on a 1.0.x install. - ### Fixed - `meta verify --docs` now reports a generated page in the docs root that no current object @@ -166,6 +104,51 @@ generator catalog and the Atlas evaluation are 1.2 work. `GeneratorRegistry.GeneratorInfo.ejectWith()`, the classes eject copies with a generator. +- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again + (TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from + `@metaobjectsdev/codegen-ts/generators` once more, beside `namesFile`, `routesFileHono` + and the prompt tier. 1.0 had removed those four so that an owned copy was the only way to + run them. An assessment of an adopter found its owned copies were almost pure duplication + of the reference and had already fallen behind it, so the rule is now: **import a stock + generator to run it as shipped, and `meta eject` it when you need to change what it + emits.** Both paths are supported and tested, and the factory keeps its name in an + ejected copy, so moving between them is one import line. Ruling and reasoning: + ADR-0034 Amendment 5. + - **Nothing changes for a project that owns its copies.** They run as before, and + `meta eject --list` and `meta verify --codegen` behave as before. + - **To drop a copy you never changed**, replace its local import with the package import, + delete the file and run `meta gen`. Only a copy `meta eject --list --format text` marks + `identical to the reference` is proven unchanged: `0 line(s) of your own` rules out + added or edited lines, not removed or reordered ones, so check the file's history + before dropping a copy with any other mark. `queries` and `barrel` regenerate nothing if the + copy matched the reference. `entity`, `routes` and `routes-hono` also move their + HTTP-adapter import from `codegen/runtime/` back to `@metaobjectsdev/runtime-ts`, which + then has to be installed again. For those three, a copy is unchanged only when + `meta eject --list --format text` also marks every `codegen/runtime/` file it reaches + identical: if you edited the adapter, keep the owned generator. Those rows are printed + in the text form only (`meta gen --list --format json` carries them as + `source.runtimeCopy[]`), so a listing without them is not evidence that the adapter is + unchanged. Steps and the diff to expect: + [own-your-codegen](docs/features/own-your-codegen.md#replacing-an-unmodified-owned-copy-with-the-import). + - **An imported generator is still a reference helper.** Its output is not covered by the + compatibility policy and can change in a MINOR; `meta verify --codegen` reports that + until you regenerate. Own the copy if output has to hold still across upgrades. +- **`meta gen --list` prints how to import each generator.** Every row outside the `docs` + layer shows the import line that runs the generator as shipped, and `--format json` + carries it as `wire.import`. The catalog prints the import only: each generator's + documented options say how to call it. A gate imports each package's factory module and + requires the name the catalog prints, and requires an ejectable generator's reference + template to export the same symbol. +- **`callable` and `trace-helper` can be ejected (TypeScript).** Both gained a reference + template, so `meta eject callable` and `meta eject trace-helper` copy a generator you + own: which objects get a module and where it lands. The module body stays in the package + (`renderCallableFile`, `renderTraceHelper`, now exported from `@metaobjectsdev/codegen-ts`), + the same split the prompt tier uses. The rule is that every stock generator can be + ejected except the `template` primitive, which has no emit logic of its own to own. Not + yet ejectable in this change: `shared-model` and the docs tier (`docs`, `api-docs`, + `mermaid-er`) in TypeScript. The Java `entity` and `extractor` generators became + ejectable in ADR-0034 Amendment 6. + - **Agent skills: a cost test for owned generators.** The `metaobjects-codegen` skill now says when not to write a generator: only when generator code plus its generated output and added metadata is fewer lines than the hand-written code it replaces, more instances of the shape are expected, or drift protection is the @@ -569,6 +552,18 @@ until you regenerate. With neither or both declared, the goal used to resolve these two names to Kotlin, the only port that had them; it now refuses with `ejectable on more than one port` and asks for `-Dport=java|kotlin`, as it already did for `names` and `requirement-tests`. +- **`meta init`'s scaffolded `metaobjects.config.ts` shows the import first.** The selection + is still empty (`generators: []`), and an existing project is not touched. The comment + now shows the package import as the way to wire a generator and `meta eject` as the way to + change one. `meta init`'s next steps and `meta gen`'s "no generators are wired" help say + the same. +- **The config loader no longer rejects an import of the four generators.** 1.0.x answered + `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"` with a message naming + `meta eject`. That import now loads. +- **The agent-context skills teach "import by default, eject to customize".** The codegen + skill and the always-on context no longer send an agent to eject a generator it is not + going to change. The audit skill reports an owned copy it can prove unmodified as a + finding, and reads an import of the four as a blocker only on a 1.0.x install. - **TypeScript: `RequirementTestArgs` and `RequirementView` gained required fields — a compile break for code that builds them by hand.** `RequirementTestArgs` has six new required fields (`package`, `unit`, `id`, `witnessKey`, `skip`, `digest`) and From f2a42163db1a70eb2a0ed604b568a8c265adda99 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 11 Oct 2026 13:02:13 -0400 Subject: [PATCH 10/10] no-mistakes(document): Fix stale generator count in nextjs recipe --- docs/recipes/nextjs-vercel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes/nextjs-vercel.md b/docs/recipes/nextjs-vercel.md index d8a33a04a..e4946a27e 100644 --- a/docs/recipes/nextjs-vercel.md +++ b/docs/recipes/nextjs-vercel.md @@ -203,7 +203,7 @@ Everything above is configuration. When you need different *output* — a differ shape, a different form library, an auth wrapper around every handler — take the generator: ```bash -meta eject --list # all ten reference templates, grouped by package +meta eject --list # every stock generator, grouped by package meta eject routes-hono # → codegen/generators/routes-hono.ts meta eject form # → codegen/generators/form.ts meta eject hooks # → codegen/generators/hooks.ts