Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ generator catalog and the Atlas evaluation are 1.2 work.
`<allowUnmetRequires>true</allowUnmetRequires>` 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(<condition>, FALSE))` / `bool_and(...)` on Postgres and `MAX` /
`MIN` of `COALESCE(<condition>, 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`
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 `<name><Grain>`, so `purchasedAt:day` is
`purchasedAtDay`). A `field.*` or `identity.*` child on a report is an error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<Entity>.md`, `<Template>.md`), plus an overview with the whole-model ER diagram. Options: `apiSurfaces`, `templates`. |
| `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`. |
| `requirementsFile()` | `requirements` | the declared `requirement.*` ledger, each entry headed by its dotted path and `title`. Writes nothing for a model with no `requirement.*` node. |
| `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. |
| `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. |
| `metamodelDocsFile()` | `metamodel-docs` | the built-in vocabulary reference, under `metamodel/` (was `meta docs --metamodel`). |
| `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`). |

Expand Down
6 changes: 3 additions & 3 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor

| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
|---|---|---|---|---|---|---|
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 380 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 384 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`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 |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 37 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
Expand Down Expand Up @@ -267,7 +267,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.

## Fixture-to-doc mapping

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

| Fixture prefix | Feature doc |
|---|---|
Expand Down Expand Up @@ -541,7 +541,7 @@ files into a real Cube.

## Orphaned fixtures (tested but not yet documented)

The fixtures in the nine corpora mapped above (metamodel 380 + yaml 16 + verify 37
The fixtures in the nine corpora mapped above (metamodel 384 + yaml 16 + verify 37
+ render 16 + persistence 54 + api-contract 91 + source-resolution 25 + scope 10 +
dependency 23) each map to a feature doc, and so do the two requirement corpora and the
`cube-model` corpus, whose case lists live in their own READMEs. None are orphaned today. The remaining
Expand Down
2 changes: 1 addition & 1 deletion docs/features/cube-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ somewhere to go: an `identity.reference` onto an entity that is not a cube makes
| `count` with `@distinct` | `type: count_distinct`, `sql: x` |
| `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 |
| `sum`, `avg`, `min`, `max` | `type: sum`, `avg`, `min`, `max`, `sql: x` |
| `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`. |
| `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. |
| 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. |
| `measure.ratio` | `type: number`. Postgres: `CAST({num} AS NUMERIC) / NULLIF({den}, 0)`. MySQL: `{num} / NULLIF({den}, 0)`. |
| 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`. |
Expand Down
Loading
Loading