Skip to content

Commit 38afbe4

Browse files
committed
no-mistakes(review): fix stale #362 scoping; null-guard MapJsonb comparer Hash/Snap
1 parent 3914a01 commit 38afbe4

15 files changed

Lines changed: 121 additions & 28 deletions

File tree

‎agent-context/skills/metaobjects-audit/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
366366

367367
**Do NOT flag:** a bag whose readers genuinely pass it through (logged, echoed, stored, forwarded to a third party) without reading a key; a third-party or LLM raw response whose shape is not the adopter's to declare; an **array of scalars**, which is `isArray: true` on the base subtype and never an object; a bag the metadata's own comment justifies as open with a reason.
368368

369-
**Remedy — a ladder, chosen by the WRITER's declared type, and every rung gives the consumer a TYPED HANDLE rather than a string to get and set.** `list[X]` ⇒ the base subtype + `isArray: true`. `dict[str, X]` with a known `X` ⇒ **`field.map` + `@objectRef`** (a value object) or `@valueType` (a scalar) — registered vocabulary that emits `jsonb("col").$type<Record<string, X>>()` and `z.record(z.string(), X)`. **Check the CONSUMER before recommending this rung: `field.map` codegen now completes on all five ports** (Java types it `java.util.Map<String, V>`; C# emits the `Dictionary<string, V>` property and its EF jsonb storage mapping), **but no persistence- or api-contract-conformance fixture exercises `field.map` on ANY port, and the runtime persistence tier is uneven** — only Python's `ObjectManager` encodes a map, while `runtime-ts`, Java's OMDB and the Kotlin Exposed lane carry none. So where GENERATED CODE is the consumer, check the port before recommending this rung — a `field.map @objectRef` writes its nested value-object values **unvalidated** on Java TPH (discriminator-rooted) write paths (per-field `validateValue` does not cascade `@Valid`) and on EVERY C# write path — vanilla create, vanilla PATCH, and TPH alike (the map never reaches the recursively-validating value-object arms; the generic arms check the dictionary property itself, never its values). TypeScript (`z.record` over the VO's insert schema) and Python (`dict[str, VO]` Pydantic) validate map values, Kotlin writes no map column, and scalar-valued maps (`@valueType`) are unaffected everywhere. Recommend the rung for a Java TPH entity or for C# only with that said and the adopter validating map values at their own boundary before write; [issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the Java TPH half, and the C# scope is wider than that issue. Prefer a value object where a PORT RUNTIME must read the column back; a genuinely dynamic key set stays a bag. A serialized DTO / dataclass / `@Serializable` class ⇒ declare it as an `object.value` and point the field at it with `field.object` + `@objectRef` + `@storage: jsonb`. Only `dict[str, Any]` whose readers pin no key stays an open bag. **What the adopter GETS, per port** — this is the point of the conversion and worth stating in the finding: TypeScript emits the Drizzle column as `.$type<VO>()` plus the VO's own Zod schema and inferred type; Python annotates the field with the referenced VO's Pydantic model (and its `<Ref>Create` on the wire, so a nested violation still validates); Kotlin emits typed Exposed jsonb codecs backed by a shared Jackson `MetaJsonbMapper`. In none of them does the consumer touch a JSON string — that is what the open bag costs them today, since `unknown` forces every reader to cast. The column stays jsonb, so this is a codegen/contract change and not necessarily a migration; what changes is that `unknown` becomes a generated type, the casts delete, and the keys become metadata-owned — which also promotes their literals into drift signature 11's scope, where the `->>'key'` sites were previously exempt because the metadata owned nothing. **Verify the column type is genuinely unchanged before promising no migration**, and parity-gate each converted read against the raw-SQL one before deleting it. Convert one column at a time: the shape is a judgment per bag, and a wrong guess declares a contract the writers do not honor.
369+
**Remedy — a ladder, chosen by the WRITER's declared type, and every rung gives the consumer a TYPED HANDLE rather than a string to get and set.** `list[X]` ⇒ the base subtype + `isArray: true`. `dict[str, X]` with a known `X` ⇒ **`field.map` + `@objectRef`** (a value object) or `@valueType` (a scalar) — registered vocabulary that emits `jsonb("col").$type<Record<string, X>>()` and `z.record(z.string(), X)`. **Check the CONSUMER before recommending this rung: `field.map` codegen now completes on all five ports** (Java types it `java.util.Map<String, V>`; C# emits the `Dictionary<string, V>` property and its EF jsonb storage mapping), **but no persistence- or api-contract-conformance fixture exercises `field.map` on ANY port, and the runtime persistence tier is uneven** — only Python's `ObjectManager` encodes a map, while `runtime-ts`, Java's OMDB and the Kotlin Exposed lane carry none. So where GENERATED CODE is the consumer, check the port before recommending this rung — a `field.map @objectRef` writes its nested value-object values **unvalidated** on Java TPH (discriminator-rooted) write paths (per-field `validateValue` does not cascade `@Valid`) and on EVERY C# write path — vanilla create, vanilla PATCH, and TPH alike (the map never reaches the recursively-validating value-object arms; the generic arms check the dictionary property itself, never its values). TypeScript (`z.record` over the VO's insert schema) and Python (`dict[str, VO]` Pydantic) validate map values, Kotlin writes no map column, and scalar-valued maps (`@valueType`) are unaffected everywhere. Recommend the rung for a Java TPH entity or for C# only with that said and the adopter validating map values at their own boundary before write; [issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Prefer a value object where a PORT RUNTIME must read the column back; a genuinely dynamic key set stays a bag. A serialized DTO / dataclass / `@Serializable` class ⇒ declare it as an `object.value` and point the field at it with `field.object` + `@objectRef` + `@storage: jsonb`. Only `dict[str, Any]` whose readers pin no key stays an open bag. **What the adopter GETS, per port** — this is the point of the conversion and worth stating in the finding: TypeScript emits the Drizzle column as `.$type<VO>()` plus the VO's own Zod schema and inferred type; Python annotates the field with the referenced VO's Pydantic model (and its `<Ref>Create` on the wire, so a nested violation still validates); Kotlin emits typed Exposed jsonb codecs backed by a shared Jackson `MetaJsonbMapper`. In none of them does the consumer touch a JSON string — that is what the open bag costs them today, since `unknown` forces every reader to cast. The column stays jsonb, so this is a codegen/contract change and not necessarily a migration; what changes is that `unknown` becomes a generated type, the casts delete, and the keys become metadata-owned — which also promotes their literals into drift signature 11's scope, where the `->>'key'` sites were previously exempt because the metadata owned nothing. **Verify the column type is genuinely unchanged before promising no migration**, and parity-gate each converted read against the raw-SQL one before deleting it. Convert one column at a time: the shape is a judgment per bag, and a wrong guess declares a contract the writers do not honor.
370370

371371
---
372372

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,7 @@ violating the referenced `object.value`'s constraints is accepted and written, s
596596
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597597
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598598
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the Java TPH half, and the C# scope is wider
600-
than that issue. Every rung but the first keeps the
599+
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
601600
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
602601
migration — read the emitted DDL before promising that.
603602

‎docs/features/field-types.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Three rows need a footnote:
6262
> validate. This is generated code, so **reading your own source will not reveal it** — the
6363
> failure is silent acceptance, not an error. Validate map values at your own boundary
6464
> before write. [Issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks
65-
> the Java TPH half; the C# surface is wider than that issue's scope.
65+
> the gap.
6666
6767
**The RUNTIME tier is not there yet, and no conformance corpus covers it.** No
6868
persistence- or api-contract-conformance fixture exercises `field.map` on any port; it is

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
366366

367367
**Do NOT flag:** a bag whose readers genuinely pass it through (logged, echoed, stored, forwarded to a third party) without reading a key; a third-party or LLM raw response whose shape is not the adopter's to declare; an **array of scalars**, which is `isArray: true` on the base subtype and never an object; a bag the metadata's own comment justifies as open with a reason.
368368

369-
**Remedy — a ladder, chosen by the WRITER's declared type, and every rung gives the consumer a TYPED HANDLE rather than a string to get and set.** `list[X]` ⇒ the base subtype + `isArray: true`. `dict[str, X]` with a known `X` ⇒ **`field.map` + `@objectRef`** (a value object) or `@valueType` (a scalar) — registered vocabulary that emits `jsonb("col").$type<Record<string, X>>()` and `z.record(z.string(), X)`. **Check the CONSUMER before recommending this rung: `field.map` codegen now completes on all five ports** (Java types it `java.util.Map<String, V>`; C# emits the `Dictionary<string, V>` property and its EF jsonb storage mapping), **but no persistence- or api-contract-conformance fixture exercises `field.map` on ANY port, and the runtime persistence tier is uneven** — only Python's `ObjectManager` encodes a map, while `runtime-ts`, Java's OMDB and the Kotlin Exposed lane carry none. So where GENERATED CODE is the consumer, check the port before recommending this rung — a `field.map @objectRef` writes its nested value-object values **unvalidated** on Java TPH (discriminator-rooted) write paths (per-field `validateValue` does not cascade `@Valid`) and on EVERY C# write path — vanilla create, vanilla PATCH, and TPH alike (the map never reaches the recursively-validating value-object arms; the generic arms check the dictionary property itself, never its values). TypeScript (`z.record` over the VO's insert schema) and Python (`dict[str, VO]` Pydantic) validate map values, Kotlin writes no map column, and scalar-valued maps (`@valueType`) are unaffected everywhere. Recommend the rung for a Java TPH entity or for C# only with that said and the adopter validating map values at their own boundary before write; [issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the Java TPH half, and the C# scope is wider than that issue. Prefer a value object where a PORT RUNTIME must read the column back; a genuinely dynamic key set stays a bag. A serialized DTO / dataclass / `@Serializable` class ⇒ declare it as an `object.value` and point the field at it with `field.object` + `@objectRef` + `@storage: jsonb`. Only `dict[str, Any]` whose readers pin no key stays an open bag. **What the adopter GETS, per port** — this is the point of the conversion and worth stating in the finding: TypeScript emits the Drizzle column as `.$type<VO>()` plus the VO's own Zod schema and inferred type; Python annotates the field with the referenced VO's Pydantic model (and its `<Ref>Create` on the wire, so a nested violation still validates); Kotlin emits typed Exposed jsonb codecs backed by a shared Jackson `MetaJsonbMapper`. In none of them does the consumer touch a JSON string — that is what the open bag costs them today, since `unknown` forces every reader to cast. The column stays jsonb, so this is a codegen/contract change and not necessarily a migration; what changes is that `unknown` becomes a generated type, the casts delete, and the keys become metadata-owned — which also promotes their literals into drift signature 11's scope, where the `->>'key'` sites were previously exempt because the metadata owned nothing. **Verify the column type is genuinely unchanged before promising no migration**, and parity-gate each converted read against the raw-SQL one before deleting it. Convert one column at a time: the shape is a judgment per bag, and a wrong guess declares a contract the writers do not honor.
369+
**Remedy — a ladder, chosen by the WRITER's declared type, and every rung gives the consumer a TYPED HANDLE rather than a string to get and set.** `list[X]` ⇒ the base subtype + `isArray: true`. `dict[str, X]` with a known `X` ⇒ **`field.map` + `@objectRef`** (a value object) or `@valueType` (a scalar) — registered vocabulary that emits `jsonb("col").$type<Record<string, X>>()` and `z.record(z.string(), X)`. **Check the CONSUMER before recommending this rung: `field.map` codegen now completes on all five ports** (Java types it `java.util.Map<String, V>`; C# emits the `Dictionary<string, V>` property and its EF jsonb storage mapping), **but no persistence- or api-contract-conformance fixture exercises `field.map` on ANY port, and the runtime persistence tier is uneven** — only Python's `ObjectManager` encodes a map, while `runtime-ts`, Java's OMDB and the Kotlin Exposed lane carry none. So where GENERATED CODE is the consumer, check the port before recommending this rung — a `field.map @objectRef` writes its nested value-object values **unvalidated** on Java TPH (discriminator-rooted) write paths (per-field `validateValue` does not cascade `@Valid`) and on EVERY C# write path — vanilla create, vanilla PATCH, and TPH alike (the map never reaches the recursively-validating value-object arms; the generic arms check the dictionary property itself, never its values). TypeScript (`z.record` over the VO's insert schema) and Python (`dict[str, VO]` Pydantic) validate map values, Kotlin writes no map column, and scalar-valued maps (`@valueType`) are unaffected everywhere. Recommend the rung for a Java TPH entity or for C# only with that said and the adopter validating map values at their own boundary before write; [issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Prefer a value object where a PORT RUNTIME must read the column back; a genuinely dynamic key set stays a bag. A serialized DTO / dataclass / `@Serializable` class ⇒ declare it as an `object.value` and point the field at it with `field.object` + `@objectRef` + `@storage: jsonb`. Only `dict[str, Any]` whose readers pin no key stays an open bag. **What the adopter GETS, per port** — this is the point of the conversion and worth stating in the finding: TypeScript emits the Drizzle column as `.$type<VO>()` plus the VO's own Zod schema and inferred type; Python annotates the field with the referenced VO's Pydantic model (and its `<Ref>Create` on the wire, so a nested violation still validates); Kotlin emits typed Exposed jsonb codecs backed by a shared Jackson `MetaJsonbMapper`. In none of them does the consumer touch a JSON string — that is what the open bag costs them today, since `unknown` forces every reader to cast. The column stays jsonb, so this is a codegen/contract change and not necessarily a migration; what changes is that `unknown` becomes a generated type, the casts delete, and the keys become metadata-owned — which also promotes their literals into drift signature 11's scope, where the `->>'key'` sites were previously exempt because the metadata owned nothing. **Verify the column type is genuinely unchanged before promising no migration**, and parity-gate each converted read against the raw-SQL one before deleting it. Convert one column at a time: the shape is a judgment per bag, and a wrong guess declares a contract the writers do not honor.
370370

371371
---
372372

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,7 @@ violating the referenced `object.value`'s constraints is accepted and written, s
596596
reading the adopter's own source will not reveal it. Scalar-valued maps (`@valueType`) are
597597
unaffected. Do not recommend this rung for a Java TPH entity — or for C# at all — without
598598
saying so and pointing at boundary validation of map values before write;
599-
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the Java TPH half, and the C# scope is wider
600-
than that issue. Every rung but the first keeps the
599+
[issue #362](https://github.com/metaobjectsdev/metaobjects/issues/362) tracks the gap. Every rung but the first keeps the
601600
column jsonb, so moving a column up the ladder is a codegen/contract change rather than a
602601
migration — read the emitted DDL before promising that.
603602

0 commit comments

Comments
 (0)