@@ -90,6 +90,133 @@ edit (two registered `description` strings) and was ruled a hold, as 1.0.4's was
9090 nested ref names the hop that broke (*"'app::Task.title' has no member 'display'."*). The
9191 did-you-mean hint still appears when the object itself does not resolve.
9292
93+ - **C#: a `field.decimal` reached through a nested object generated code that did not
94+ compile.** `ExtractDelegateEmitter.ScalarReader` — the reader map for the runtime-DELEGATING
95+ extract mirror — had no `Decimal` branch, so a decimal fell through to the `DlgString`
96+ default while `Fr010FieldMapping.ScalarMirrorType` typed the same mirror property `decimal?`:
97+ `error CS0029: Cannot implicitly convert type 'string' to 'decimal?'`. A decimal **array**
98+ broke the same way with `CS0266` (`IReadOnlyList<string?>` → `IReadOnlyList<decimal?>`),
99+ because the delegating mirror kind-types array elements through `ScalarMirrorType` too. Only
100+ the delegating path was affected — that is the path for a field reached through a nested
101+ object or an array-of-objects, so a flat top-level payload went through the self-contained
102+ reader, which always had its Decimal branch. Fixed by adding the branch plus a `DlgDecimal`
103+ helper mirroring `ExtractMap.AsDecimal`'s never-throws contract (an out-of-range double
104+ degrades to null rather than throwing `OverflowException` mid-parse, which is the point of a
105+ *lenient* extract). `ScalarMirrorType`'s doc-comment had claimed the self-contained and
106+ delegating mirrors were "in lock-step" with nothing testing it; a new gate now compiles
107+ generated output for **every** scalar subtype in `FIELD_SUBTYPES`, in both the single and the
108+ array position, so a newly registered subtype cannot quietly take a mismatched default.
109+
110+ - **C#: the generated `AppDbContext` failed to compile when an entity name collided with a
111+ `DbSet` property name.** The reference-FK configuration named its column through
112+ `nameof(<Owner>.<Prop>)`, an expression sitting inside the `DbContext` class body — where C#
113+ simple-name lookup binds `<Owner>` to a *member* of the context before it considers a type of
114+ the same name, and the context declares one `DbSet` per entity. Reachable from stock
115+ metadata, not just exotic input: `Pluralize("Address") == "Addresses"`, so a model carrying
116+ both an `Address` and an `Addresses` entity emitted `nameof(Addresses.AddressId)`, bound
117+ `Addresses` to `DbSet<Address>`, and failed with `CS1061`. Now emitted as a typed lambda —
118+ `.HasForeignKey(e => e.AddressId)`, or `.HasForeignKey(e => new { e.OrgId, e.SiteId })` for a
119+ composite — whose parameter is local and therefore unshadowable, and which unlike a bare
120+ string literal keeps the property name compile-checked. The M:N `UsingEntity<Through>(...)`
121+ sides carried the identical hazard and were converted too.
122+
123+ - **`meta init --refresh-docs` no longer reports a scaffold it did not perform.** On an
124+ established project the refresh-only path did the right thing — agent context refreshed in
125+ place, metadata, `.metaobjects/config.json`, `codegen/generators/` and
126+ `metaobjects.config.ts` all left alone, nothing created — but then printed the full first-run
127+ banner: "Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts", "Codegen
128+ generators copied to codegen/generators/", and next-steps telling you to set
129+ `"type": "module"`. It read as though the command had just scaffolded over your project. The
130+ output now reports the refresh and states what was left untouched. `--refresh-docs` on a
131+ repo that is *not* yet initialized still falls through to a full init and still prints the
132+ scaffold banner, so the branch keys off what actually happened rather than off the flag.
133+
134+ - **Java: the generated extract mapper did not compile for most scalar subtypes.** A
135+ responding `template.prompt` types its `<Template>Response` record through
136+ `SpringTypeMapper.javaTypeName`, which is richly kind-typed (`BigDecimal`, `LocalDate`,
137+ `LocalTime`, `Instant`, `UUID`, `URI`, `InetAddress`, `Float`, `Long` for currency) — while
138+ `SpringOutputParserGenerator`'s mapper recognised only Integer/Long/Double/Boolean and fell
139+ through to `ExtractMap.asString` for everything else, with **every** scalar array going to
140+ `asStringList`. Each mismatch is `incompatible types` at compile time: **9 of 15 scalar
141+ subtypes as a single component, 13 of 15 as an array**. It shipped because no test payload in
142+ the module carried a decimal, date, time, timestamp, currency, uuid, uri or inet — and
143+ because javac reports only the FIRST bad argument of a constructor call, so even a fixture
144+ with several would have looked like one defect (which is exactly how it was first reported,
145+ as a decimal-only bug). The record stays strictly typed per ADR-0052; the generated parser now
146+ coerces on the way in, with never-throws helpers emitted only where used. A new per-subtype
147+ gate compiles generated output for every subtype in both positions.
148+
149+ - **Java: `MetaObjectExtractor.assemble` violated its own documented never-throws contract.**
150+ Its javadoc promises the assembled object comes back with malformed data already classified
151+ into the report and no exception — but the per-field write goes through `DataConverter`, which
152+ throws on a value it cannot convert. One unconvertible component therefore destroyed the
153+ entire extract, the opposite of the lenient tier's purpose: a model answering `"31/12/2026"`
154+ for one date should cost you that date, not the twenty fields beside it that parsed. The
155+ component is now left unset, which is what "lost" already means everywhere else in that pass.
156+
157+ - **Java: an unparseable decimal became `0` instead of null.** `DataConverter.toBigDecimal`
158+ caught the `NumberFormatException` and returned `BigDecimal.ZERO` — the one wrong answer
159+ available, because `0` is a plausible amount, so unreadable input reached a money field as a
160+ real-looking value with nothing to distinguish it from a genuine zero. Every sibling
161+ converter (`toInt` / `toLong` / `toDouble` / `toFloat`) throws for the same input; decimal
162+ was the lone exception. It now throws too, and the lenient extract tier's never-throws guard
163+ turns that into a lost component — so the answer there is `null`, which is the honest one.
164+
165+ - **Java/Kotlin: a declared `field.decimal @isArray` was silently dropped.** `DataTypes` had no
166+ `DECIMAL_ARRAY`, and `arrayTypeFor(DECIMAL)` mapped decimal to ITSELF with a comment saying
167+ arrays-of-decimal "aren't in the metamodel" so the declaration would "degrade gracefully". It
168+ did not degrade gracefully: the loader accepts the declaration, all five ports generate code
169+ for it, and C# populates it correctly (now pinned by value in
170+ `Fr010DelegatingMirrorLockStepTests`) — so this was a JVM gap presented as a design position,
171+ not a metamodel rule. Added `DECIMAL_ARRAY` plus a `toBigDecimalArray` that element-converts
172+ through `toBigDecimal` rather than a double, so precision survives the round trip. The new
173+ member takes id 22 rather than a slot beside `DECIMAL=10`: those ids are persisted
174+ identifiers, and renumbering the existing members to keep the enum tidy would silently
175+ repoint stored data. Kotlin inherits the fix — it consumes the same `metadata` module — and
176+ C#, TypeScript and Python needed no change (that conversion table is JVM-only; TS binds
177+ `field.decimal` to `string` and Python to `Decimal`).
178+
179+ - **Java: `DataConverter.toDate` could not parse an ISO-8601 date.** The `String` arm was a bare
180+ `Long.parseLong`, so it accepted epoch milliseconds and nothing else — a plain `"2026-03-04"`
181+ threw `NumberFormatException`. Since that is the form `normalization.md` puts on the wire, no
182+ `field.date` or `field.timestamp` could be assembled from its own wire representation. It now
183+ accepts epoch millis (tried first, and only for an all-digit string, so every value that
184+ parsed before still parses identically) plus ISO instant, zone-less date-time, and date-only
185+ forms. Unparseable input still throws, preserving the failure mode for callers outside the
186+ lenient tier.
187+
188+ ### Added
189+
190+ - **C#: `DbContextGenerator.EmitsReferenceForeignKeys`, an opt-out for the reference-FK
191+ configuration.** The navigation-less `HasOne<Target>()` overload that configuration uses is
192+ correct *because* the stock entity generator emits no reference navigations (ADR-0038
193+ replaced reverse navigation with explicit FK finders) — a premise this generator cannot
194+ detect, and one that does not hold for an adopter who substitutes their own entity generator
195+ and emits navigations. There, EF's conventions discover those navigations and build their own
196+ relationship over the same FK column, and a navigation-less config claiming that column
197+ leaves the convention-built one unable to identify its dependent ("The dependent side could
198+ not be determined for the one-to-one relationship between 'X.Y' and 'Y.X'") — failing model
199+ validation, which takes down every query in the application rather than just that
200+ relationship. Override the property to `false` and configure those relationships yourself;
201+ nothing else the generator emits changes. Documented in
202+ [`relationships.md`](docs/features/relationships.md).
203+
204+ - **The extract engine's `FieldKind` vocabulary is now gated across ports**
205+ (`scripts/check-extract-field-kinds.mjs`, `gates` lane). Every port's extract engine runs
206+ `fixtures/extract-conformance/`, and that corpus's `schema.json` `kind` values ARE this
207+ vocabulary — so a port that adds or drops a kind changes what the shared corpus can express
208+ while every existing fixture keeps passing. The drift is invisible by construction, and it had
209+ already happened: C# carries a `Decimal` kind no other port has and no fixture exercises. The
210+ gate reads each port's real definition rather than a copied list (an unrecognised declaration
211+ shape fails loudly instead of reporting an empty set, which would read as "no drift"), and it
212+ fails if Kotlin ever grows its own `FieldKind` — it ships no extract engine, driving the shared
213+ Java one. Deviations pass only when
214+ [`expected-field-kinds.json`](fixtures/extract-conformance/expected-field-kinds.json) records
215+ them with a reason; the C# one is recorded there, including why deleting it is not the
216+ one-liner it looks like (`ExtractMap.AsDecimal` has no string arm, so an engine that stopped
217+ producing `decimal` would silently return null for every decimal). A recorded deviation that
218+ is no longer true also fails, so the record cannot go stale.
219+
93220## [1.0.4] — 2026-09-14
94221
95222_All four registries publish: npm `1.0.4` (full lockstep across all 14 `@metaobjectsdev/*`
0 commit comments