Skip to content

fix(codegen-ts): derived flattened parent produces no flat member keys - #539

Merged
dmealing merged 2 commits into
mainfrom
fm/mo-524-derived-flat-parent
Nov 6, 2026
Merged

dmealing merged 2 commits into
mainfrom
fm/mo-524-derived-flat-parent

Conversation

@dmealing

@dmealing dmealing commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • flatSurfaceFields and flattenedMembersOf in codegen-ts src/flattened.ts now return no members for a flattened field.object that is derived (carries an origin.*). The flat keys (for example shipToStreet) no longer appear in the read, create and update zod schemas, filter and sort allowlists, OpenAPI bodies, or form. Matches the Drizzle table and meta migrate, which already create no columns for that parent.
  • docs/features/field-types.md states that a derived flattened field owns no columns and exposes no flat keys, and that a write cannot set them. CHANGELOG.md gains a Fixed entry for the change.
  • test/flattened-storage-e2e.test.ts adds a case with a derived flattened parent. It asserts the surface field list, the Drizzle table columns (id, owner_id, ref), and that no generated Parcel file mentions the flat shipTo keys.
Validation

Risk Assessment

✅ Low: One early-return in flattenedMembersOf makes flatSurfaceFields, the Drizzle table, entity-file and every schema/allowlist consumer treat a derived flattened parent as a single non-flattened field, matching drizzle-schema and migrate's expected-schema; the new e2e test exercises the real generator output.

Testing

Drove the real worktree CLI on a disposable project plus OpenAPI and UI descriptor probes. The derived flattened parent produced no flat keys anywhere after the fix, while the old code leaked them. The non-derived flattened control kept its columns and keys. The new regression test fails before the fix and passes after it. The extra ci-local.sh --quick run was killed at its background time limit and gave no result either way; I did not restart it. The regression evidence is the pipeline's baseline TS fast and unit run, which passed earlier, plus the targeted test file. Other-language ports and the docker suite were not run; the change touches TypeScript codegen only. No UI is involved, so no screenshots were taken. The worktree is clean and the disposable project was removed.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
meta gen on a model with a derived flattened parent (Parcel.shipTo) emits no shipToStreet/shipToCity in write-table, Zod, queries or routes ✅ pass live Ran the worktree CLI (bun server/typescript/packages/cli/bin/meta.ts gen) in a disposable project. Generated Parcel.ts has no flat keys. ParcelInsertSchema and ParcelUpdateSchema hold only ref and o…
A non-derived flattened parent (Owner.home) still gets flat columns, Zod keys and allowlists (control) ✅ pass live Generated Owner.ts has homeStreet/homeCity columns, create and update Zod keys, and the homeCity filter/sort allowlist.
OpenAPI bodies and UI descriptor for the derived-parent entity carry no flat keys; the pre-fix code leaked them (adversarial before/after) ✅ pass live Probe test calling buildOpenApiDocument and buildEntityUiDescriptor on the disposable model. After the fix: ParcelCreate and ParcelUpdate have [ref, ownerId], no shipTo* query params, UI fields [id, r…
Regression: new test in flattened-storage-e2e.test.ts fails on the old code and passes on the fix ✅ pass live bun test test/flattened-storage-e2e.test.ts gives 21 pass with the fix. With the old flattened.ts it fails: flatSurfaceFields returned shipToStreet and shipToCity instead of shipTo.
  • Evidence: Generated output after fix (derived flattened parent) (local evidence, not published)
Evidence: ci-local --quick log (partial; run was killed at its time limit, no EXIT line)
metaobjects local CI  (mode: quick — TS + drift gates only)

── ▶ leak-scan (security) ──────────────────────────────────────────────
leak-scan: clean
── ✓ leak-scan (security) (0s)

── ▶ pom-version parity ──────────────────────────────────────────────
check-pom-versions: ✓ 3 parent + 1 property pom(s) match reactor 8.2.0
── ✓ pom-version parity (0s)

── ▶ bun-version parity ──────────────────────────────────────────────
check-bun-version: bun 1.3.14 matches the workflow pin.
── ✓ bun-version parity (0s)

── ▶ uv.lock version parity ──────────────────────────────────────────────
check-uv-lock-version: uv.lock agrees with pyproject.toml (1.2.0)
── ✓ uv.lock version parity (0s)

── ▶ publish-intent parity ──────────────────────────────────────────────
publish-intent parity: OK (lockstep 1.2.0; 2 declared source-only)
── ✓ publish-intent parity (0s)

── ▶ publish-set parity ──────────────────────────────────────────────
   1. @metaobjectsdev/metadata  (server/typescript/packages/metadata)
   2. @metaobjectsdev/render  (server/typescript/packages/render)
   3. @metaobjectsdev/docs-site  (server/typescript/packages/docs-site)
   4. @metaobjectsdev/migrate-ts  (server/typescript/packages/migrate-ts)
   5. @metaobjectsdev/codegen-ts  (server/typescript/packages/codegen-ts)
   6. @metaobjectsdev/runtime-ts  (server/typescript/packages/runtime-ts)
   7. @metaobjectsdev/sdk  (server/typescript/packages/sdk)
   8. @metaobjectsdev/runtime-web  (client/web/packages/runtime-web)
   9. @metaobjectsdev/codegen-ts-react  (server/typescript/packages/codegen-ts-react)
  10. @metaobjectsdev/codegen-ts-tanstack  (server/typescript/packages/codegen-ts-tanstack)
  11. @metaobjectsdev/react  (client/web/packages/react)
  12. @metaobjectsdev/tanstack  (client/web/packages/tanstack)
  13. @metaobjectsdev/cli  (server/typescript/packages/cli)
  14. @metaobjectsdev/ai-runtime  (server/typescript/packages/ai-runtime)
publish set: OK (lockstep 1.2.0; 14 packages, tier-ordered, closed over sibling deps)
ok:   happy path: private + off-lockstep excluded, order [metadata, render, docs-site, cli]
ok:   untiered member: threw on "publish order undeclared"
ok:   unclosed set: threw on "not closed over its own runtime dependencies"
ok:   inverted tier order: threw on "publishes a dependency AFTER its dependent"
ok:   regression: docs-site (#2) publishes before cli (#12) in the real tree
ok:   regression: docs-site has a declared tier

publish-set tests: all passed
── ✓ publish-set parity (1s)

── ▶ no committed pre-release version ──────────────────────────────────────────────
check-no-prerelease-versions: ✓ no pre-release version in any version declaration
── ✓ no committed pre-release version (0s)

── ▶ script-name hook collisions ──────────────────────────────────────────────
script-name-hooks: ✓ 10 root scripts, no lifecycle-hook collisions
── ✓ script-name hook collisions (0s)

── ▶ ci lane selection ──────────────────────────────────────────────
ok:   [server/typescript/packages/cli/src/x.ts] -> 'ts'
ok:   [client/web/packages/react/src/y.tsx] -> 'ts'
ok:   [server/java/metadata/src/main/java/A.java] -> 'java'
ok:   [server/java/codegen-kotlin/src/main/kotlin/B.kt] -> 'java'
ok:   [server/python/src/metaobjects/loader.py] -> 'python'
ok:   [server/csharp/MetaObjects/Loader.cs] -> 'csharp'
ok:   [fixtures/conformance/foo/meta.json] -> 'ts java python csharp'
ok:   [spec/metamodel.md] -> 'ts java python csharp'
ok:   [scripts/ci-local.sh] -> 'ts java python csharp'
ok:   [.github/workflows/local-ci.yml] -> 'ts java python csharp'
ok:   [agent-context/skills/x.md] -> 'ts java python csharp'
ok:   [weird-new-toplevel/file.txt] -> 'ts java python csharp'
ok:   [docs/features/cli.md] -> ''
ok:   [README.md CHANGELOG.md] -> ''
ok:   [server/typescript/a.ts server/python/b.py] -> 'ts python'
ok:   [docs/x.md fixtures/y.json] -> 'ts java python csharp'
ALL PASS
ok:   union all -> 'ts java python csharp'
ok:   union reorders to canonical -> 'ts java python csharp'
ok:   union deduplicates -> 'ts java'
ok:   union single -> 'python'
ok:   union empty -> ''
ok:   union drops unknown tokens -> 'ts'
ok:   every lane green -> nothing to re-run -> ''
ok:   docs-only run skips a red java -> java still not green -> 'java'
ok:   one red ts lane selects the whole ts port -> 'ts'
ok:   skipped is walked past to the real verdict -> ''
ok:   cancelled is not green -> 'csharp'
ok:   a lane with no verdict in the window is not green -> 'python'
ok:   no history at all -> everything -> 'ts java python csharp'
ok:   every port-testing job in .github/workflows/local-ci.yml is mapped by lane_port -> ''
ok:   every lane in lane_port exists as a job in .github/workflows/local-ci.yml -> ''
all checks passed
── ✓ ci lane selection (0s)

── ▶ extract field kinds ──────────────────────────────────────────────
extract FieldKind: 4 ports agree on [STRING, INT, LONG, DOUBLE, BOOLEAN, ENUM, OBJECT] (1 sanctioned deviation(s): csharp)
── ✓ extract field kinds (0s)

── ▶ no-magic gate wired (5 ports) ──────────────────────────────────────────────
no-magic gate coverage: 5 ports wired; 13 shapes tracked, 0 known gaps
── ✓ no-magic gate wired (5 ports) (0s)

── ▶ test-file references resolve ──────────────────────────────────────────────
test-file references: 33 named, all resolve
── ✓ test-file references resolve (0s)

── ▶ metamodel-version bump ──────────────────────────────────────────────
  metamodel-version: additive change since v1.2.0; declared 1.3 (1.2 → 1.3) — ok.
ok:   no change classifies as nothing
ok:   a removed subtype is BREAKING
ok:   an added subtype is ADDITIVE
ok:   a removed attr is BREAKING
ok:   an added OPTIONAL attr is ADDITIVE
ok:   an added REQUIRED attr is BREAKING
ok:   optional → required is BREAKING
ok:   required → optional is ADDITIVE
ok:   a changed valueType is BREAKING
ok:   a changed isArray is BREAKING
ok:   an enum member REMOVED is BREAKING
ok:   an enum member ADDED is ADDITIVE
ok:   an OPEN attr becoming a closed enum is BREAKING
ok:   a closed enum OPENING is ADDITIVE
ok:   a removed child rule is BREAKING
ok:   an added OPTIONAL child rule is ADDITIVE
ok:   an added MANDATORY child rule is BREAKING
ok:   raising min is BREAKING
ok:   lowering min is ADDITIVE
ok:   capping an unbounded max is BREAKING
ok:   lifting a cap is ADDITIVE
ok:   lowering a finite max is BREAKING
ok:   a removed commonAttr is BREAKING
ok:   a changed default subtype is BREAKING (it changes what an unqualified declaration MEANS)
ok:   a removed default subtype is BREAKING
ok:   an added default subtype is ADDITIVE
ok:   a type description change is PROSE ONLY
ok:   a `rules` change is PROSE ONLY — this is exactly #210, and why prose only warns
ok:   an attr description change is PROSE ONLY
ok:   no change requires no bump
ok:   additive requires a minor

... [328377 bytes truncated] ...

db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it

test/unit/verify-output.test.ts:
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta: failed to load metadata: ERR_INVALID_TEMPLATE: template "Broken" @payloadRef "DoesNotExist" does not resolve to an object.value or sourceless object.projection at root
  in meta.ai.json at $['metadata.root'].children[0]['template.output']
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta: [Welcome] (email/html) ERR_VAR_NOT_ON_PAYLOAD: nonExistentField (prompts/e/html:1)
meta: meta verify — 1 drift error(s) across 1 template(s).
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it
meta verify — running --codegen --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against
meta: [Doc] (document) ERR_VAR_NOT_ON_PAYLOAD: missingField (prompts/o/x:1)
meta: meta verify — 1 drift error(s) across 1 template(s).
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); name each gate to run it

 1946 pass
 11 skip
 0 fail
 1 snapshots, 8817 expect() calls
Ran 1957 tests across 210 files. [253.42s]
── ✓ conformance: typescript (8m04s)

── ▶ ts unit suites ──────────────────────────────────────────────
bun install v1.3.14 (0d9b296a)

Checked 561 installs across 590 packages (no changes) [185.00ms]
@metaobjectsdev/metadata build: Exited with code 0
@metaobjectsdev/runtime-web build: Exited with code 0
@metaobjectsdev/angular build: Exited with code 0
@metaobjectsdev/tanstack build: Exited with code 0
@metaobjectsdev/react build: Exited with code 0
@metaobjectsdev/sdk bundle-agent-context: bundled agent-context → <worktree>/server/typescript/packages/sdk/agent-context
@metaobjectsdev/sdk bundle-agent-context: Exited with code 0
bun test v1.3.14 (0d9b296a)

test/registry-coverage.test.ts:

[registry-coverage] subtypes: 59/67 exercised (88.1%), 8 UNTESTED
[registry-coverage] untested subtypes:
  attr.boolean
  attr.class
  attr.double
  attr.expression
  attr.filter
  attr.int
  attr.intMap
  attr.long
[registry-coverage] exercised subtypes with untested attrs: 30

 3932 pass
 0 fail
 11742 expect() calls
Ran 3932 tests across 196 files. [21.78s]
bun test v1.3.14 (0d9b296a)

 472 pass
 0 fail
 1747 expect() calls
Ran 472 tests across 25 files. [488.00ms]
bun test v1.3.14 (0d9b296a)

 963 pass
 14 skip
 0 fail
 2480 expect() calls
Ran 977 tests across 65 files. [6.36s]
bun test v1.3.14 (0d9b296a)

 156 pass
 0 fail
 702 expect() calls
Ran 156 tests across 29 files. [4.30s]
bun test v1.3.14 (0d9b296a)

 63 pass
 0 fail
 198 expect() calls
Ran 63 tests across 16 files. [1039.00ms]
bun test v1.3.14 (0d9b296a)

 23 pass
 0 fail
 90 expect() calls
Ran 23 tests across 3 files. [518.00ms]
bun test v1.3.14 (0d9b296a)

 422 pass
 0 fail
 1081 expect() calls
Ran 422 tests across 39 files. [2.02s]
bun test v1.3.14 (0d9b296a)

 21 pass
 0 fail
 71 expect() calls
Ran 21 tests across 6 files. [134.00ms]
bun test v1.3.14 (0d9b296a)

 58 pass
 0 fail
 123 expect() calls
Ran 58 tests across 11 files. [276.00ms]
bun test v1.3.14 (0d9b296a)

 89 pass
 0 fail
 1 snapshots, 670 expect() calls
Ran 89 tests across 22 files. [1373.00ms]
bun test v1.3.14 (0d9b296a)

 105 pass
 0 fail
 194 expect() calls
Ran 105 tests across 9 files. [246.00ms]
bun test v1.3.14 (0d9b296a)

 21 pass
 0 fail
 38 expect() calls
Ran 21 tests across 4 files. [1036.00ms]
bun test v1.3.14 (0d9b296a)

 49 pass
 0 fail
 76 expect() calls
Ran 49 tests across 6 files. [737.00ms]
bun test v1.3.14 (0d9b296a)

 21 pass
 0 fail
 35 expect() calls
Ran 21 tests across 4 files. [327.00ms]
── ✓ ts unit suites (47s)

── ▶ completeness-gate (mutation) ──────────────────────────────────────────────
$ stryker run
^[[32m22:37:45 (724763) INFO ProjectReader^[[39m Found 6 of 989 file(s) to be mutated.
^[[32m22:37:45 (724763) INFO Instrumenter^[[39m Instrumented 6 source file(s) with 1767 mutant(s)
^[[32m22:37:45 (724763) INFO ConcurrencyTokenProvider^[[39m Creating 6 test runner process(es).
^[[32m22:37:46 (724763) INFO DryRunExecutor^[[39m Starting initial test run (command test runner with "perTest" coverage analysis). This may take a while.
^[[32m22:37:48 (724763) INFO DryRunExecutor^[[39m Initial test run succeeded. Ran 1 tests in 2 seconds (net 2581 ms, overhead 0 ms).
Terminated
error: script "conformance:mutation" exited with code 143
Evidence: Before/after OpenAPI + UI probe
AFTER: ParcelCreate/ParcelUpdate=[ref,ownerId]; shipTo* params: none; UI=[id,ref,ownerId,shipTo]
BEFORE: ParcelCreate/ParcelUpdate=[ref,ownerId,shipToStreet,shipToCity]; shipToCity query param x9; UI=[id,ref,ownerId,shipToStreet,shipToCity]

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
meta gen on a model with a derived flattened parent (Parcel.shipTo) emits no shipToStreet/shipToCity in write-table, Zod, queries or routes ✅ pass live Ran the worktree CLI (bun server/typescript/packages/cli/bin/meta.ts gen) in a disposable project. Generated Parcel.ts has no flat keys. ParcelInsertSchema and ParcelUpdateSchema hold only ref and o…
A non-derived flattened parent (Owner.home) still gets flat columns, Zod keys and allowlists (control) ✅ pass live Generated Owner.ts has homeStreet/homeCity columns, create and update Zod keys, and the homeCity filter/sort allowlist.
OpenAPI bodies and UI descriptor for the derived-parent entity carry no flat keys; the pre-fix code leaked them (adversarial before/after) ✅ pass live Probe test calling buildOpenApiDocument and buildEntityUiDescriptor on the disposable model. After the fix: ParcelCreate and ParcelUpdate have [ref, ownerId], no shipTo* query params, UI fields [id, r…
Regression: new test in flattened-storage-e2e.test.ts fails on the old code and passes on the fix ✅ pass live bun test test/flattened-storage-e2e.test.ts gives 21 pass with the fix. With the old flattened.ts it fails: flatSurfaceFields returned shipToStreet and shipToCity instead of shipTo.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test test/flattened-storage-e2e.test.ts in codegen-ts, with and without the fix
  • Worktree meta eject + meta gen (run from source) on a disposable project with a derived flattened parent, a replica view, and a non-derived flattened control
  • Throwaway bun test probing buildOpenApiDocument and buildEntityUiDescriptor before and after the fix
  • scripts/ci-local.sh --quick --strict-toolchains (killed at the background time limit; no result)
  • Baseline already run by the pipeline: scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (passed)
⚠️ **Document** - 1 info
  • ℹ️ server/typescript/packages/codegen-ts/src/flattened.ts:54 - Judgment for outer executor: the doc edit assumes flatSurfaceFields goes through flattenedMembersOf. asFlatKey still forwards the member's isDerived() rather than the parent's. Confirm that path is unreachable for a derived parent, or add the parent check there too.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ction: derived flattened field owns no columns and exposes no flat keys. CHANGELOG already covered the fix. No other stale docs found.
@dmealing
dmealing merged commit c1fcd34 into main Nov 6, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-524-derived-flat-parent branch November 6, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant