Skip to content

Commit e075dab

Browse files
dmealingclaude
andcommitted
docs(authoring): the jsonb ladder still taught the #362 gap 1.0.1 closed
The authoring skill's `field.map` caveat told readers that Java skips nested value-object validation on TPH write paths and that "C# validates them on NO write path", and pointed at #362 as a live gap — advising against the rung for a Java TPH entity "or for C# at all". 1.0.1 fixed all of that. The audit skill and docs/features/field-types.md were both updated at the fix; this one was missed. It is the worst of the three to leave stale, because it is the instrument an author uses to CHOOSE a rung — a closed gap presented as open steers a correct `field.map` declaration down to an untyped bag, which is the one column shape the ladder exists to replace. Reframed the way the audit skill already puts it: this is a VERSION question, not a port question. Every port validates on every write path as of 1.0.1; on 1.0.0 the hole is open, so an adopter's pinned version is what decides whether they still need their own boundary check — and one written while it was open is now redundant rather than load-bearing. The mechanism is kept, in the past tense, because the failure mode was silent acceptance and someone diagnosing an old row still needs to recognise it. Regenerated fixtures/agent-context-conformance/ in the same commit: the authoring skill lands in all five stacks, so an edit without the regen turns ts-unit red on the next push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent ed78233 commit e075dab

6 files changed

Lines changed: 90 additions & 78 deletions

File tree

  • agent-context/skills/metaobjects-authoring
  • fixtures/agent-context-conformance
    • java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring
    • java-react/expected/.claude/skills/metaobjects-authoring
    • python/expected/.claude/skills/metaobjects-authoring
    • ts-react-tanstack/expected/.claude/skills/metaobjects-authoring
    • ts-requirements/expected/.claude/skills/metaobjects-authoring

agent-context/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -584,19 +584,21 @@ Python's `ObjectManager` encodes a map today; `runtime-ts`, Java's OMDB and the
584584
lane carry no map handling at all. So a map you intend to read back through a PORT RUNTIME is
585585
still better declared as a value object, and a genuinely dynamic key set stays a bag.
586586

587-
**One sharp edge where generated code IS the consumer: nested map values can be written
588-
UNVALIDATED, per port.** TypeScript (`z.record` over the VO's insert schema) and Python
589-
(`dict[str, VO]` Pydantic) validate map values, and Kotlin writes no map column. **Java**
590-
validates them on its vanilla create/PATCH handlers but NOT on TPH (discriminator-rooted) write
591-
paths — those validate field-by-field with `validateValue`, which does not cascade `@Valid`.
592-
**C# validates them on NO write path** — vanilla create, vanilla PATCH, and TPH alike: the map
593-
never reaches the recursively-validating value-object arms (they admit `field.object` only),
594-
and the generic arms check the dictionary property itself, never its values. A posted value
595-
violating the referenced `object.value`'s constraints is accepted and written, silently, and
596-
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597-
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598-
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
587+
**One sharp edge where generated code IS the consumer, and it is now a VERSION question rather
588+
than a port question: nested map values.** As of **1.0.1** every port validates them on every
589+
write path ([#362](https://github.com/metaobjectsdev/metaobjects/issues/362)). On **1.0.0 the
590+
hole is open**, so check the adopter's pinned version before recommending this rung to a Java
591+
or C# consumer: Java validated nested map values on its vanilla create/PATCH handlers but not
592+
on TPH (discriminator-rooted) write paths, which validate field-by-field with `validateValue`
593+
and do not cascade `@Valid`; C# validated them on no write path at all — the map never reached
594+
the recursively-validating value-object arms (they admit `field.object` only), and
595+
`ValueObjectValidator` treated a `Dictionary` as a plain `IEnumerable`, validating
596+
`KeyValuePair` structs instead of the values. TypeScript and Python were never affected, and
597+
Kotlin writes no map column. Scalar-valued maps (`@valueType`) carry no nested bean and were
598+
never affected on any version. **The failure mode was silent acceptance, not an error** — a
599+
POST or PATCH carrying an invalid nested value returned 201/200 and wrote the row — so an
600+
adopter still on 1.0.0 needs their own boundary check, and one written while this was open is
601+
now redundant rather than load-bearing. Every rung but the first keeps the
600602
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
601603
migration — read the emitted DDL before promising that.
602604

0 commit comments

Comments
 (0)