Repository navigation
feat: add brownfield recipe for moving existing models into MetaObjects - #518
Merged
Merged
Conversation
Readers with Pydantic, Zod or Drizzle models could not see how to adopt
MetaObjects without a rewrite. docs/features/adopting-existing-models.md
takes each one from writing the model, through generating beside the
existing code and proving the types match, to moving call sites one at a
time and retiring the old code.
The proof is a test file that tsc or pytest runs, so nothing reads the
adopter's source and no new command is needed. TypeScript compares field
by field, because mutual assignability passes `any` and an optional field
on one side. Python compares model_json_schema() in both modes. Column
names and Zod rules are invisible to tsc, so `meta verify --db` and
z.toJSONSchema cover those.
examples/brownfield/ holds a project for each, run end to end with
deliberate mismatches by brownfield-recipe.test.ts and
test_brownfield_recipe.py; the database half joins the real-PG lanes. The
recipe ships in the metaobjects-authoring skill.
The Pydantic proof found a real bug: an array field's default generated
`tags: list[str] = '{}'`. Python now writes a list, from the same four
spellings TypeScript accepts.
Review of the recipe found Python's array default parser wrote nan and inf into generated source, read JSON null as 'None', and dropped an empty default for subtypes with no element parser. The Zod rules proof failed on field order alone and compared only input.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
Readers with existing Pydantic, Zod or Drizzle models could not see how to move them into a MetaObjects model without a rewrite.
docs/features/adopting-existing-models.mdtakes each kind of model from writing the model, through generating beside the existing code and proving the types match, to moving one call site at a time and retiring the old code. It ships in themetaobjects-authoringskill, and a test keeps the two copies identical.examples/brownfield/{drizzle,zod,pydantic}, each run end to end bybrownfield-recipe.test.tsandtest_brownfield_recipe.py, including deliberate mismatches that must fail. A newbrownfield recipe real-PG checkstep runs themeta verify --dbhalf in the real-Postgres lanes.field.string[]: { default: "{}" }generatedtags: list[str] = '{}'. It now generates a list. Native YAML list defaults keep working, and a default Python cannot spell is left to the database.gate_conf_tsnow gives the codegen-ts suite the 30s limit the other suites use, because a real compile test timed out at bun's default 5s under load. An unused import is gone fromapi_docs_builder.py, and thefield.enumrow infield-types.mdnow states what Python generates.No new CLI flag
The recipe needs none. MetaObjects does not read the adopter's source: the language's own tools do the comparison, from a test file the recipe writes and later deletes.
tscover a smallDifferstype. It compares field by field, because plain mutual assignability passesanyand an optional field present on one side.model_json_schema()in both modes. mypy cannot do this, because two Pydantic classes are different types whatever their fields are.tsccannot see: column names go tometa verify --db, and Zod rules such as.max()go to az.toJSONSchemacomparison.Before you merge
pgEnumcolumns, Python enum member names and custom validators.tsccannot see.Validation
Risk Assessment
✅ Low: Docs, examples and a narrow Python array-default fix. The fixer's native-list handling reuses the existing element parsers and refuses nested, null and non-finite shapes, and the mirrored recipe copies are byte-identical to the source.
Testing
Drove the real Python
metaobjects genCLI on a temp copy of examples/brownfield/pydantic. String default "{}" emitslist[str] = [], native default [a, b] emits["a", "b"], and [] emits= []. The equivalence proof passes on the unmodified example and fails on drifted metadata, as intended. The Zod recipe ran through brownfield-recipe.test.ts (CLI run plus tsc); its Drizzle half was skipped for lack of Postgres. Full Python suite with integration extras passed (4193), excluding Docker-backed suites. SDK agent-context mirror tests passed (90). The full Python suite run is a test-suite result, not a live product run, so it is recorded as untested for live purposes.tags: list[str] = []tags: list[str] = ["a", "b"]on Order and OrderCreatetags: list[str] = []Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed ✅
server/python/src/metaobjects/codegen/runtime/shared_predicates.py:109- array_default() returns None for any non-string @default, and has_literal_default() then treats the field as having no Python default. Its docstring (line 104) says the metadata value is always a string, but that is false: the loader keeps a native YAML list, because field.json gives @default no fixed subType. Verified by generating examples/brownfield/pydantic withfield.string[]: { name: tags, required: true, default: [a, b] }. Base 1d2943f emittedtags: list[str] = ['a', 'b']on both Order and OrderCreate, which was correct. Head emitstags: list[str](required, no default) on Order andtags: list[str] | None = Noneon OrderCreate, with no error or warning.default: []loses its default the same way. So this fix silently removes a default that worked before and turns a defaulted read field into a required one, and the proof then reports a difference the model looks like it already answers. Fix: in array_default, take a listrawaselementsand run it through the same element parsers. Correct the docstring and add native-list cases to test_array_default_is_a_python_list_not_the_metadata_string. Also touches the same contract: entity_model.py _field_line and _create_field_line (both gate on has_literal_default), extractor_generator.py:98 (same predicate), and the CHANGELOG.md Fixed entry and adopting-existing-models.md 'must be a string' row, which should stay true after the fix.docs/features/adopting-existing-models.md:68- The recipe never states the generate command or the TypeScript install. Step 2 and the 'Generate beside ...' lines show only a config. A grep of the file finds nometa gen, nometaobjects genand nonpm install, although Pydantic gets apip installline and later steps givenpx tsc --noEmitandnpx meta verify. An agent following the recipe on a fresh project must work out that it needs@metaobjectsdev/cli(plus@metaobjectsdev/codegen-tsfor the Zod config, which imports it directly) and thennpx meta gen/metaobjects gen. Add both to each section. The text below '## The recipe' is byte-mirrored, so the same edit must go to agent-context/skills/metaobjects-authoring/references/adopting-existing-models.md and the five fixtures/agent-context-conformance copies. That is why this was not fixed inline.🔧 Fix applied.
✅ Re-checked - no issues remain.
🔧 **Test** - 1 issue found → auto-fixed ✅
[a, b]gives tags: list[str] = ["a", "b"] on Order and create model;[]gives []; "{}" and "{a,b}" still give [] and ["a", "b"]scripts/ci-local.sh --only ts-slowto cover it.scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsfull run, exit 0 (build+typecheck, TS conformance, TS unit suites, mutation gate score 54.62 vs break threshold 35, about 10 min)cd server/typescript/packages/cli && bun test test/integration/brownfield-recipe.test.ts(10 pass, 1 skip: real-Postgres case)cd server/typescript/packages/sdk && bun test test/agent-context(90 pass, includes recipe-mirrors)cd server/python && uv run pytest tests/codegen/test_brownfield_recipe.py tests/codegen/test_entity_model.py(47 pass)Live: copied examples/brownfield/pydantic to a temp dir, set the tags array default to[a, b],[],"{}","{a,b}", ran the realmetaobjects gen, inspected generated Order.pydiff of the '## The recipe' onward text between docs/features/adopting-existing-models.md and the agent-context skill reference (identical); install and gen lines presentcd server/python && uv run pytest --continue-on-collection-errorsfull Python suite: 4024 pass, 3 skip, 1 fail + 18 collection errors, all from optional deps (fastapi, pg8000) missing in the sandbox venv🔧 Fix applied.
✅ Re-checked - no issues remain.
tags: list[str] = []tags: list[str] = ["a", "b"]on Order and OrderCreatetags: list[str] = []scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainscd server/typescript/packages/cli && bun test test/integration/brownfield-recipe.test.ts (10 pass, 1 skip)cd server/python && uv run --extra dev pytest tests/codegen/test_brownfield_recipe.py tests/codegen/test_entity_model.py (47 passed)Live: temp copy of examples/brownfield/pydantic; uv run --project server/python metaobjects gen with string default, native [a, b], and []Live: pytest proof/test_equivalence.py on unmodified example (8 passed) and drifted metadata (2 failed)cd server/python && uv run --extra integration --extra dev pytest tests/ -n 8 -k 'not persistence and not api_contract' (4193 passed)cd server/typescript/packages/sdk && bun test test/agent-context (90 pass)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.