diff --git a/AGENTS.md b/AGENTS.md index 396cd39d7..1e010f5ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken. - **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. **Cross-port conformance corpora** (every port runs the shared corpus): -- 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. +- 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. - Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical. - 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".) - 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index aca1dafb6..8374bd3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,20 @@ generator catalog and the Atlas evaluation are 1.2 work. `true` runs such a list when the files are supplied another way. Every Java and Kotlin `requires` closure, and the skills' pom examples, are compiled over the fitness corpus in a test. +- **`@agg: any` / `all` without `@of`, in every port.** A `measure.aggregate` with `@agg: any` + or `all` may omit `@of` and declare a `@filter` or `@segment` instead: it is then true when + some row (`any`) or every row (`all`) in the group passes that condition. `{ "name": + "hasError", "@agg": "any", "@filter": { "success": false } }` states "did any call fail" + over a `success` column, which a boolean `@of` could not. A row whose condition is NULL + counts as not passing, as a `WHERE` drops it, so only a group with no rows reads NULL. It + lowers to `bool_or(COALESCE(, FALSE))` / `bool_and(...)` on Postgres and `MAX` / + `MIN` of `COALESCE(, 0)` on SQLite, D1 and MySQL, and to the same SQL in the Cube + model. The registry makes + `measure.aggregate` `@of` optional (`metamodelVersion` stays `1.1`), and a new loader rule M0 + refuses, with `ERR_INVALID_MEASURE`, an `@of`-less measure whose `@agg` is not `any` or `all` + (it was `ERR_MISSING_REQUIRED_ATTR`) and an `@of`-less `any` / `all` with no condition. + Gated by the `reporting-any-all-predicate`, `error-measure-any-without-condition` and + `error-measure-sum-without-of` conformance fixtures. Found by an adopter on 1.1.0-rc.4. - **The eject contract: every generator hands over its emit, and a test in every port holds it to that** (ADR-0034 Amendment 8). `fixtures/generator-registry-conformance/registry.json` @@ -1279,6 +1293,48 @@ until you regenerate. - Pre-release tooling: a `prerelease:publish --dry-run` no longer ends with "is on the pre-release registry", and the Java step is labelled for what it runs (`package`). +- **A bare entity name the path already fixes resolves in that entity's package, in every port.** + A dimension's `"@of": "Realm.name"` after a `@via` hop onto `world::Realm` was resolved in + the declaring entity's package and refused (`must name a field of 'world::Realm', the entity + @via ... reaches`), so a cross-package `@of` needed the full name. It now resolves in the + package of the entity the `@via` reaches, and the owner of a report's `@spine` resolves in + `@from`'s package rather than the report's. A fully-qualified name is unchanged. The loader and every port's report shape use + the one rule. Gated by the `reporting-cross-package-short-names` conformance fixture. Found + by an adopter on 1.1.0-rc.4. +- **`meta gen` with only documentation generators no longer says to wire a prompt generator.** + A project whose `generators` lists only docs generators was told, for each `template.prompt`, + to add `promptRender()`, and that the payload value objects "were emitted regardless" when the + run had emitted no code. The advisory now fires only when the run lists a generator that + writes code, and it says the value objects were emitted only when `entityFile()` (or another + entity-module generator) ran. Found by an adopter on 1.1.0-rc.4. +- **`agentDocsFile()` says when it skips `agent/ui.md`.** The page is written only when the run + lists a client UI generator or the config sets `includeUiTier: true`, and before this a + project whose UI is generated by a separate config lost the page with no message. When the + config leaves `includeUiTier` unset, `meta gen` now warns `agent/ui.md not written: no client + UI generator in this run; set includeUiTier: true if another config generates the UI, or + includeUiTier: false if there is no UI.` A project with no UI anywhere sets `includeUiTier: + false`, which skips the page without the warning. Both settings are documented in + `docs/features/migrations/docs-are-generators.md`. Found by an adopter on 1.1.0-rc.4. +- **A pre-release CLI's version in `.metaobjects/.agent-context.json` is caught.** `meta init` + stamps the running CLI's version into that committed file's `generatedBy`; run with a release + candidate, it recorded `1.1.0-rc.4` and `tools/prerelease/detect-prerelease-pins.sh` passed. + `meta init` now warns when it writes a pre-release `generatedBy`, and the detector fails on + one and names `meta init --refresh-docs --docs-only`, run with the release CLI, as the fix. + Found by an adopter on 1.1.0-rc.4. +- **`prerelease-link` leaves a pom it did not change byte-identical, and reports only real + repins.** A `link` / `unlink` round trip added a final newline to a `pom.xml` that had none, + so `git status` showed it modified, and both commands said "repinned ... in `app/pom.xml`" + for a module pinned only through its parent's `${metaobjects.version}`. The rewrites keep a + file's final-newline state (`pom.xml`, and the blocks appended to `pyproject.toml` and + `.npmrc`), and a repin is reported only for a file whose text changed. + Found by an adopter on 1.1.0-rc.4. +- **`prerelease-link unlink` removes the git exclude lines `link` added.** `link` appended its + ignore rules as bare lines to the clone's shared `info/exclude`, and `unlink` left them there. + They now sit in one managed block, which `unlink` removes without touching a line the user + wrote, and a repeated `link` does not add them twice. The file is shared by every worktree + of the clone, so an entry that another linked worktree, or another project linked in the same + worktree, still needs stays until that one is unlinked. A `NuGet.config` that `link` wrote is + removed on `unlink` rather than left as a bare XML declaration. - `verify --codegen` (Python, C#) says when the committed output was generated with another column rule, another table rule, or both, and names the `naming` setting that reproduces it, instead of advising a regeneration that would commit the wrong column or table names. diff --git a/README.md b/README.md index 411ba70bc..b918024df 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ metaobjects/ ├── CLAUDE.md # project instructions for Claude ├── spec/ # canonical metamodel docs, ADRs, roadmap ├── fixtures/ # 25 cross-language conformance corpora — the oracle -│ ├── conformance/ # metamodel (loader + serializer + navigation), 380 fixtures +│ ├── conformance/ # metamodel (loader + serializer + navigation), 384 fixtures │ ├── yaml-conformance/ # YAML authoring desugar │ ├── render-conformance/ # FR-004 byte-identical render oracle │ ├── verify-conformance/ # FR-004 template-drift gate diff --git a/agent-context/skills/metaobjects-authoring/references/declaring-reports.md b/agent-context/skills/metaobjects-authoring/references/declaring-reports.md index 289368316..544a7fbe6 100644 --- a/agent-context/skills/metaobjects-authoring/references/declaring-reports.md +++ b/agent-context/skills/metaobjects-authoring/references/declaring-reports.md @@ -14,7 +14,10 @@ and an `object.report` combines them by name. Four node kinds: - `dimension.attribute` / `dimension.time` — what to group by (`@of: Entity.field`; a time dimension lists the `@grains` it supports: `hour`, `day`, `week`, `month`, `quarter`, `year`); - `measure.aggregate` (`@agg`: `count`, `sum`, `avg`, `min`, `max`, `any`, `all`) and `measure.ratio` - (`@numerator` / `@denominator`, both measures of the entity); + (`@numerator` / `@denominator`, both measures of the entity). `any` / `all` take a + `field.boolean` `@of`, or no `@of` and a `@filter` or `@segment`: `{ "name": "hasError", + "@agg": "any", "@filter": { "success": false } }` is "some row has `success` false" (a row + whose condition is null does not pass); - `segment.filter` — a named, reusable `@filter` ("active purchase"); - `object.report` — a top-level object: `@from` an entity, `@dimensions` (`name` or `name:grain`), `@measures`. @@ -63,6 +66,8 @@ Three rules an author trips on: a `sum`. The view joins the hop through an `identity.reference` between the two entities (`{ "identity.reference": { "name": "fkProgram", "@fields": ["programId"], "@references": "Program" } }`); a relationship with none behind it loads and then fails `meta migrate`. + After `@via`, a bare entity name in `@of` resolves in the package of the entity the `@via` + reaches (`"@of": "Realm.name"` after a hop onto `world::Realm`). 3. **A report declares no fields.** Its columns are derived: one per dimension, then one per measure (a time dimension at a grain is ``, so `purchasedAt:day` is `purchasedAtDay`). A `field.*` or `identity.*` child on a report is an error. diff --git a/agent-context/skills/metaobjects-authoring/references/reporting.md b/agent-context/skills/metaobjects-authoring/references/reporting.md index 37fa747e2..31489594b 100644 --- a/agent-context/skills/metaobjects-authoring/references/reporting.md +++ b/agent-context/skills/metaobjects-authoring/references/reporting.md @@ -47,7 +47,7 @@ A report declares no fields. Its columns are one per `@dimensions` item in liste | `avg` of `int`, `long`, `currency`, `decimal` | the measure's name | `decimal` | no | | `avg` of `double` / `float` | the measure's name | `double` | no | | `min` / `max` | the measure's name | the `@of` field's type | no | -| `any` / `all` (boolean OR / AND, `bool_or` / `bool_and`; `@of` must be a `field.boolean`) | the measure's name | `boolean` | no | +| `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 | | `measure.ratio` | the measure's name | `decimal` | no | | any measure above except `count`, declaring `@default` | the measure's name | as above | yes: it reads the default instead of null | | 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 | diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-docs.md b/agent-context/skills/metaobjects-codegen/references/typescript-docs.md index 2159716b4..477959e31 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript-docs.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript-docs.md @@ -21,7 +21,7 @@ generate, docs included). Each factory is an ordinary entry in `generators`, fro | `docsFile()` | `docs` | the neutral model surface — one page per entity and per template (`.md`, `