Skip to content

Commit d033d81

Browse files
dmealingclaude
andcommitted
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
1 parent 18d7cd9 commit d033d81

1 file changed

Lines changed: 126 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 126 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,86 +7,6 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10-
### Added — `@of` becomes optional on `@agg: collect`: the whole-object rollup ([#335](https://github.com/metaobjectsdev/metaobjects/issues/335))
11-
12-
**A projection could roll related rows up into an array of one COLUMN, and had no way to
13-
roll them up into an array of OBJECTS.** `origin.aggregate @agg: collect` required `@of`, so
14-
"every supplier's `{id, name}` for this product" was inexpressible — the shape had to be a
15-
second round-trip, or a hand-written view, which is unmanaged and invisible to
16-
`meta verify --db`. When `origin.collection` retired in `0.24.0` this became the one
17-
coverage gap the retirement guide had to state rather than close.
18-
19-
**`@of` is now OPTIONAL on `collect`, and omitting it means a whole-object rollup:** the
20-
carrying `field.object @isArray @objectRef` collects each related row as its declared value
21-
object.
22-
23-
```jsonc
24-
{ "field.object": {
25-
"name": "supplierBriefs", "isArray": true, "@objectRef": "SupplierBrief",
26-
"children": [
27-
{ "origin.aggregate": { "@agg": "collect", "@via": "Product.suppliers" } }
28-
]
29-
}}
30-
```
31-
32-
**The declared value object IS the exposure.** Members bind to the `@via` terminal entity's
33-
fields BY NAME, and a field the entity has but the value object omits is simply not
34-
projected. That is deliberate — it is the [#270](https://github.com/metaobjectsdev/metaobjects/issues/270)
35-
guarantee (a curated value object must not silently become the full entity) carried down to
36-
the DDL tier. Name matching, rather than `extends`, is also deliberate: it keeps one value
37-
object collectable from two different entities, which `extends` would forbid. The convention
38-
is written into the byte-gated `origin.aggregate` registry prose so no port has to infer it.
39-
40-
**Eight load errors, in all five ports.** The carrying field must be a `field.object`
41-
declaring `@objectRef`; that `@objectRef` must name an `object.value`; `@via` is required
42-
(there is no `@of` entity to infer a single hop from); the path must be to-many; `@distinct`
43-
is refused; `@orderBy` keys must resolve against the `@via` **terminal** entity; every value
44-
object member must match a terminal field; and a matched member must agree on **both** type
45-
axes — subtype and array-ness. `@distinct` is refused by CHOICE, not engine limit: it works
46-
on both engines, but it is a guaranteed no-op whenever the value object carries the primary
47-
key, and a silent no-op is worse than a refusal.
48-
49-
**A new error code, `ERR_COLLECT_WHOLE_OBJECT`,** carries the five refusals that would
50-
otherwise have shared `ERR_INVALID_ORIGIN`. That is not taxonomy for its own sake: the shared
51-
corpus compares error **code + source** and never message text, and `ERR_INVALID_ORIGIN` is
52-
exactly what a loader that still *requires* `@of` emits for this same metadata — so five of
53-
the eight negative fixtures passed against three ports containing none of the rules. With the
54-
distinct code they fail, and Task-by-task porting has a real signal.
55-
56-
**View lowering, both dialects.** Postgres emits
57-
`COALESCE(jsonb_agg(jsonb_build_object(…) ORDER BY <pk> ASC) FILTER (WHERE <pk> IS NOT NULL), '[]'::jsonb)`.
58-
`jsonb`, not `json`: PG's `json` type has neither an equality nor an ordering operator, so
59-
the `json_agg(json_build_object(… ORDER BY …))` form does not run at all. Default element
60-
order is the related entity's **primary key** ascending — ordering rows by a serialized
61-
object is meaningless — and an explicit `@orderBy` leads with the PK appended as a tie-break.
62-
The scalar `@of` arm deliberately keeps its existing no-tie-break behaviour, since changing
63-
it would move the emitted SQL of every project already using `@orderBy`.
64-
65-
**SQLite needed a shape nobody would have guessed, and only a real engine found it.** On
66-
SQLite 3.44 (D1's pinned baseline) the in-aggregate `ORDER BY` clause **destroys the JSON
67-
subtype**: `json_group_array(json_object(…) ORDER BY …)` returns an array of quoted STRINGS
68-
rather than objects, and wrapping the argument in `json()` does not survive it either.
69-
Dropping the `ORDER BY` was not an option — element order would stop being deterministic and
70-
an author's `@orderBy` would silently do nothing. So the ordered array is built first and
71-
re-wrapped element-by-element through `json_each`, which iterates in array order. The
72-
emitted SQL text alone could never have shown this; it was caught by the emit → apply →
73-
introspect → re-diff round-trip against a real engine, which is the standing rule that
74-
golden SQL is not evidence for new DDL.
75-
76-
Also worth knowing: inside the rollup a `field.long` member arrives as a JSON **number**,
77-
while the same value as a top-level `BIGINT` column arrives as a string from
78-
node-postgres. That is inherent to JSON, not a codegen choice, and it is lossy above 2^53.
79-
80-
**Also in this change — array fields are not filterable or sortable.** A
81-
`field.<scalar> isArray: true` carrying `@filterable: true` or `@sortable: true` is now a
82-
load error in all five ports (`ERR_FILTERABLE_UNSUPPORTED_SUBTYPE` /
83-
`ERR_SORTABLE_UNSUPPORTED_SUBTYPE`): no operator in the FR-009 scalar band applies to a
84-
collection column, and no dialect can `ORDER BY` one. `@sortable` also gains the subtype
85-
validation `@filterable` already had.
86-
87-
`metamodelVersion` moves `0.11``0.12`.
88-
89-
9010
## [0.24.1] — npm `0.24.1` · PyPI `0.24.1` · NuGet `0.24.1` · Maven `7.24.1`
9111

9212
### 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
14161
needs pull in opposite directions — normalization *fixes* the crash and *hides* the empty
14262
array — so presence and content are asked as separate questions.
14363

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
14666
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.
15378

15479
**Two things this took, worth recording.** The Java port needed a fix the other three did
15580
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
175100
loader errors instead of discarding them — a checked-in fixture had been declaring the
176101
illegal form and driving those suites green. All five ports green.
177102

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
114+
object.
115+
116+
```jsonc
117+
{ "field.object": {
118+
"name": "supplierBriefs", "isArray": true, "@objectRef": "SupplierBrief",
119+
"children": [
120+
{ "origin.aggregate": { "@agg": "collect", "@via": "Product.suppliers" } }
121+
]
122+
}}
123+
```
124+
125+
**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+
178198
### Fixed — the C# EF Core target had no foreign keys at all, so `@onDelete` was inert ([#294](https://github.com/metaobjectsdev/metaobjects/issues/294))
179199

180200
**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
430450
worded `ERR_COLLECTION_NOT_FOUND` is untouched. The cross-port `source-resolution-conformance`
431451
corpus pins error CODES, not message text, and the code is unchanged.
432452

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+
433471
### Added — every shipped metadata example is now gated against the strict registry ([#337](https://github.com/metaobjectsdev/metaobjects/issues/337))
434472

435473
**The same failure has now landed three times, and an adopter found it every time.** A doc

0 commit comments

Comments
 (0)