@@ -63,6 +63,124 @@ an object with a read-only primary source beside a writable one, which `object.e
6363`object.projection` each refuse and which only ever loaded because `object.base` carries neither
6464rule.
6565
66+ ### Fixed — BEHAVIOUR: `@schema` is honoured by every table, view and callable binding
67+
68+ **A `@schema` object's generated code addressed the wrong schema.** `migrate-ts` creates it
69+ where it says — `CREATE TABLE "sales"."widgets"` — and under ADR-0015 it owns where a table
70+ lives. No code generator in any port read the attribute: the names artifact carried it in
71+ TypeScript, C#, Kotlin and Java, and every binding then dropped it, so generated queries
72+ addressed `public.widgets` while the migration built `sales.widgets`.
73+
74+ Whether that surfaced depended entirely on the adopter's `search_path` — configured one way
75+ the generated code silently read a DIFFERENT table, configured another it worked. **This is
76+ therefore a behaviour change on upgrade for anyone whose `search_path` was masking it**: their
77+ generated code will now address the schema the model declares. Nothing in this repo sets
78+ `search_path`, so there was no layer compensating; three shipped conformance fixtures declare
79+ a non-default `@schema`, so the shape is not theoretical. Pin the old behaviour by removing
80+ `@schema` (and letting the connection resolve the name), not by configuration.
81+
82+ Each port binds it in the form its target expresses, through the constant:
83+ `pgSchema(WidgetNames.schema).table(…)` in TypeScript (a different CALL shape, not a third
84+ argument), `[Table(WidgetNames.Name, Schema = WidgetNames.Schema)]` in C#, and
85+ `Table(WidgetNames.SCHEMA + "." + WidgetNames.NAME)` in Kotlin. Views and stored-proc
86+ wrappers are included — fixing tables alone would leave the two halves disagreeing with each
87+ other as well as with the database. sqlite/d1 are excluded and that is correct rather than a
88+ gap: SQLite has no schema concept and migrate already refuses a non-default `@schema` there.
89+
90+ A write-through entity's **replica view** takes its own schema, not the entity's: the object's
91+ primary source there is the WRITABLE table, so the obvious lookup would have qualified the
92+ view with the write table's schema — a name paired with someone else's schema, which is worse
93+ than no schema at all.
94+
95+ **The gate said `@schema` was unread, and that was false when it was written.** The C# row
96+ asserted "NO generator reads it" while `CallableGenerator` had been reading it and splicing it
97+ into emitted SQL as a spelled literal since before this work stream; the row still read
98+ `dropped` only because the fixture put `@schema` on a plain entity and not on the one
99+ generator that qualified. It is on both now. The TypeScript row's "NO generator anywhere reads
100+ it" was wrong for a different reason — the projection view-DDL builder has always read it.
101+
102+ ### Fixed — a jsonb value-object enum was stored as EF's integer ordinal (C#)
103+
104+ A value object stored as owned JSON had no enum conversion on either `.ToJson` arm, so EF Core
105+ 8's default applied and a member persisted as its **ordinal** — `{"mood": 0}` — where
106+ TypeScript, Java, Kotlin and Python all store the member SYMBOL. A silent cross-port wire
107+ break, and a positionally fragile one: reordering `@values` silently re-maps already-stored
108+ rows, which is exactly why `@intValueMap` was specified as a map rather than an array.
109+
110+ The conversion is `HasConversion<string>()` unconditionally, deliberately **not** the
111+ `@intValueMap` pair the flattened arm uses: the int map is a COLUMN-storage concern and there
112+ is no column inside a JSON document. It reaches no port's value-object path, so routing it in
113+ here would have swapped one cross-port divergence for another.
114+
115+ ### Fixed — three flattened value-object member kinds bound a column no migration creates (C#)
116+
117+ `@storage: flattened` spreads a value object onto the owning table as
118+ `<parent column>_<member column>`, and migrate's `flattenObjectField` iterates the value
119+ object's fields **unconditionally**. The generator named scalars and non-array enums only, so
120+ an array enum and a nested value object fell through to EF's `<Nav>_<Prop>` default and bound
121+ a column the migration never creates (Postgres `42703`). Both are now named. `field.map` is
122+ still not, deliberately — this port configures a top-level `field.map` nowhere either, so
123+ there is no proven mapping to mirror and forcing one risks a broken model build; it now emits
124+ a **gen-time warning** naming the member and the exact column migrate will create, and is
125+ recorded as G9 in the port's `KNOWN_GAPS.md`.
126+
127+ ### Fixed — the gate over the gates counted PROSE as coverage, and inverted its own verdict
128+
129+ `scripts/check-no-magic-gate-coverage.sh` matched each shape marker against the whole gate
130+ file, and a javadoc line reading ``contains {@code "object.value"} as a substring`` satisfied
131+ `java:value_object` on its own — so the ledger would have reported "no gap" with both real
132+ declarations deleted. Markers now match with comment-only lines stripped.
133+
134+ The first fix carried a worse bug than the one it fixed: piping the stripper into `grep -q`
135+ under `set -o pipefail` meant a MATCH was reported as a GAP, because `grep -q` closes the pipe
136+ on its first hit and the upstream dies of SIGPIPE. It briefly reported 22 gaps that did not
137+ exist. Same inversion as the pre-release pin detector's `grep | head`, and the more dangerous
138+ half of the pair — a check that inverts is worse than one that merely misses.
139+
140+ ### Fixed — four of five gates asserted "the fixture loads clean" against a LAX loader
141+
142+ Every no-magic gate carries the line *"A gate whose fixture the loader would reject proves
143+ nothing"* above an `errors == []` assertion. In TypeScript, C#, Python and Kotlin that
144+ assertion could not fail on an unknown attribute, because the loader was not strict — so it
145+ proved the fixture PARSES, never that it is legal under the sealed registry (ADR-0023), which
146+ is the rule an adopter's model faces. All four now load strict; all four fixtures were already
147+ clean under it, so no fixture changed — only what the gate is entitled to claim.
148+
149+ Kotlin needed an API fix, and that is the durable half: `loadString` was the one loader entry
150+ point with **no options parameter**, while its own siblings `loadUris`/`loadResources` took
151+ `LoaderOptions` precisely so a caller could ask for strict. An inline model is what every
152+ codegen test loads, so the one shape that could never be checked strictly was the one shape
153+ the tests all use. `MetaDataLoader.fromString` and the ktx `loadString` now take options and
154+ pass them through; the existing 3-arg forms delegate with null.
155+
156+ ### Fixed — FR-015 was documented as shipping cross-port, and Kotlin bound an unregistered attribute
157+
158+ `AGENTS.md` claimed the projection codegen fan-out including "FR-015 proc-callables" was
159+ "shipped cross-port". It was not: callable generators exist in TypeScript and C# only, **Java
160+ and Python have none at all**, and Kotlin's bound its arguments from `@param`, an attribute
161+ that port invented and no provider registers. Under the sealed registry no metadata carrying
162+ it can load, so the predicate answered false for every field of every loadable model: every
163+ wrapper was silently zero-argument, while an adopter following the documented contract and
164+ declaring `@parameterRef` had their arguments ignored without a word.
165+
166+ Kotlin now binds the registered `@parameterRef`; `@param` is deleted from the repository
167+ rather than merely unread. The claim is corrected, and the Java and Python shortfall is
168+ recorded in each port's `KNOWN_GAPS.md` — building those two generators is feature work and is
169+ additionally blocked on `@exposeAsRoute`, which the originating plan left explicitly undecided.
170+
171+ ### Fixed — an index's physical attributes never reached the generated schema (TypeScript)
172+
173+ The two index emitters were reconciled on NAME earlier in this line and still disagreed on
174+ SHAPE. `@expr`, `@using`, `@orders` and `@where` all reach migrate and none reached codegen:
175+ an `@expr`-only index emitted **nothing** (both loops required a non-empty `@fields` while
176+ migrate keys an expression index off `@expr`), `@where` never arrived so a PARTIAL unique index
177+ read as a **fully** unique one, and `@orders` was dropped so a DESC index read as ASC. The last
178+ had a test pinning it — asserting the direction was absent, over a fixture whose own comment
179+ says "customerId + placedAt DESC".
180+
181+ The two near-identical loops are now one function, because a rule written twice is a rule that
182+ can be forgotten twice, which is how these came to be honoured by neither.
183+
66184### Added — every physical database name has ONE spelling per run, in all five ports
67185
68186A generated per-object artifact now carries an object's physical database names — the
0 commit comments