Skip to content

Commit bc8167b

Browse files
authored
feat: any/all measures without @Of and 1.1.0 rc.5 adopter fixes (#443)
* fix: close the rc.4 adopter findings for 1.1.0 Reporting, in every port: - A bare entity name the path already fixes resolves in that entity's package: a dimension's @Of after @via resolves in the package of the entity the @via reaches, and a report's @spine owner resolves in @from's package. Gated by reporting-cross-package-short-names. - @agg any / all may omit @Of and quantify the measure's @filter or @segment over the rows (bool_or / bool_and on Postgres, MAX / MIN of the condition on SQLite, D1 and MySQL, and in the Cube model). New loader rule M0 refuses an @of-less measure of any other @agg, and an @of-less any / all with no condition, with ERR_INVALID_MEASURE. Tooling: - meta gen no longer tells a docs-only run to wire a prompt generator, and claims emitted value objects only when an entity generator ran. - agentDocsFile() warns when it skips agent/ui.md; includeUiTier is documented. - meta init warns when a pre-release CLI writes generatedBy, and detect-prerelease-pins.sh fails on a pre-release generatedBy. - prerelease-link keeps a pom's final-newline state, reports only real repins, and keeps its git exclude lines in a managed block that unlink removes. * no-mistakes(review): Fix docs-only prompt gate, ui.md opt-out, shared-exclude unlink * no-mistakes(review): Harden prerelease-link unlink and simplify report @via walk * no-mistakes(review): Guard spine any/all on fact rows; compile C# test * no-mistakes(document): Document no-@Of any/all in reporting and Cube docs * no-mistakes(review): Count NULL any/all conditions as failing; flag legacy excludes * no-mistakes(review): Make legacy-exclude cleanup command robust and hedge notice * no-mistakes(document): docs: align detector check count with generatedBy check
1 parent df08643 commit bc8167b

90 files changed

Lines changed: 2469 additions & 227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
7272
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.
7373

7474
**Cross-port conformance corpora** (every port runs the shared corpus):
75-
- Metamodel: `fixtures/conformance/` (380 fixtures; 29 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
75+
- Metamodel: `fixtures/conformance/` (384 fixtures; 29 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
7676
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
7777
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
7878
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.

‎CHANGELOG.md‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ generator catalog and the Atlas evaluation are 1.2 work.
122122
`<allowUnmetRequires>true</allowUnmetRequires>` runs such a list when the files are
123123
supplied another way. Every Java and Kotlin `requires` closure, and the skills' pom
124124
examples, are compiled over the fitness corpus in a test.
125+
- **`@agg: any` / `all` without `@of`, in every port.** A `measure.aggregate` with `@agg: any`
126+
or `all` may omit `@of` and declare a `@filter` or `@segment` instead: it is then true when
127+
some row (`any`) or every row (`all`) in the group passes that condition. `{ "name":
128+
"hasError", "@agg": "any", "@filter": { "success": false } }` states "did any call fail"
129+
over a `success` column, which a boolean `@of` could not. A row whose condition is NULL
130+
counts as not passing, as a `WHERE` drops it, so only a group with no rows reads NULL. It
131+
lowers to `bool_or(COALESCE(<condition>, FALSE))` / `bool_and(...)` on Postgres and `MAX` /
132+
`MIN` of `COALESCE(<condition>, 0)` on SQLite, D1 and MySQL, and to the same SQL in the Cube
133+
model. The registry makes
134+
`measure.aggregate` `@of` optional (`metamodelVersion` stays `1.1`), and a new loader rule M0
135+
refuses, with `ERR_INVALID_MEASURE`, an `@of`-less measure whose `@agg` is not `any` or `all`
136+
(it was `ERR_MISSING_REQUIRED_ATTR`) and an `@of`-less `any` / `all` with no condition.
137+
Gated by the `reporting-any-all-predicate`, `error-measure-any-without-condition` and
138+
`error-measure-sum-without-of` conformance fixtures. Found by an adopter on 1.1.0-rc.4.
125139

126140
- **The eject contract: every generator hands over its emit, and a test in every port holds
127141
it to that** (ADR-0034 Amendment 8). `fixtures/generator-registry-conformance/registry.json`
@@ -1279,6 +1293,48 @@ until you regenerate.
12791293
- Pre-release tooling: a `prerelease:publish --dry-run` no longer ends with "is on the
12801294
pre-release registry", and the Java step is labelled for what it runs (`package`).
12811295

1296+
- **A bare entity name the path already fixes resolves in that entity's package, in every port.**
1297+
A dimension's `"@of": "Realm.name"` after a `@via` hop onto `world::Realm` was resolved in
1298+
the declaring entity's package and refused (`must name a field of 'world::Realm', the entity
1299+
@via ... reaches`), so a cross-package `@of` needed the full name. It now resolves in the
1300+
package of the entity the `@via` reaches, and the owner of a report's `@spine` resolves in
1301+
`@from`'s package rather than the report's. A fully-qualified name is unchanged. The loader and every port's report shape use
1302+
the one rule. Gated by the `reporting-cross-package-short-names` conformance fixture. Found
1303+
by an adopter on 1.1.0-rc.4.
1304+
- **`meta gen` with only documentation generators no longer says to wire a prompt generator.**
1305+
A project whose `generators` lists only docs generators was told, for each `template.prompt`,
1306+
to add `promptRender()`, and that the payload value objects "were emitted regardless" when the
1307+
run had emitted no code. The advisory now fires only when the run lists a generator that
1308+
writes code, and it says the value objects were emitted only when `entityFile()` (or another
1309+
entity-module generator) ran. Found by an adopter on 1.1.0-rc.4.
1310+
- **`agentDocsFile()` says when it skips `agent/ui.md`.** The page is written only when the run
1311+
lists a client UI generator or the config sets `includeUiTier: true`, and before this a
1312+
project whose UI is generated by a separate config lost the page with no message. When the
1313+
config leaves `includeUiTier` unset, `meta gen` now warns `agent/ui.md not written: no client
1314+
UI generator in this run; set includeUiTier: true if another config generates the UI, or
1315+
includeUiTier: false if there is no UI.` A project with no UI anywhere sets `includeUiTier:
1316+
false`, which skips the page without the warning. Both settings are documented in
1317+
`docs/features/migrations/docs-are-generators.md`. Found by an adopter on 1.1.0-rc.4.
1318+
- **A pre-release CLI's version in `.metaobjects/.agent-context.json` is caught.** `meta init`
1319+
stamps the running CLI's version into that committed file's `generatedBy`; run with a release
1320+
candidate, it recorded `1.1.0-rc.4` and `tools/prerelease/detect-prerelease-pins.sh` passed.
1321+
`meta init` now warns when it writes a pre-release `generatedBy`, and the detector fails on
1322+
one and names `meta init --refresh-docs --docs-only`, run with the release CLI, as the fix.
1323+
Found by an adopter on 1.1.0-rc.4.
1324+
- **`prerelease-link` leaves a pom it did not change byte-identical, and reports only real
1325+
repins.** A `link` / `unlink` round trip added a final newline to a `pom.xml` that had none,
1326+
so `git status` showed it modified, and both commands said "repinned ... in `app/pom.xml`"
1327+
for a module pinned only through its parent's `${metaobjects.version}`. The rewrites keep a
1328+
file's final-newline state (`pom.xml`, and the blocks appended to `pyproject.toml` and
1329+
`.npmrc`), and a repin is reported only for a file whose text changed.
1330+
Found by an adopter on 1.1.0-rc.4.
1331+
- **`prerelease-link unlink` removes the git exclude lines `link` added.** `link` appended its
1332+
ignore rules as bare lines to the clone's shared `info/exclude`, and `unlink` left them there.
1333+
They now sit in one managed block, which `unlink` removes without touching a line the user
1334+
wrote, and a repeated `link` does not add them twice. The file is shared by every worktree
1335+
of the clone, so an entry that another linked worktree, or another project linked in the same
1336+
worktree, still needs stays until that one is unlinked. A `NuGet.config` that `link` wrote is
1337+
removed on `unlink` rather than left as a bare XML declaration.
12821338
- `verify --codegen` (Python, C#) says when the committed output was generated with another
12831339
column rule, another table rule, or both, and names the `naming` setting that reproduces it,
12841340
instead of advising a regeneration that would commit the wrong column or table names.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ metaobjects/
263263
├── CLAUDE.md # project instructions for Claude
264264
├── spec/ # canonical metamodel docs, ADRs, roadmap
265265
├── fixtures/ # 25 cross-language conformance corpora — the oracle
266-
│ ├── conformance/ # metamodel (loader + serializer + navigation), 380 fixtures
266+
│ ├── conformance/ # metamodel (loader + serializer + navigation), 384 fixtures
267267
│ ├── yaml-conformance/ # YAML authoring desugar
268268
│ ├── render-conformance/ # FR-004 byte-identical render oracle
269269
│ ├── verify-conformance/ # FR-004 template-drift gate

‎agent-context/skills/metaobjects-authoring/references/declaring-reports.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ and an `object.report` combines them by name. Four node kinds:
1414
- `dimension.attribute` / `dimension.time` — what to group by (`@of: Entity.field`; a time
1515
dimension lists the `@grains` it supports: `hour`, `day`, `week`, `month`, `quarter`, `year`);
1616
- `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio`
17-
(`@numerator` / `@denominator`, both measures of the entity);
17+
(`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a
18+
`field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError",
19+
"@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row
20+
whose condition is null does not pass);
1821
- `segment.filter` — a named, reusable `@filter` ("active purchase");
1922
- `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or
2023
`name:grain`), `@measures`.
@@ -63,6 +66,8 @@ Three rules an author trips on:
6366
a `sum`. The view joins the hop through an `identity.reference` between the two entities
6467
(`{ "identity.reference": { "name": "fkProgram", "@fields": ["programId"], "@references":
6568
"Program" } }`); a relationship with none behind it loads and then fails `meta migrate`.
69+
After `@via`, a bare entity name in `@of` resolves in the package of the entity the `@via`
70+
reaches (`"@of": "Realm.name"` after a hop onto `world::Realm`).
6671
3. **A report declares no fields.** Its columns are derived: one per dimension, then one per
6772
measure (a time dimension at a grain is `<name><Grain>`, so `purchasedAt:day` is
6873
`purchasedAtDay`). A `field.*` or `identity.*` child on a report is an error.

‎agent-context/skills/metaobjects-authoring/references/reporting.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ A report declares no fields. Its columns are one per `@dimensions` item in liste
4747
| `avg` of `int`, `long`, `currency`, `decimal` | the measure's name | `decimal` | no |
4848
| `avg` of `double` / `float` | the measure's name | `double` | no |
4949
| `min` / `max` | the measure's name | the `@of` field's type | no |
50-
| `any` / `all` (boolean OR / AND, `bool_or` / `bool_and`; `@of` must be a `field.boolean`) | the measure's name | `boolean` | no |
50+
| `any` / `all` (boolean OR / AND, `bool_or` / `bool_and`; over a `field.boolean` `@of`, or with no `@of` over the measure's `@filter` / `@segment`) | the measure's name | `boolean` | no |
5151
| `measure.ratio` | the measure's name | `decimal` | no |
5252
| any measure above except `count`, declaring `@default` | the measure's name | as above | yes: it reads the default instead of null |
5353
| in a `@spine` report, a dimension over a column of the spine entity | the dimension's name | the `@of` field's type | yes when the field is `@required` or in that entity's `identity.primary`; no for a dimension beyond the spine entity |

‎agent-context/skills/metaobjects-codegen/references/typescript-docs.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ generate, docs included). Each factory is an ordinary entry in `generators`, fro
2121
| `docsFile()` | `docs` | the neutral model surface — one page per entity and per template (`<Entity>.md`, `<Template>.md`), plus an overview with the whole-model ER diagram. Options: `apiSurfaces`, `templates`. |
2222
| `apiDocsFile()` | `api-docs` | the api reference — `api/<Entity>.md`, `api/README.md`, `api/AGENT-API.md`: the concrete imports, signatures, payload field shapes and runnable examples for THIS project's generated code. Options: `subDir` (default `api`), `modelBaseUrl`. |
2323
| `requirementsFile()` | `requirements` | the declared `requirement.*` ledger, each entry headed by its dotted path and `title`. Writes nothing for a model with no `requirement.*` node. |
24-
| `agentDocsFile()` | `agent-docs` | `agent/schema.md`, `agent/ui.md` (only when the run also lists a client-tier generator) and `agent/requirements.md` — the three `agent/` pages the always-on pointer reads before touching a tier. |
24+
| `agentDocsFile()` | `agent-docs` | `agent/schema.md`, `agent/ui.md` (only when the run also lists a client-tier generator, OR the config sets `includeUiTier: true` — the escape hatch for a docs-only config whose UI is generated by a separate config; skipping it with a UI surface in the model and `includeUiTier` unset prints an advisory naming `includeUiTier`; `includeUiTier: false` says there is no UI anywhere and skips it without one) and `agent/requirements.md` — the three `agent/` pages the always-on pointer reads before touching a tier. |
2525
| `metamodelDocsFile()` | `metamodel-docs` | the built-in vocabulary reference, under `metamodel/` (was `meta docs --metamodel`). |
2626
| `docsSiteFile()` | `docs-site` | an HTML site under `site/` (was `meta docs --site`). Option: `prompts`. `meta eject docs-site` copies its templates and assets to `codegen/docs-site/` (was `meta docs --scaffold-site`). |
2727

‎docs/CONFORMANCE.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
2626

2727
| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
2828
|---|---|---|---|---|---|---|
29-
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 380 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
29+
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 384 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
3030
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 16 | 16 / 16 | 15 / 16 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 15 / 16 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 16 / 16 |
3131
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 37 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
3232
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
@@ -267,7 +267,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
267267

268268
## Fixture-to-doc mapping
269269

270-
### `fixtures/conformance/` — metamodel loader + canonical serializer (380)
270+
### `fixtures/conformance/` — metamodel loader + canonical serializer (384)
271271

272272
| Fixture prefix | Feature doc |
273273
|---|---|
@@ -541,7 +541,7 @@ files into a real Cube.
541541

542542
## Orphaned fixtures (tested but not yet documented)
543543

544-
The fixtures in the nine corpora mapped above (metamodel 380 + yaml 16 + verify 37
544+
The fixtures in the nine corpora mapped above (metamodel 384 + yaml 16 + verify 37
545545
+ render 16 + persistence 54 + api-contract 91 + source-resolution 25 + scope 10 +
546546
dependency 23) each map to a feature doc, and so do the two requirement corpora and the
547547
`cube-model` corpus, whose case lists live in their own READMEs. None are orphaned today. The remaining

‎docs/features/cube-export.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ somewhere to go: an `identity.reference` onto an entity that is not a cube makes
322322
| `count` with `@distinct` | `type: count_distinct`, `sql: x` |
323323
| `count` with `@distinct` over a tuple `x1, x2` | Postgres: `type: count_distinct`, `sql: 'ROW(x1, x2)'`, with one `filters` entry `x1 IS NOT NULL AND x2 IS NOT NULL`, so a tuple with a null component is not counted, as in the view. MySQL: the view's own `COUNT(DISTINCT x1, x2)` as a `type: number` measure (a condition `c` makes it `COUNT(DISTINCT CASE WHEN c THEN x1 END, x2)`); MySQL's multi-argument form skips a tuple with a null component and compares by the columns' collation, which a `JSON_ARRAY(x1, x2)` key does not: on mysql:8.4 under the default `utf8mb4_0900_ai_ci` it counted `'abc'` and `'ABC'` as two tuples where the view counts one |
324324
| `sum`, `avg`, `min`, `max` | `type: sum`, `avg`, `min`, `max`, `sql: x` |
325-
| `any`, `all` | `type: boolean`. Postgres: `sql: 'bool_or(x)'` / `'bool_and(x)'`, a condition `c` as `bool_or(x) FILTER (WHERE c)`. MySQL: `sql: 'MAX(x)'` / `'MIN(x)'`, a condition as `MAX(CASE WHEN c THEN x END)`. The aggregate is the view's own, written in `sql` because Postgres has no `MAX(boolean)` and the condition cannot ride in `filters`. |
325+
| `any`, `all` | `type: boolean`. Postgres: `sql: 'bool_or(x)'` / `'bool_and(x)'`, a condition `c` as `bool_or(x) FILTER (WHERE c)`. MySQL: `sql: 'MAX(x)'` / `'MIN(x)'`, a condition as `MAX(CASE WHEN c THEN x END)`. The aggregate is the view's own, written in `sql` because Postgres has no `MAX(boolean)` and the condition cannot ride in `filters`. With no `@of`, the condition is the aggregated value, a null condition counting as not passing: `bool_or(COALESCE(c, FALSE))` / `bool_and(COALESCE(c, FALSE))`, `MAX(COALESCE(c, 0))` / `MIN(COALESCE(c, 0))` on MySQL. |
326326
| any of the above with a condition `c` | the same, with one `filters` entry `c`. For a Postgres tuple, `c` is ANDed after the not-null terms in that one entry. |
327327
| `measure.ratio` | `type: number`. Postgres: `CAST({num} AS NUMERIC) / NULLIF({den}, 0)`. MySQL: `{num} / NULLIF({den}, 0)`. |
328328
| a `measure.aggregate` with `@default: n` | two members. `<m>Raw` is the aggregate as the rows above write it (its `type`, `sql` and `filters`), with `public: false`. `<m>` is `type: number` (`type: boolean` for an `any` or `all`, where `n` is written `TRUE` / `FALSE` on Postgres and `1` / `0` on MySQL), `sql: 'COALESCE({<m>Raw}, n)'`, and carries the measure's `title` and `description`. |

0 commit comments

Comments
 (0)