Skip to content

fix(codegen): complete field.map codegen for the Java and C# ports - #360

Merged
dmealing merged 22 commits into
mainfrom
fm/mo-fieldmap-jsonb-k4
Sep 10, 2026
Merged

dmealing merged 22 commits into
mainfrom
fm/mo-fieldmap-jsonb-k4

Conversation

@dmealing

@dmealing dmealing commented Sep 10, 2026

Copy link
Copy Markdown
Member

⚠️ On the green checkmark: it is leak-scan only — 1 of 1

The port test suites do not run on PRs in this repo. The Java / C# / Python / Kotlin
compile + conformance gates run on push to main (self-hosted runner), on release tags,
nightly, and on manual dispatch — never on a pull request. So this PR's passing check
confirms exactly one thing: no leaked private names or absolute paths.

The real evidence for this branch is the pipeline's own test step plus local runs on the
final head (10cd44dde):

suite result
Java codegen-spring (mvn clean test) 247 passed
C# MetaObjects.Codegen.Tests 444 passed, 1 skipped
C# MetaObjects.Conformance.Tests 1024 passed
C# MetaObjects.Render.Tests 291 passed
C# MetaObjects.Cli.Tests 77 passed
C# MetaObjects.IntegrationTests (Testcontainers PG) 120 passed
sdk agent-context-conformance 5 passed

The pipeline's test step also drove the product livedotnet meta gen → EF Core 8 +
Npgsql → Postgres 16 in Docker → real HTTP — which is what caught the NULL-column 500 fixed
in this branch. No static gate here would have found it. Do not read the checkmark as the
ports passing.


Intent

Docs-only round, required by the maintainer as a precondition of merging this branch. Re-submitted after the previous run failed on a TEST-REPORTING schema error, not a code failure.

TEST STEP - READ THIS FIRST. The prior run failed with: validate test analyzer findings: scenario 4 result "pass" requires live validation. That is a reporting-schema violation, not a broken test. Mark a scenario "pass" ONLY if you actually drove it live in THIS run and observed the result. If you did not execute it, do not report it as passing - report it honestly as not run, or run it. Do not carry a result forward from a previous run or infer one from the diff.

WHAT THIS ROUND IS. A review round found that the field.map codegen change made a surface REACHABLE that silently accepts invalid nested data, and that my first attempt to document it got the SCOPE WRONG. Both are now fixed in docs only.

The gap, verified in source rather than inferred: a field.map @objectref can write its nested value-object values UNVALIDATED, and the scope is per port.

  • Java (codegen-spring): validated on the vanilla create/PATCH handlers - this branch added an explicit map-value loop - but NOT on TPH (discriminator-rooted) write paths, which validate field-by-field with validateValue. That does not cascade @Valid into a nested bean, and appendValueObjectValidation has exactly one call site, on the vanilla handler (SpringControllerGenerator.java:345).
  • C#: validated on NO write path - not vanilla create, not vanilla PATCH, not TPH. AppendCreateVoValidation iterates ValueObjectFields (RoutesGenerator.cs:565), which admits only field.object, so a map never reaches the recursively-validating value-object arms; the generic PATCH arm checks the dictionary property itself with TryValidateProperty, never its values.
  • TypeScript and Python DO validate map values (z.record over the VO insert schema; dict[str, VO] Pydantic). Kotlin writes no map column at all. Scalar-valued maps (@valueType) are unaffected everywhere - there is no nested bean to validate.

WHY IT IS DOCUMENTED RATHER THAN FIXED, which is a maintainer ruling and not mine: the path is newly reachable (before the MapField type-mapper arm a map-bearing entity failed Java codegen outright, and on C# a map property was emitted but never persisted), so no existing adopter regresses, and holding the branch for a hole in a path nobody uses yet costs more than it buys. Tracked as issue #362.

WHY DOCS AND NOT ONLY KNOWN_GAPS, also a maintainer ruling: unlike the other gaps, this is GENERATED code that silently accepts invalid values - an adopter cannot discover it by reading their own source, and the failure mode is acceptance rather than an error. So the warning goes where someone decides to USE field.map: the field.map section of docs/features/field-types.md, and the authoring and audit skills' ladder guidance, which an agent reads when choosing the rung. Both skills previously told the reader the rung was safe where generated code is the consumer, which is exactly where this bites; both are corrected, and the expected-skill goldens regenerated.

Scope: documentation, skills and regenerated goldens ONLY. No generator, emission, runtime or test behavior changes, and no gap entry closed or narrowed.

This repository is PUBLIC: no private consumer project names, no personal information, no absolute local paths in any committed file or commit message.

What Changed

  • Complete field.map codegen on the two ports that previously failed it: Java's SpringTypeMapper/DTO/payload/controller generators now emit java.util.Map<String, V> instead of hitting the unsupported Spring DTO type mapping throw, and the value-object emission walk now reaches an @objectRef value object referenced only through a map. C#'s DbContextGenerator now emits an explicit jsonb column type plus a converter/comparer pair for Dictionary<string, V> properties on entities, read-only projections and flattened value-object members, so the property actually persists to the TS-owned migration's column instead of being dropped by EF.
  • Fix correctness bugs the completion surfaced: map properties are nullable-by-default (matching the migration's nullable column) with a null-guarded MapJsonb comparer Hash/Snapshot to stop NULL-column reads from 500ing, and enum members inside a map's value object now serialize as their symbol via JsonStringEnumConverter instead of the default integer ordinal. Add matching coverage: Java's SpringMapFieldCodegenTest, C#'s MapNullColumnGeneratedServerTest and MapValueObjectWriteBehaviorGeneratedServerTest, plus updates to the existing DbContextCompileTests/MapFieldCodegenTests/ObjectFieldCodegenTests/SpringTypeMapperTest suites.
  • Document a validation gap this newly reachable surface exposes: a field.map @objectRef can write nested value-object values unvalidated, scoped per port (C#: no write path validates nested map values; Java: only TPH/discriminator write paths skip it) — recorded in docs/features/field-types.md, the C#/Java/Kotlin KNOWN_GAPS.md files, the metaobjects-authoring/metaobjects-audit skills, their regenerated golden fixtures, and the CHANGELOG.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The docs round's claims were independently verified against source, and the two pipeline fix commits (MapJsonb null-guard, enum-symbol serialization) are narrow, exactly implement the user-prescribed remedies, and carry genuinely discriminating executed tests; the remaining unvalidated-nested-map-write gap is maintainer-authorized containment tracked as issue #362.

Testing

Stood up the real generated C# API (compiled from the actual generators, not a mock) on Kestrel against a live Testcontainers Postgres and drove three end-to-end scenarios: a NULL field.map column reading/listing/patching cleanly (the regression this test step originally caught and that was then fixed), a map-valued-object's enum member persisting as its symbol rather than its ordinal in the raw jsonb column, and a nested map value-object missing a required field being silently accepted on create (matching the newly-corrected per-port documentation). All three passed live. Wrote and committed one new integration test file covering the latter two scenarios, since no live test previously existed for them (only a non-live in-process compile test covered the enum case). Java's field.map codegen has no live-drivable runtime surface on this branch by design (codegen-only; no port persists a map at runtime except Python, and no conformance corpus touches it), so its regression safety was checked via its existing non-live codegen test suite (38 tests, all green) rather than a live scenario.

  • Live validation: ✅ go - 3 of 4 scenarios driven live against the product
Scenario Result Live Evidence
NULL field.map column reads, lists, and PATCHes cleanly through the generated C# API ✅ pass live dotnet test ...--filter FullyQualifiedName~MapNullColumn → Passed (see field-map-live-integration-tests.log); GET by id, GET list, and PATCH all succeed against a seeded row whose map columns are SQL…
A field.map @objectref value object's enum member persists to jsonb as its symbol, not its ordinal ✅ pass live New test An_enum_valued_map_component_persists_its_symbol_not_its_ordinal, dotnet test ...--filter FullyQualifiedName~MapValueObjectWriteBehavior → Passed; raw jsonb column read back via direct SQL (b…
A field.map @objectref nested value missing a @required field is silently accepted on create (matches the corrected per-port doc claim) ✅ pass live New test A_map_valued_address_missing_its_required_street_is_silently_accepted_on_create → 201 Created returned, not 400, for an Address missing its required "street" inside a map value; confirms docs…
Java field.map codegen still emits compilable, correctly-typed java.util.Map<String,V> for a map-bearing entity ⏸️ untested no No live-exercisable runtime surface exists for Java field.map on this branch: no port's persistence layer other than Python's ObjectManager reads/writes a map at runtime, and no persistence- or api-co…
Evidence: Live C# field.map integration test run (3/3 passed)
  Determining projects to restore...
  All projects are up-to-date for restore.
  MetaObjects.Render -> ~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects.Render/bin/Debug/net8.0/MetaObjects.Render.dll
  MetaObjects -> ~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects/bin/Debug/net8.0/MetaObjects.dll
  MetaObjects.Codegen -> ~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects.Codegen/bin/Debug/net8.0/MetaObjects.Codegen.dll
  MetaObjects.IntegrationTests -> ~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects.IntegrationTests/bin/Debug/net8.0/MetaObjects.IntegrationTests.dll
Test run for ~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects.IntegrationTests/bin/Debug/net8.0/MetaObjects.IntegrationTests.dll (.NETCoreApp,Version=v8.0)
Microsoft (R) Test Execution Command Line Tool Version 17.8.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
~/.no-mistakes/worktrees/3d4ac8180e56/01M24SZGYK804EY8JM00HCAP8W/server/csharp/MetaObjects.IntegrationTests/bin/Debug/net8.0/MetaObjects.IntegrationTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.6+bf9b858c26 (64-bit .NET 8.0.30)
[xUnit.net 00:00:00.03]   Discovering: MetaObjects.IntegrationTests
[xUnit.net 00:00:00.07]   Discovered:  MetaObjects.IntegrationTests
[xUnit.net 00:00:00.07]   Starting:    MetaObjects.IntegrationTests
[testcontainers.org 00:00:00.04] Connected to Docker:
  Host: unix:///var/run/docker.sock
  Server Version: 29.1.3
  Kernel Version: 7.1.1-76070101-generic
  API Version: 1.52
  Operating System: Pop!_OS 22.04 LTS
  Total Memory: 60.40 GB

[testcontainers.org 00:00:00.23] Docker container 057b6e3e0b35 created
[testcontainers.org 00:00:00.25] Start Docker container 057b6e3e0b35
[testcontainers.org 00:00:00.37] Wait for Docker container 057b6e3e0b35 to complete readiness checks
[testcontainers.org 00:00:00.37] Docker container 057b6e3e0b35 ready
[testcontainers.org 00:00:00.49] Docker container a4ae6b67bcf8 created
[testcontainers.org 00:00:00.50] Docker container f0b4574a199f created
[testcontainers.org 00:00:00.50] Start Docker container a4ae6b67bcf8
[testcontainers.org 00:00:00.50] Start Docker container f0b4574a199f
[testcontainers.org 00:00:00.63] Wait for Docker container f0b4574a199f to complete readiness checks
[testcontainers.org 00:00:00.63] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container f0b4574a199f
[testcontainers.org 00:00:00.64] Wait for Docker container a4ae6b67bcf8 to complete readiness checks
[testcontainers.org 00:00:00.64] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container a4ae6b67bcf8
[testcontainers.org 00:00:01.68] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container f0b4574a199f
[testcontainers.org 00:00:01.68] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container a4ae6b67bcf8
[testcontainers.org 00:00:02.71] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container f0b4574a199f
[testcontainers.org 00:00:02.71] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container a4ae6b67bcf8
[testcontainers.org 00:00:03.75] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container f0b4574a199f
[testcontainers.org 00:00:03.75] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container a4ae6b67bcf8
[testcontainers.org 00:00:04.78] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container f0b4574a199f
[testcontainers.org 00:00:04.78] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container a4ae6b67bcf8
[testcontainers.org 00:00:04.81] Docker container f0b4574a199f ready
[testcontainers.org 00:00:04.81] Docker container a4ae6b67bcf8 ready
[testcontainers.org 00:00:06.31] Delete Docker container f0b4574a199f
[testcontainers.org 00:00:06.37] Delete Docker container a4ae6b67bcf8
  Passed MetaObjects.IntegrationTests.Api.MapNullColumnGeneratedServerTest.A_null_map_column_reads_lists_and_patches_cleanly [6 s]
  Passed MetaObjects.IntegrationTests.Api.MapValueObjectWriteBehaviorGeneratedServerTest.A_map_valued_address_missing_its_required_street_is_silently_accepted_on_create [6 s]
[testcontainers.org 00:00:07.25] Docker container 3e73bd6f635f created
[testcontainers.org 00:00:07.26] Start Docker container 3e73bd6f635f
[testcontainers.org 00:00:07.37] Wait for Docker container 3e73bd6f635f to complete readiness checks
[testcontainers.org 00:00:07.37] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container 3e73bd6f635f
[testcontainers.org 00:00:08.42] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container 3e73bd6f635f
[testcontainers.org 00:00:09.46] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container 3e73bd6f635f
[testcontainers.org 00:00:10.48] Execute "pg_isready --host localhost --dbname postgres --username postgres" at Docker container 3e73bd6f635f
[testcontainers.org 00:00:10.52] Docker container 3e73bd6f635f ready
[testcontainers.org 00:00:10.81] Delete Docker container 3e73bd6f635f
[xUnit.net 00:00:11.27]   Finished:    MetaObjects.IntegrationTests
  Passed MetaObjects.IntegrationTests.Api.MapValueObjectWriteBehaviorGeneratedServerTest.An_enum_valued_map_component_persists_its_symbol_not_its_ordinal [4 s]

Test Run Successful.
Total tests: 3
     Passed: 3
 Total time: 11.4989 Seconds
- Outcome: 🔧 2 issues found → auto-fixed ✅ across 2 runs (44m11s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ docs/features/field-types.md:65 - All three locations of the unvalidated-write warning end with a sentence scoping the tracking issue to Java only — field-types.md:65 "[Issue field.map @objectRef writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only) #362] tracks the Java TPH half; the C# surface is wider than that issue's scope", agent-context/skills/metaobjects-authoring/SKILL.md:599 "tracks the Java TPH half, and the C# scope is wider than that issue", and the same phrase in agent-context/skills/metaobjects-audit/SKILL.md:369 — but issue field.map @objectRef writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only) #362 was retitled at 2026-09-10T04:30:08Z to "field.map @objectref writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only)" (verified live via the GitHub timeline API), so it now explicitly covers the C# surface. The docs commit was finalized at 04:42Z, after the rename: the sentence is stale as committed. A reader following any of the three links lands on an issue whose title contradicts the sentence, undermining the one pointer this round exists to give, and suggesting the C# half is untracked when it is not. The round's own user intent already says the whole per-port gap is "Tracked as issue field.map @objectRef writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only) #362", so the correction aligns the text with the stated intent. Fix: update the sentence in the three source files (the technical per-port content stays exactly as-is) and regenerate the 10 expected-skill goldens via the sdk regen script.
  • ⚠️ server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs:760 - The generated MapJsonb helper null-guards Eq (line 741: if (a is null || b is null) return ReferenceEquals(a, b)) but not Hash (line 760-762: var h = v.Count; dereferences v) or Snap (line 770-775: a scalar-valued map takes new Dictionary&lt;string, TValue&gt;(v), which throws ArgumentNullException on a null v). EF Core's own built-in value comparers guard null inside the typed lambdas (e.g. its string comparer is v =&gt; v == null ? 0 : v.GetHashCode()), and ValueComparer<T>.Snapshot/GetHashCode have no null guard of their own — the comparer contract expects the lambdas to handle null. The null state is reachable through this branch's own semantics: the map column is nullable by default (no @required), C# PATCH can null it (the branch's G7 rewrite states the generic merge arm writes map columns, including present-null), and other ports' inserts leave it NULL. A subsequent query materializes the property as null (the = new() initializer does not survive EF's shaper), and change tracking then runs Snap/Hash over it — a NullReferenceException/ArgumentNullException inside EF on an ordinary read of a row whose map column is NULL, for every scalar-valued map. The compile tests never execute the model, so nothing gates this. Fix: mirror Eq's guard — if (v == null) return 0; in Hash and a null early-return in Snap (returning null preserves current materialization semantics); the guards are dead code if EF never passes null, which is the safe direction.

🔧 Fix applied.
1 warning still open:

  • ⚠️ server/csharp/MetaObjects.Codegen/Generators/DbContextGenerator.cs:724 - The MapJsonb helper this branch adds serializes the whole map value with raw System.Text.Json (Options = new() at line 724, consumed by the Converter's Serialize(v, Options) at 728, and by Eq/Snap's JSON arms), so any field.enum member of the map's @objectRef value object persists as its ORDINAL — {&#34;tier&#34;: 0} — while every other jsonb path in this same generated model deliberately persists the member SYMBOL: the sibling owned-field.object ToJson path emits HasConversion&lt;string&gt;() per enum member via JsonEnumConversions (DbContextGenerator.cs:1159) with the comment explicitly calling the ordinal form "a cross-port wire-contract break, and a positionally fragile one: reordering @values silently re-maps already-stored data"; TS (z.record + JSON.stringify), Python (Pydantic) and Kotlin (Jackson) all write the symbol. Concrete trace: DbContextCompileTests' own fixture is the reproducer — Order.sites is Dictionary&lt;string, Address&gt; and Address carries Kind (HOME/WORK) and int-backed Tier (@intValueMap A=1,B=2); a C# create through the generated routes persists {&#34;hq&#34;:{&#34;kind&#34;:0,&#34;tier&#34;:0}} — for Tier, the ordinal 0 means neither A (mapped 1) nor B (mapped 2) — and a TS/Python reader of that row fails validation (z.enum/Pydantic reject the number), silently at write time. No gate catches it: no corpus exercises field.map, and the map fixtures in MapFieldCodegenTests/DbContextCompileTests have no enum-valued VO. Note the C# read side is permissive (STJ reads both ordinals and names), so this only corrupts at write. Smallest honest remedy: register System.Text.Json.Serialization.JsonStringEnumConverter on the emitted Options (matching the ToJson path's symbol convention for both string- and int-backed enums) and extend MapJsonb_comparer_hash_and_snapshot_tolerate_a_null_dictionary-style coverage to a VO with an enum member asserting the provider value carries the symbol — or, if the maintainer prefers the field.map @objectRef writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only) #362 containment pattern for this newly-reachable surface, record the divergence there; but nothing in the intent or decisions chose ordinals.

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Test** - 2 issues found → auto-fixed ✅
  • 🚨 server/csharp/MetaObjects.Codegen/Generators/EntityGenerator.cs:1365 - Every read arm of a C# adopter's generated API returns HTTP 500 when any row's field.map column is NULL. Live-reproduced against the real product (dotnet meta gen output, EF Core 8.0.10 + Npgsql 8.0.10, Postgres 16 in docker): GET /api/orders/{id}, GET list, and PATCH/PUT all throw System.InvalidCastException "Column 'labels' is null" from NpgsqlDataReader.GetString — the list endpoint is poisoned by a single NULL-map row (deterministic: delete the row → 200, re-insert → 500). Root cause: EntityGenerator.MapProperty emits the map as a NON-nullable CLR property public Dictionary&lt;string, V&gt; X { get; set; } = new(); while the column is nullable by default (the TS-owned migration creates nullable jsonb; the committed review discussion states "no @required"), and the new MapJsonb ValueConverter's provider type is string — EF Core 8 omits the shaper's IsDBNull check for non-nullable model properties, so materialization throws before change tracking ever runs. That means the null guards this branch committed in MapJsonb.Hash/Snap (38afbe4) are unreachable through the read path: the test MapJsonb_comparer_hash_and_snapshot_tolerate_a_null_dictionary validates the lambdas in isolation and stays green while the product 500s. NULL cells are reachable cross-port (any non-C# writer or raw insert leaves NULL — the write path itself writes {} via the initializer, so C#-only tables hide the bug). There is no authorable mitigation: @required on a field.map is accepted by the loader but changes nothing in the emission (verified by regenerating with @required — identical property). New on this branch's newly-reachable surface (before the EF mapping, the property was never read from a column); not covered by issue field.map @objectRef writes nested value-object values UNVALIDATED (C#: all write paths; Java: TPH only) #362 (that is the write-side unvalidated-acceptance hole), KNOWN_GAPS.md, or the field-types.md warning. Fix requires an emission decision the author must make (nullable Dictionary<string,V>? property with route/PATCH null-clear semantics, or model-level nullability config, or documenting the read-side gap) — out of bounds for this round's "no emission changes without maintainer instruction" scope.
  • 🚨 live validation verdict: no-go (6 of 9 scenarios were driven live against the product); failed: A row whose field.map column is NULL (written by another port or raw SQL) reads and patches cleanly through the generated C# stack — the null-tolerance goal of the committed MapJsonb Hash/Snap guards
  • Live validation: ❌ no-go - 6 of 9 scenarios driven live against the product
Scenario Result Live Evidence
A C# adopter creates a row carrying a field.map @objectref whose value object has field.enum members; the jsonb column stores the member SYMBOLS and the row round-trips ✅ pass live csharp-null-map-repro.txt + csharp-live-drive-http.txt: POST 201, psql shows sites = {"hq": {"kind": "HOME", "tier": "A", "street": "1 Main"}}
The same generated C# routes still enforce scalar constraints (missing @required note rejected) ✅ pass live csharp-live-drive-http.txt: POST {"labels":{"a":"b"}} (no note) → {"error":"validation"} HTTP 400
Adversarial (docs warning / #362): a posted map value violating the referenced object.value's own constraints is silently accepted on C# create AND patch ✅ pass live csharp-live-drive-http.txt: street over @maxlength and empty-street accepted → HTTP 201 (row persisted, visible in csharp-null-map-repro.txt); PATCH → HTTP 200
A row whose field.map column is NULL (written by another port or raw SQL) reads and patches cleanly through the generated C# stack — the null-tolerance goal of the committed MapJsonb Hash/Snap guards ❌ fail live csharp-null-map-read.txt + csharp-null-map-repro.txt + csharp-null-map-exception.txt: GET/GET-list/PATCH → HTTP 500, System.InvalidCastException "Column 'labels' is null" at NpgsqlDataReader.GetString…
Java: every entity-facing generator completes over a map-bearing entity (no unsupported-type throw) and a real Hibernate Validator cascades into nested map values on the vanilla surface ✅ pass live java-live-drive.txt: GENERATED (7 files incl. CustomerDto/CustomerController); real Hibernate Validator → VALID: 0 violations, INVALID: 1 violation at addresses[hq].street "size must be between 1 and…
Java: TPH (discriminator-rooted) write paths do NOT cascade validation into map values (the warning's negative claim) ⏸️ untested no Proving the negative live requires generating a TPH model, booting the generated Spring controller against a database, and posting an invalid nested map value — a full Spring Boot harness not stood up…
The warning's remaining per-port claims: TypeScript (z.record) and Python (Pydantic) validate map values; Kotlin writes no map column ⏸️ untested no Driving them needs a TS project whose generated zod insert schema is executed against an invalid map value and a Python Pydantic model check (plus a Kotlin write-path probe); none were exercised here…
The regenerated expected-skill goldens still byte-match the shipped skills through the sdk's agent-context conformance gate ✅ pass live cd server/typescript/packages/sdk &amp;&amp; bun test agent-context-conformance executed in this run over the real agent-context/skills files and the 10 updated goldens: 5 pass, 0 fail
The unvalidated-write warning, both skills' ladder corrections, and the CHANGELOG [Unreleased] entry are present with the corrected per-port scope and a non-stale #362 reference ⏸️ untested no These are non-executable committed artifacts (markdown/CHANGELOG) with no runtime consumer to drive; verified by inspection of the committed content — their behavioral claims about C# and Java were li…
  • cd server/typescript/packages/sdk && bun test agent-context-conformance — 5 pass, 0 fail over the regenerated expected-skill goldens
  • dotnet test server/csharp/MetaObjects.Codegen.Tests/MetaObjects.Codegen.Tests.csproj — 444 passed, 1 skipped, 0 failed (includes MapJsonb enum-symbol, null-dictionary, and map-mapping tests)
  • cd server/java && mvn -pl codegen-spring -am test -Dtest='SpringMapFieldCodegenTest,SpringTypeMapperTest' -Dsurefire.failIfNoSpecifiedTests=false — 10 + 28 tests, 0 failures (surefire reports)
  • dotnet run --project server/csharp/MetaObjects.Cli -- gen <scratch-meta> --out <scratch-out> --namespace Live.Drive — real dotnet meta gen over a field.map model (labels @valueType, sites @objectRef VO with string-backed and int-backed enums)
  • Live C# end-to-end: scratch ASP.NET host wiring the generated MapOrderRoutes exactly like the repo's reference server (JsonStringEnumConverter on HttpJsonOptions, UseNpgsql, EF Core 8.0.10 / Npgsql 8.0.10) against docker postgres:16; curl POST/PATCH/GET drives + psql inspection of raw jsonb
  • Live null-map reproduction: raw-SQL row with NULL labels/sites → GET/GET-list/PATCH all 500 with System.InvalidCastException at NpgsqlDataReader.GetString; delete row → 200; re-insert → 500
  • Live Java drive: consumer main invoking SpringDtoGenerator/SpringValueObjectGenerator/SpringNamesGenerator/SpringRepositoryGenerator/SpringControllerGenerator/SpringFilterAllowlistGenerator via their public API, in-process javac of the emitted CustomerDto/Address, then a real Hibernate Validator 8.0.1.Final cascade over valid and invalid nested map values
  • Content inspection: docs/features/field-types.md callout, both skills' ladder paragraphs (stale #362 caveat gone, per-port split present), CHANGELOG [Unreleased] entry

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 3 of 4 scenarios driven live against the product
Scenario Result Live Evidence
NULL field.map column reads, lists, and PATCHes cleanly through the generated C# API ✅ pass live dotnet test ...--filter FullyQualifiedName~MapNullColumn → Passed (see field-map-live-integration-tests.log); GET by id, GET list, and PATCH all succeed against a seeded row whose map columns are SQL…
A field.map @objectref value object's enum member persists to jsonb as its symbol, not its ordinal ✅ pass live New test An_enum_valued_map_component_persists_its_symbol_not_its_ordinal, dotnet test ...--filter FullyQualifiedName~MapValueObjectWriteBehavior → Passed; raw jsonb column read back via direct SQL (b…
A field.map @objectref nested value missing a @required field is silently accepted on create (matches the corrected per-port doc claim) ✅ pass live New test A_map_valued_address_missing_its_required_street_is_silently_accepted_on_create → 201 Created returned, not 400, for an Address missing its required "street" inside a map value; confirms docs…
Java field.map codegen still emits compilable, correctly-typed java.util.Map<String,V> for a map-bearing entity ⏸️ untested no No live-exercisable runtime surface exists for Java field.map on this branch: no port's persistence layer other than Python's ObjectManager reads/writes a map at runtime, and no persistence- or api-co…
  • dotnet test MetaObjects.IntegrationTests --filter FullyQualifiedName~MapNullColumn (live: Testcontainers Postgres + Kestrel-hosted generated server)
  • dotnet test MetaObjects.IntegrationTests --filter FullyQualifiedName~MapValueObjectWriteBehavior (new test, live: same harness) — added and committed as 59590f0a3
  • dotnet test MetaObjects.Codegen.Tests --filter DbContextCompileTests|MapFieldCodegenTests|ObjectFieldCodegenTests (non-live, executes compiled generated code in-process; regression smoke check)
  • mvn -pl codegen-spring test -Dtest=SpringMapFieldCodegenTest,SpringTypeMapperTest (non-live codegen/compile tests; regression smoke check, Java side unchanged since round 1)
  • gh-axi issue view 362 — confirmed retitled issue text matches the corrected docs/skill wording
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 info
  • ℹ️ server/csharp/MetaObjects.Codegen.Tests/Issue203AutoSetStampingTests.cs:192 - Pre-existing xUnit2031 analyzer warning (Where before Assert.Single) in a file this change never touched; surfaced by the Codegen.Tests build alongside the branch's files. Left unfixed as out of the change's scope — the fix would edit test code in an unrelated file.
✅ **Push** - passed

✅ No issues found.

@dmealing
dmealing force-pushed the fm/mo-fieldmap-jsonb-k4 branch from a46e3da to de40933 Compare September 10, 2026 04:00
@dmealing dmealing changed the title fix(codegen): close field.map codegen gap in Java and C# ports fix(codegen): close the field.map codegen gap in the Java and C# ports Sep 10, 2026
dmealing and others added 22 commits September 10, 2026 00:42
…ng the build

`field.map` is registered in all five ports, but the Spring port had no `MapField`
arm at all: `scalarFields()` excludes only `ObjectField`, so a mapped field flowed
straight into the DTO record and hit `SpringTypeMapper`'s unsupported-type throw.
Any entity carrying one failed Java codegen outright.

Kotlin is the reference implementation and is complete; this mirrors it rather than
inventing semantics:

- `javaTypeName` gains a `MapField` arm returning `java.util.Map<String, V>`. V is
  the value object named by `@objectRef` — resolved exactly as the `field.object`
  arm resolves its own — or the scalar named by `@valueType`, over the same 11
  subtypes the loader admits. Scalars are the WRAPPED types (a Java type argument
  cannot be primitive); `@valueType: timestamp` is an absolute `Instant`
  unconditionally, since a map value has no column of its own to be "without time
  zone".
- The two `List<>` wrap sites skip a map. isArray does not apply to one, and every
  other port emits the map bare — wrapping would produce a `List<Map<String,V>>`
  no other port can round-trip.
- The value-object reachability walk now spans a map's `@objectRef`, mirroring the
  C# `ReferencesValueObject` predicate, which already did. A VO reached only
  through a map was never emitted, leaving the DTO naming a record that did not
  exist.
- `@Valid` cascades onto a value-object map component. Bean Validation descends
  into a map's values, matching the TS zod emit's
  `z.record(z.string(), <VO>InsertSchema)`.

A bare `field.map` (neither attr set) still throws rather than guessing a value
type — the loader forbids that state, so the throw is the same contract a bare
`ObjectField` gets.

Tests: 11 mapper arms + a generator suite that asserts the emitted component types,
the VO emission, the `@Valid` cascade, and — the strongest proof — that the
generated sources actually compile. 241 green on `clean test`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…ead of none

`EntityGenerator.MapProperty` already emitted `Dictionary<string, V>` with a
`[Column(...)]`, but `DbContextGenerator` had no map branch at all, so EF got no
STORAGE mapping — no column type, no value converter. The property did not persist
the way the TS-owned schema DDL declares it: on Npgsql a `Dictionary<string,string>`
binds to HSTORE by default and a `Dictionary<string,int>` binds to nothing, so the
column the migration creates (jsonb) and the column EF writes disagreed. Silently.

- A top-level `field.map` now emits `.HasColumnType("jsonb").HasConversion(...)`
  through a shared `MapJsonb` converter/comparer pair — the C# analog of Kotlin's
  `jsonb(col, encoder, decoder)`: an explicit (de)serializer, with no reliance on
  Npgsql's dynamic-JSON opt-in that generated code cannot make for a consumer.
- The COMPARER is not decoration. EF snapshots a value-converted property by
  reference, so with a converter alone an in-place `entity.Labels["k"] = v` is never
  detected and the UPDATE never fires — the same silent non-persistence being fixed
  here. The snapshot deep-copies; equality compares the serialized JSON, which is
  the right notion for a value object as well as a scalar.
- The helper is gated on the model carrying a map, so a map-free model stays
  byte-identical — same discipline as `UnmappedEnumValue`. The gate spans the
  flattened-VO case too, or a model whose only map sits inside a flattened value
  object would name a helper the file never declares.
- A FLATTENED value object's map member now binds its `<prefix>_<col>` jsonb column
  rather than warning. That warning's stated reason was that the port configured a
  top-level map nowhere either, so there was no proven mapping to mirror; that is no
  longer true. Its requirement was never "warn" — it was "do not bind silently to a
  column the migration does not create", which an explicit `HasColumnName` satisfies
  properly. The test that pinned the warning now pins the binding.
- The value object's type is emitted FULLY QUALIFIED: the DbContext's usings are a
  fixed set covering entity namespaces only, and a value object is neither an entity
  nor a view. Same rule the `System.Guid` / `System.Uri` emissions already follow.

The EF surface is proven by compilation, not by string matching: `DbContextCompileTests`
now carries scalar, value-object and flattened-member maps and compiles the emitted
context against real EF Core 8 assemblies, so the two-arg
`HasConversion(ValueConverter, ValueComparer)` overload and the generic helper are
shown to resolve on both receivers.

All C# suites green: 441 codegen, 1024 conformance, 291 render, 77 cli, and 120
Testcontainers-Postgres integration tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…bearing entity

The unsupported-type throw lived in the SHARED type mapper, so a `field.map` could
break any generator that types a field — fixing the DTO path alone would have left
the repository / controller / allowlist / names surfaces failing on the same model
with nothing catching it. This runs all six over the fixture.

Also pins that a map is NOT offered as a filterable column: no port can lower a
filter operator over an open-keyed jsonb map, so admitting one would generate a
query surface that fails at the engine. Asserted unconditionally — guarding it on
`Files.exists` would let the check evaporate the day the artifact stops being
emitted, gating nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
Three places carry the "a JSON column is a ladder" guidance and are meant to agree:
`docs/features/field-types.md`, the authoring skill and the audit skill. All three
still described the two gaps just closed — Java reaching its unsupported-type throw,
C# getting no EF column mapping — so leaving them would make the docs assert the
opposite of the code.

Corrected, and the honest remainder stated rather than dropped: codegen now completes
on all five ports, but that is CODEGEN only. No persistence- or api-contract corpus
exercises `field.map` on any port, and the RUNTIME persistence tier is uneven — only
Python's ObjectManager encodes a map today; `runtime-ts`, Java's OMDB and the Kotlin
Exposed lane carry no map handling at all (OMDB's jsonb path is gated on the
`@storage` attr a map does not have, and serializes through a per-MetaObject Gson
adapter with no map-of-value-object binding).

That distinction is what the guidance now turns on, because it is the one that
changes an adopter's decision: recommend the map rung where GENERATED CODE is the
consumer; prefer a value object where a PORT RUNTIME must read the column back. The
old "Java/C# can't do this at all" advice was wrong in a way that would send an
adopter down the wrong rung; "it all works now" would be wrong in the other
direction.

The loader's ladder error message needed no change — it names the rungs without
claiming port completeness.

Expected-skill fixtures regenerated via `regen-agent-context-conformance.ts`;
the sdk corpus test is green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…o longer throws

Every finding is a consequence of the same thing: while `SpringTypeMapper` threw on a
`field.map`, no downstream surface could ever see one. Removing the throw made six
paths reachable that had never been exercised, and each had been written as
"ObjectField or nothing".

1. PAYLOAD (medium-high). `SpringPayloadGenerator.resolveFieldType` routed a map past
   the nested-payload arm into the plain type mapper, typing the component as the
   SOURCE value object's FQN — a type the payload path never emits. `nestedTargetOf`
   matched only `ObjectField`, so the target never entered the emission closure
   either. A `@payloadRef` VO carrying a map-of-VO therefore emitted a
   `<Name>Payload` naming a record nothing generated. Now routed through
   `resolveMapFieldType`, which emits the nested payload and returns
   `Map<String, <Target>Payload>`; `nestedTargetOf` agrees, as its javadoc requires.

2. C# PROJECTION (medium). The jsonb map config lived only in `EmitFieldTypeConfig`,
   which never runs for a read-only `object.projection` — but `EntityGenerator` emits
   the `Dictionary<string, V>` property for one anyway. A view exposing a map got a
   property with no mapping, which is the exact failure the mapping exists to prevent.
   The config is now a shared `MapJsonbConfig` called from both sites.

3. SORT ALLOWLIST (medium). Every map name landed in `SORT_ALLOWLIST`, so
   `?sort=labels:asc` passed and lowered to `ORDER BY` over a jsonb column —
   meaningless ordering on Postgres, 400 elsewhere, api-contract divergence. The
   filter side was already safe, but only because no operator band matches a map.

4. UNRESOLVED REF (low). `mapValueJavaType` promised null for an unresolvable
   `@objectRef` while `MetaDataUtil.getObjectRef` throws — and its own sibling
   `mapValueObjectRefOf` catches. One dangling ref gave a not-found from the mapper
   and a silent skip from the validation path. Now caught, as documented.

5. VALUE-OBJECT `@Valid` (low). The entity DTO's cascade was widened to span maps but
   the VO generator's was not, so nested constraints went unenforced exactly one level
   down.

6. PATCH (low). POST cascades into a map's value objects via `@Valid`; PATCH validates
   per-field with `validateValue`, which does not cascade. PATCH could persist a
   nested value object POST rejects. It now validates each map value.

Each fix carries a test that fails without it, including two the old assertions could
not have caught: the sort test also asserts a plain string STAYS sortable (so it
cannot pass on an empty allowlist), and the projection test asserts the property is
emitted before asserting the mapping is. The projection arm is compile-proven by
adding a map to `DbContextCompileTests`' projection, since it is a different emitter
from the entity one.

Java 246 green on `clean test`; C# 442 codegen + 1024 conformance + 291 render + 77
cli green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…the answer

Cleanup pass over the map work. Nothing here changes what a correct model generates,
with one deliberate exception noted below.

**Sort allowlist → the canonical band.** Both sort allowlists now ask
`FilterOps.supportsFiltering(subType)` — the cross-port answer the filter side already
uses and the loader itself checks in `validateSortableHasSupportedSubtype`. This
closes a hole the hand-listed version had left: the TPH base allowlist still skipped
only `ObjectField`, so a map was unsortable on a vanilla entity and sortable on a TPH
base. Hand-maintained exclusion lists are how that happened; a shared band cannot
drift from the loader.

**One predicate for "which value object does this map carry."** `SpringTypeMapper`
was resolving `@objectRef` itself and accepting ANY target, while its sibling requires
`object.value` — so `@objectRef` naming an entity typed a component as that entity
while nothing emitted a record for it. It now calls the sibling. The FQN rule gets a
name (`fqJavaTypeName`), and the array-wrap rule gets one (`wrapsAsList`) so its two
call sites state it once instead of twice.

**One emitter for element-wise PATCH validation.** The map branch was the array branch
copy-pasted with a renamed loop variable, leaving the 400 envelope in three places.

**C#: gate the helper on what was emitted, not a second derivation of it.**
`NeedsMapJsonbHelper` hand-mirrored three emission sites and was wrong about two —
it undercounted them and did not model `jsonbObjectsOnly` suppressing the flattened
arm. Replaced by a scan of `modelLines`, which is complete at that point: exact by
construction. `MapJsonbSuffix` likewise keeps the top-level and flattened tiers from
drifting.

**The one behavior change — the emitted comparer.** It compared serialized JSON, which
is key-ORDER sensitive: a dictionary rebuilt in a different order read as changed and
would issue an UPDATE for a row nothing touched. It also serialized both dictionaries
on every equality check and deep-copied on every snapshot. Now entry-wise, with
scalars settling on `EqualityComparer<T>.Default` and never serializing; only a
value-object value falls through to JSON, because the generated POCO compares by
reference. The snapshot deep-copies only when the value type needs it. Pinned by test.

**Two KNOWN_GAPS files that had become false.** C# G9 ("a field.map member of a
flattened value object gets no EF column mapping") described the gap this work closed,
prescribed "do not use field.map inside a flattened value object", and pinned itself
to a test renamed two commits ago — while the generator's surviving warning still
says "See KNOWN_GAPS.md". Marked closed with what replaced it. G7's contract clause
called a map an owned navigation, which the converter path contradicts. The Java entry
said field.map was "still staged out, needs a persistence-conformance roundtrip column
first" — the codegen rung did not depend on that gate, but the gate is still open and
still right for the RUNTIME tier, which is now what it says.

**Tests.** A drift gate pins the map's `@valueType` table against `javaTypeName`'s own
arms, so adding a scalar subtype to one and not the other fails instead of shipping a
divergent map. The hand-rolled javac block became
`SpringTestFixtures.compileGenerated` — the 17th copy in the package, and now the
last one that needs writing. Dropped a test wholly subsumed by the table test and a
compile-fixture field covered by two other receivers.

Java 246 green on `clean test`; C# 442 + 1024 + 291 + 77 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…ettable

G7's "Today" paragraph claimed a VO-typed column is skipped on PATCH as a
deliberate cross-port Day-1 rule. Program D landed the opposite: the vanilla
PATCH/PUT handler passes voFields into AppendPartialMergeLoop, which emits
typed value-object arms AHEAD of the generic FindProperty arm (present value
-> deserialize + recursive VO validation + assign the CLR nav; present-null
clears a nullable column or 400s a @required one; absent -> untouched).
Re-scope the entry to the true residual — VO columns on TPH entities, whose
per-subtype path passes an empty VO list — and state explicitly that the
vanilla-path behavior may mean the entry is already closed except for TPH,
with the ruling deliberately deferred to issue #359 (closing needs Program
D's intent, which this file does not own).

Co-Authored-By: Claude Code <noreply@anthropic.com>
…mption that was only asserted

An audit of this branch's four load-bearing claims that came from reasoning rather
than from the code. Two verified against the source, one now has a real test, one is
deleted.

**Deleted — the Npgsql/hstore claim.** Two places asserted that an unmapped
`Dictionary<string,string>` binds to `hstore` on Npgsql and a `Dictionary<string,int>`
to nothing: the generator's map-loop comment and, worse, `docs/features/field-types.md`,
where an adopter reads it as fact. That came from model knowledge. It was never
measured against Npgsql, a live database, or the EF provider — and it conflates two
layers (Npgsql's ADO type mapping and what the EF provider does with an unmapped CLR
property), which are not the same question.

The FIX never depended on it, only the stated rationale did, so the rationale is now
what can be defended: without this mapping the property has no column type and no
converter, so what happens to it is the PROVIDER's business rather than the model's —
and the column the TS-owned migration creates is jsonb (ADR-0015), which only an
explicit mapping guarantees EF agrees with. That argument holds whatever any provider's
defaults turn out to be.

The claim also appears in two earlier commit messages on this branch. Those are pushed
and are not being rewritten — a rewritten SHA trips the pipeline's custody check — so
this commit is the correction of record.

**Tested — `@Valid` cascades into a Map's VALUES.** The generator emits `@Valid` on a
value-object map component entirely on the strength of that claim, and the only thing
gating it was an assertion that the annotation appears in the emitted source. That
tests for the presence of a string: if the cascade did not happen, the assertion would
still have passed while nested constraints went unenforced on every POST.

There is now a test that runs a real validator (Hibernate Validator is already in this
module's test scope) over the exact shape the generator emits — `@Valid` on a
`Map<String, Bean>` whose value type carries `@NotNull` — and asserts both that a
violation is raised and that its path names the nested member. Confirmed it gates:
removing the annotation makes it fail with the message naming the assumption.

**Verified, kept as written** — the two claims the pipeline's document round wrote:
`RoutesGenerator.AppendArrayNullClears` really does emit a post-save
`UPDATE ... SET <col> = NULL` for a nullable array-of-VO, and Kotlin's VO PATCH really
does bind through Jackson `treeToValue`. Both read in the source rather than taken on
the finding's word.

Java 247 green on `clean test`; C# 442 + 1024 + 291 + 77 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…values unvalidated

A `KNOWN_GAPS.md` entry is the wrong home for this one. The other gaps describe
things an adopter can see in their own source or schema; this is GENERATED code that
silently accepts invalid nested values, so reading their own source will never reveal
it and the failure mode is acceptance rather than an error. It belongs where someone
decides to use `field.map`, not only where someone goes looking for gaps.

Two places now carry it:

- `docs/features/field-types.md`, in the `field.map` section itself, as a callout
  before the runtime-tier caveat.
- The authoring skill's ladder guidance, which an agent reads when choosing the rung.
  That one also had a claim this makes false: it said the rung "is safe where
  generated code is the consumer" — generated code as the consumer is precisely
  where this bites. Corrected rather than merely appended to.

The substance, verified in source rather than inferred: the TPH settable set is
`scalarFields` minus pk/discriminator/auto-set and `scalarFields` skips only
`ObjectField`, so a map is in it; the TPH write paths validate per field with
`validateValue`, which does not cascade `@Valid`; and `appendValueObjectValidation`
has exactly one call site, on the vanilla handler. Scalar-valued maps are unaffected —
no nested bean exists to validate.

Newly reachable rather than a regression: before the `MapField` type-mapper arm, a
map-bearing entity failed Java codegen outright, so no shipped model can have been
using the path. That is why it does not block the release, and why it still needs to
be loud. Tracked as issue #362.

Expected-skill goldens regenerated; the sdk corpus test is green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sjn9TaHoZFdDPF2dLcppv
…nvalidated-write fixes

Drives the real generated C# server (Roslyn-compiled generator output hosted on
Kestrel over Testcontainers Postgres) rather than reading source:

- An_enum_valued_map_component_persists_its_symbol_not_its_ordinal — proves the
  MapJsonb JsonStringEnumConverter fix (85f87c1) actually persists a
  field.map @objectref VO's enum members as their symbol, by reading the raw
  jsonb column back with a direct SQL query independent of the app's own
  reader.
- A_map_valued_address_missing_its_required_street_is_silently_accepted_on_create
  — confirms live the exact claim docs/features/field-types.md now carries:
  a nested VO value inside a field.map is unvalidated on the C# create path
  (201, not 400).

Both passed when run against a live Postgres container in this validation
round, alongside the pre-existing MapNullColumnGeneratedServerTest
(NULL-column read/list/PATCH regression, also verified live).
@dmealing
dmealing force-pushed the fm/mo-fieldmap-jsonb-k4 branch from de40933 to 10cd44d Compare September 10, 2026 07:02
@dmealing dmealing changed the title fix(codegen): close the field.map codegen gap in the Java and C# ports fix(codegen): complete field.map codegen for the Java and C# ports Sep 10, 2026
@dmealing
dmealing merged commit a469f9e into main Sep 10, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-fieldmap-jsonb-k4 branch September 10, 2026 07:06
dmealing added a commit that referenced this pull request Sep 10, 2026
…tamodel attr

`ts-unit` has been failing on main since #360 (run 34448286129, the only red job
in that run). The audit skill's new `field.map` rung explains that per-field
`validateValue` does not cascade `@Valid` on Java TPH write paths — and the
grounding test, which scans code spans for `@attr` tokens and demands each be
registered vocabulary, convicted it.

The gate is right to be strict: it exists because a shipped skill once taught
metadata the loader rejects. This is the false-positive class it already carries
an exemption list for, alongside `@RestController` and `@Serializable` — a
framework annotation named in prose about a port. `@Valid` joins them with a
comment saying which rung names it and why, per the list's convention.

Fixing rather than reporting: main is red for everyone until this lands, and the
verdict is unambiguous — `jakarta.validation.Valid` is not, and will never be,
registered MetaObjects vocabulary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
dmealing added a commit that referenced this pull request Sep 10, 2026
…uld have mis-attributed 1.0.0

`## [Unreleased]` sat BELOW `## [1.0.0]`, left behind by the 1.0 cut: the cut wrote a
short framing entry and never converted the old heading under it, so ~1200 lines
of already-shipped 1.0.0 detail were still filed as unreleased.

That is not cosmetic. `scripts/release.mjs` promotes by inserting the new version's
heading directly after `## [Unreleased]` — so cutting 1.0.1 against this file would
have relabelled the whole 1.0.0 body as 1.0.1. The dry run did exactly that, which
is how it was found.

Fixed by putting each entry under the release that shipped it: #360's `field.map`
entry — the only `###` git shows added since v1.0.0 — moves into [1.0.1], the stale
heading is dropped so the rest folds back under [1.0.0] where it belongs, and a
fresh empty [Unreleased] goes on top for the next cut. My own one-line field.map
bullet is removed as redundant now that #360's full entry sits in the section.

Content is preserved: net -3 lines, and [1.0.0] keeps all 43 of its entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
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