You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(changelog): 0.24.1 absorbs #335, ships metamodelVersion 0.12, and says what it costs
Folds the whole-object rollup out of [Unreleased] and into [0.24.1], directly
after #342 so the version story reads in order: #342 claims 0.11, #335 takes
0.12, the release ships 0.12.
Three things the fold had to state rather than imply:
- The #342 paragraph claimed the release moves 0.10 -> 0.11. That became false
the moment #335 landed in the same cut. Rewritten: the relaxation CLAIMS 0.11,
the release SHIPS 0.12, and 0.11 exists only as an untagged intermediate on
main. The rule that a slot is spent by shipping it is kept, because collapsing
#335 back onto 0.11 to look tidier would have broken that rule an hour after
the same release stated it.
- #335's array filterable/sortable half makes previously-LOADING metadata fail
to load, inside a PATCH. The entry now carries the ruling (previously-wrong
acceptance, as in #342, the 0.19.1 @min clamp and the 0.21.6 like fix), the
bound (zero instances across 1321 JSON + 124 YAML), AND the residual exposure:
pre-1.0 ^0.24.x resolves a patch, so an adopter who wrote that form
independently auto-adopts a load error on npm update. Bounded is not zero.
- The requirements-first case study gets an entry rather than shipping as an
unannounced 240-line adopter-facing document, including its negative result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
### Fixed — an expression index was undeclarable, and the one spelling that loaded was half-ignored ([#342](https://github.com/metaobjectsdev/metaobjects/issues/342))
@@ -141,15 +61,20 @@ validator and the rest of the port could disagree about whether an index had a k
141
61
needs pull in opposite directions — normalization *fixes* the crash and *hides* the empty
142
62
array — so presence and content are asked as separate questions.
143
63
144
-
**`metamodelVersion` moves `0.10` → `0.11`** — `@fields` becomes optional on both node
145
-
types, which the gate classifies as additive. The package line stays a PATCH; that
64
+
**This relaxation claims `metamodelVersion``0.11`** — `@fields` becomes optional on both
65
+
node types, which the gate classifies as additive. The package line stays a PATCH; that
146
66
severance is exactly what [ADR-0035 Amendment 2](docs/RELEASING.md) is for, and post-1.0
147
-
the changelog is the only signal on the metadata axis. **This relaxation is `0.11`'s sole
148
-
claimant** — the slot is spent by shipping it, so the next registered-vocabulary change
149
-
moves to `0.12`, whatever else is in flight when it lands. Worth stating because it has
150
-
already been misread once: unreleased work on another branch touched the same number on
151
-
the reasoning that `0.11` was still unclaimed, which would have surfaced as a red gate at
152
-
the end of that work rather than at its start.
67
+
the changelog is the only signal on the metadata axis.
68
+
69
+
**The release itself ships `0.12`, because #335 below landed in this same cut and took the
70
+
next number, as the rule requires.** A vocabulary slot is spent by *shipping* it, so the
71
+
next registered-vocabulary change moves on regardless of what else is in flight — and
72
+
`0.11` therefore exists only as an intermediate commit state on `main` and is never tagged.
73
+
That is fine and needs no apology: version numbers do not have to be dense, and one release
74
+
still ships exactly one contract number. Recorded because the rule was nearly broken from
75
+
the other direction during the cut — collapsing #335 back onto `0.11` to make "one release,
76
+
one bump" look tidier would have violated the very rule this paragraph states, an hour
77
+
after stating it.
153
78
154
79
**Two things this took, worth recording.** The Java port needed a fix the other three did
155
80
not: beyond the registry declaration, `ValidationPhase.validateIdentityNode` carried a
@@ -175,6 +100,101 @@ same blindness that let the original defect ship. `migrate-ts`'s `loadFixture` n
175
100
loader errors instead of discarding them — a checked-in fixture had been declaring the
176
101
illegal form and driving those suites green. All five ports green.
177
102
103
+
### Added — `@of` becomes optional on `@agg: collect`: the whole-object rollup ([#335](https://github.com/metaobjectsdev/metaobjects/issues/335))
104
+
105
+
**A projection could roll related rows up into an array of one COLUMN, and had no way to
106
+
roll them up into an array of OBJECTS.**`origin.aggregate @agg: collect` required `@of`, so
107
+
"every supplier's `{id, name}` for this product" was inexpressible — the shape had to be a
108
+
second round-trip, or a hand-written view, which is unmanaged and invisible to
109
+
`meta verify --db`. When `origin.collection` retired in `0.24.0` this became the one
110
+
coverage gap the retirement guide had to state rather than close.
111
+
112
+
**`@of` is now OPTIONAL on `collect`, and omitting it means a whole-object rollup:** the
113
+
carrying `field.object @isArray @objectRef` collects each related row as its declared value
**The declared value object IS the exposure.** Members bind to the `@via` terminal entity's
126
+
fields BY NAME, and a field the entity has but the value object omits is simply not
127
+
projected. That is deliberate — it is the [#270](https://github.com/metaobjectsdev/metaobjects/issues/270)
128
+
guarantee (a curated value object must not silently become the full entity) carried down to
129
+
the DDL tier. Name matching, rather than `extends`, is also deliberate: it keeps one value
130
+
object collectable from two different entities, which `extends` would forbid. The convention
131
+
is written into the byte-gated `origin.aggregate` registry prose so no port has to infer it.
132
+
133
+
**Eight load errors, in all five ports.** The carrying field must be a `field.object`
134
+
declaring `@objectRef`; that `@objectRef` must name an `object.value`; `@via` is required
135
+
(there is no `@of` entity to infer a single hop from); the path must be to-many; `@distinct`
136
+
is refused; `@orderBy` keys must resolve against the `@via`**terminal** entity; every value
137
+
object member must match a terminal field; and a matched member must agree on **both** type
138
+
axes — subtype and array-ness. `@distinct` is refused by CHOICE, not engine limit: it works
139
+
on both engines, but it is a guaranteed no-op whenever the value object carries the primary
140
+
key, and a silent no-op is worse than a refusal.
141
+
142
+
**A new error code, `ERR_COLLECT_WHOLE_OBJECT`,** carries the five refusals that would
143
+
otherwise have shared `ERR_INVALID_ORIGIN`. That is not taxonomy for its own sake: the shared
144
+
corpus compares error **code + source** and never message text, and `ERR_INVALID_ORIGIN` is
145
+
exactly what a loader that still *requires*`@of` emits for this same metadata — so five of
146
+
the eight negative fixtures passed against three ports containing none of the rules. With the
147
+
distinct code they fail, and Task-by-task porting has a real signal.
148
+
149
+
**View lowering, both dialects.** Postgres emits
150
+
`COALESCE(jsonb_agg(jsonb_build_object(…) ORDER BY <pk> ASC) FILTER (WHERE <pk> IS NOT NULL), '[]'::jsonb)`.
151
+
`jsonb`, not `json`: PG's `json` type has neither an equality nor an ordering operator, so
152
+
the `json_agg(json_build_object(… ORDER BY …))` form does not run at all. Default element
153
+
order is the related entity's **primary key** ascending — ordering rows by a serialized
154
+
object is meaningless — and an explicit `@orderBy` leads with the PK appended as a tie-break.
155
+
The scalar `@of` arm deliberately keeps its existing no-tie-break behaviour, since changing
156
+
it would move the emitted SQL of every project already using `@orderBy`.
157
+
158
+
**SQLite needed a shape nobody would have guessed, and only a real engine found it.** On
159
+
SQLite 3.44 (D1's pinned baseline) the in-aggregate `ORDER BY` clause **destroys the JSON
160
+
subtype**: `json_group_array(json_object(…) ORDER BY …)` returns an array of quoted STRINGS
161
+
rather than objects, and wrapping the argument in `json()` does not survive it either.
162
+
Dropping the `ORDER BY` was not an option — element order would stop being deterministic and
163
+
an author's `@orderBy` would silently do nothing. So the ordered array is built first and
164
+
re-wrapped element-by-element through `json_each`, which iterates in array order. The
165
+
emitted SQL text alone could never have shown this; it was caught by the emit → apply →
166
+
introspect → re-diff round-trip against a real engine, which is the standing rule that
167
+
golden SQL is not evidence for new DDL.
168
+
169
+
Also worth knowing: inside the rollup a `field.long` member arrives as a JSON **number**,
170
+
while the same value as a top-level `BIGINT` column arrives as a string from
171
+
node-postgres. That is inherent to JSON, not a codegen choice, and it is lossy above 2^53.
172
+
173
+
**Also in this change — array fields are not filterable or sortable.** A
174
+
`field.<scalar> isArray: true` carrying `@filterable: true` or `@sortable: true` is now a
175
+
load error in all five ports (`ERR_FILTERABLE_UNSUPPORTED_SUBTYPE` /
176
+
`ERR_SORTABLE_UNSUPPORTED_SUBTYPE`): no operator in the FR-009 scalar band applies to a
177
+
collection column, and no dialect can `ORDER BY` one. `@sortable` also gains the subtype
178
+
validation `@filterable` already had.
179
+
180
+
**That half makes previously-LOADING metadata fail to load, and it ships in a PATCH — so
181
+
here is the reasoning, and the exposure, in plain words.** It is the same ruling the #342
182
+
entry above makes, and the same one the `@min` clamp made in `0.19.1` and the `like`
183
+
case-sensitivity fix in `0.21.6`: a declaration that was already emitting SQL which cannot
184
+
execute was never validly expressible, so refusing it corrects previously-wrong acceptance
185
+
rather than changing a contract. Nothing ever documented the form, no generator ever emitted
186
+
it, and a structural scan of this repo — 1321 JSON and 124 YAML files — found **zero**
187
+
instances.
188
+
189
+
**Bounded is not zero, and pre-1.0 `^0.24.x` resolves a patch.** An adopter who wrote
190
+
`@filterable: true` on an array field independently will auto-adopt a load error on a
191
+
routine `npm update`, with no deliberate action on their part. The fix is to delete the
192
+
attribute — it was doing nothing, and the query it implied could never have run. This is
193
+
stated rather than left to be discovered, because the alternative to saying it is an adopter
194
+
finding out from a red build.
195
+
196
+
`metamodelVersion` moves `0.11` → `0.12`.
197
+
178
198
### Fixed — the C# EF Core target had no foreign keys at all, so `@onDelete` was inert ([#294](https://github.com/metaobjectsdev/metaobjects/issues/294))
179
199
180
200
**The issue reports that `DbContextGenerator` ignores `@onDelete`. It is worse than that:
@@ -430,6 +450,24 @@ directories throughout, so the mistake is structurally unreachable there; their
430
450
worded `ERR_COLLECTION_NOT_FOUND` is untouched. The cross-port `source-resolution-conformance`
431
451
corpus pins error CODES, not message text, and the code is unchanged.
432
452
453
+
### Added — a measured case study on declaring requirements before the code
454
+
455
+
[`docs/case-study-requirements-first.md`](docs/case-study-requirements-first.md) records what
456
+
happened when a new package declared its requirements in MetaObjects *before* it had an
457
+
implementation: 22 requirement nodes producing 24 generated checks, each with a vacuity proof
458
+
drawn from its own relation, all 24 running offline with no credentials, and 36 automated
459
+
mutations of which 36 forced a failure.
460
+
461
+
**The finding is about ordering, not tooling, and it includes the negative result.** The same
462
+
technique applied to an existing codebase surfaced about three checkable facts and was judged
463
+
not worth its cost; applied before the implementation existed it surfaced 24. Requirements
464
+
written after the code can only declare what survived implementation in legible form. The
465
+
document also records the modelling error behind an earlier no-fit verdict — inventing an
466
+
attribute to hold a requirement's oracle instead of modelling the fact as a member and
467
+
tagging it — and the boundary where modelling was measured to be the wrong choice. That
468
+
boundary is the point: [`docs/features/requirements.md`](docs/features/requirements.md)
469
+
describes the vocabulary, and this describes when using it pays.
470
+
433
471
### Added — every shipped metadata example is now gated against the strict registry ([#337](https://github.com/metaobjectsdev/metaobjects/issues/337))
434
472
435
473
**The same failure has now landed three times, and an adopter found it every time.** A doc
0 commit comments