Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3cd98a1
docs(fr-044): plan 4, the Cube exporter (cube-model reference generator)
dmealing Oct 9, 2026
fd495e5
refactor(reporting): one module for the report SQL fragments (FR-044)
dmealing Oct 9, 2026
53d7539
feat(cube-model): build the Cube model of the reporting vocabulary (F…
dmealing Oct 9, 2026
6990135
fix(cube-model): composite joins, shared @via resolver, review fixes …
dmealing Oct 9, 2026
267d7a5
fix(cube-model): match a hop by its reference, not its first column (…
dmealing Oct 9, 2026
f498079
feat(cube-model): a rollup per served report (FR-044)
dmealing Oct 9, 2026
5969fa3
fix(cube-model): standalone alias cubes, report refusals (FR-044)
dmealing Oct 10, 2026
ebb22b7
feat(cube-model): deterministic Cube YAML (FR-044)
dmealing Oct 10, 2026
52dfc75
fix(cube-model): lossless SQL scalars, source XOR, YAML parse oracle …
dmealing Oct 10, 2026
8ac5ec6
feat(cube-model): the cube-model generator and its mapping corpus (FR…
dmealing Oct 10, 2026
5a50641
test(cube-model): tuple distinct count with a condition, MySQL time d…
dmealing Oct 10, 2026
9b0134a
feat(cube-model): catalog entry, ejectable reference copy (FR-044)
dmealing Oct 10, 2026
32ab3ac
test(cube-model): live check against a real Cube instance (FR-044)
dmealing Oct 10, 2026
66d316f
ci(cube-model): run the live check as its own `cube` lane (FR-044)
dmealing Oct 10, 2026
fb52061
fix(cube-model): Cube-safe free text, rollups coarsest first (FR-044)
dmealing Oct 10, 2026
a14c02b
fix(cube-model): lane robustness, rollup tie order (FR-044)
dmealing Oct 10, 2026
a52af06
docs(cube-model): the Cube exporter (FR-044)
dmealing Oct 10, 2026
ddd8aca
docs(cube-model): plan answers and as built, spec section 5 (FR-044)
dmealing Oct 10, 2026
826e412
docs(cube-model): drop build-ledger ruling numbers from the plan's As…
dmealing Oct 10, 2026
b189b8e
docs(cube-model): review fixes (FR-044)
dmealing Oct 10, 2026
3130922
feat(cube-model): refuse @spine reports and @default measures until m…
dmealing Oct 10, 2026
45e5eb1
fix(cube-model): escape backslashes for Cube, key-field dimension, fi…
dmealing Oct 10, 2026
1a22350
chore(site): payload counts the cube-model corpus (FR-044)
dmealing Oct 10, 2026
1205ff9
test(cube-model): guard tests load clean under the @spine/@default ru…
dmealing Oct 10, 2026
1d4b0ed
fix(cube-model): MySQL tuple count as the view's, one Jinja rule, rev…
dmealing Oct 10, 2026
adc8971
feat(cube-model): a measure's @default (FR-044)
dmealing Oct 10, 2026
ba2ae64
feat(cube-model): a @spine report as a Cube view (FR-044)
dmealing Oct 10, 2026
6ff759e
fix(cube-model): spine review fixes (FR-044)
dmealing Oct 10, 2026
016aebf
refactor(cube-model): simplify pass over the exporter source (FR-044)
dmealing Oct 10, 2026
9185bb4
chore: record the merged plan PR head (#414) so this branch fast-forw…
dmealing Oct 10, 2026
6478005
no-mistakes(document): Documentation updated for FR-044 Plan 4 Cube e…
dmealing Oct 10, 2026
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
13 changes: 8 additions & 5 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ name: integration-tests
# persistence + api-contract corpora
# java-slow — the Java and Kotlin integration modules
# csharp / python — that port's integration corpora
# cube — the cube-model live check: the TS generator's output in a real Cube over
# its own throwaway Postgres (FR-044). It brings its own containers on a
# private network and ignores the shared sidecar below.
#
# RELEASE BACKSTOP. The PRIMARY gate for the migrate-ts real-PG suites is local-ci.yml's
# ts-slow lane, on every push to main; the ts-slow job here is the cold-environment
Expand All @@ -46,7 +49,7 @@ jobs:
strategy:
fail-fast: false
matrix:
lane: [ts-slow, csharp, java-slow, python]
lane: [ts-slow, csharp, java-slow, python, cube]
# A single job-level Postgres sidecar shared by every lane, instead of each
# port booting (and pulling) its own container per scenario. Each port's PG
# helper, when it sees METAOBJECTS_TEST_PG_URL, connects to this sidecar and
Expand All @@ -73,17 +76,17 @@ jobs:
with:
persist-credentials: false

- name: Set up Bun (TS lane)
if: matrix.lane == 'ts-slow'
- name: Set up Bun (TS lanes)
if: matrix.lane == 'ts-slow' || matrix.lane == 'cube'
uses: oven-sh/setup-bun@v2
with:
# Pin a known-good Bun (1.3.8 segfaults on exit; see the flake that cost a
# full re-run). setup-bun caches the Bun binary but NOT `bun install`
# output — the actions/cache step below does that.
bun-version: '1.3.14'

- name: Cache Bun install cache (TS lane)
if: matrix.lane == 'ts-slow'
- name: Cache Bun install cache (TS lanes)
if: matrix.lane == 'ts-slow' || matrix.lane == 'cube'
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The first four ship per-language today across the five ports (TS / C# / Java / P

## Status

_Last refreshed 2026-10-03._
_Last refreshed 2026-10-10._

**1.0 gating — the quiet period is RETIRED (2026-09-06).** `docs/1.0-readiness.md` §G3 no
longer asks for "one coordinated release with no metamodel-breaking change." It measured a
Expand Down Expand Up @@ -75,14 +75,14 @@ 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/` (374 fixtures; 28 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/` (374 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 **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.
- YAML / verify corpora green across the ports that ship those layers.
- **Codegen-compile gate** (all five ports; a GATE, not a corpus — it has no fixtures of its own and no row in the matrix). Every corpus above gates BEHAVIOUR; none asks whether the emitted code BUILDS, which is how four "generated code does not compile" defects shipped in 1.0.4 with the whole matrix green — `gen` exits 0 in all four cases and the adopter's build is the first thing that disagrees. Each port generates from `fixtures/persistence-conformance/canonical/meta.fitness.json` (reused deliberately: a second kitchen sink would drift from the one the other corpora already maintain) and compiles the emitted tree with its real compiler — `ts.createProgram` / Roslyn / `javac` / `KotlinCompilation` / (Python, having no static compiler) importing the generated package plus `ruff` F821. **Every port excludes its framework-bound route tier** (TS `routesFile`, C# `RoutesGenerator`, Java `SpringControllerGenerator`, Kotlin `KotlinSpringControllerGenerator`): those imports are not on an in-memory compile's classpath and stubbing them drowns the signal, so that tier is proven by the api-contract integration lane instead. One cross-port rule, not four local concessions. Found 5 further real defects on first run. Boundary detail: `docs/CONFORMANCE.md` → "Split coverage".

**Key cross-language features shipped:** FR5 family (a/b/c/d/e + WARN envelope-shape — actionable loader errors per ADR-0009); FR-003 (Java RDB runtime persistence + projections; schema migrations are TS-only — the Java migration engine was removed); FR-006 (template.output parser-on-receipt codegen per ADR-0010 in all 5 ports); FR-008 + FR-009 (cross-port REST API contract + the nine filter operators); FR-018 (M:N relationship codegen in all 5 ports — entity navigation + idiomatic ORM wiring [Drizzle m2m / EF Core `UsingEntity` / Spring repo+JPA / Exposed / Pydantic+route as the SQLAlchemy-secondary equivalent] + REST traversal `GET /<source-plural>/{id}/<relation>` + Tier-2 docs, gated by the shared api-contract m2m corpus in both lanes + persistence-conformance; the TanStack M:N client hook is a deferred client-ergonomics follow-up); SP-H (field-subtype end-to-end hardening: every concrete `field.*` subtype write+read round-trips cross-port via the persistence `op: roundtrip` gate; cut `field.byte`/`field.short`/`field.class` non-functional stubs; cross-port filter-op reconciliation for uuid/currency); source v2 paradigm (ADR-0007); metadata-ktx Kotlin facade; per-target output directories (TS codegen); FR-044 (the reporting vocabulary — `dimension.attribute`/`dimension.time`, `measure.aggregate`/`measure.ratio`, `segment.filter`, `object.report`, `@spine` on a report to take rows from a to-one dimension entity, `@default` on a measure for empty aggregates, plus the relative-date filter value — registered and loader-validated in all five ports; a report that declares a read-only `source.rdb @kind: view` is lowered to a SQL view by `meta migrate` and read by every port, and served by a generated read-only list route in every port, gated by the api-contract `report/` sub-corpus; no client hook, grid or form is generated for a report yet, see [docs/features/reporting.md](docs/features/reporting.md)).
**Key cross-language features shipped:** FR5 family (a/b/c/d/e + WARN envelope-shape — actionable loader errors per ADR-0009); FR-003 (Java RDB runtime persistence + projections; schema migrations are TS-only — the Java migration engine was removed); FR-006 (template.output parser-on-receipt codegen per ADR-0010 in all 5 ports); FR-008 + FR-009 (cross-port REST API contract + the nine filter operators); FR-018 (M:N relationship codegen in all 5 ports — entity navigation + idiomatic ORM wiring [Drizzle m2m / EF Core `UsingEntity` / Spring repo+JPA / Exposed / Pydantic+route as the SQLAlchemy-secondary equivalent] + REST traversal `GET /<source-plural>/{id}/<relation>` + Tier-2 docs, gated by the shared api-contract m2m corpus in both lanes + persistence-conformance; the TanStack M:N client hook is a deferred client-ergonomics follow-up); SP-H (field-subtype end-to-end hardening: every concrete `field.*` subtype write+read round-trips cross-port via the persistence `op: roundtrip` gate; cut `field.byte`/`field.short`/`field.class` non-functional stubs; cross-port filter-op reconciliation for uuid/currency); source v2 paradigm (ADR-0007); metadata-ktx Kotlin facade; per-target output directories (TS codegen); FR-044 (the reporting vocabulary — `dimension.attribute`/`dimension.time`, `measure.aggregate`/`measure.ratio`, `segment.filter`, `object.report`, `@spine` on a report to take rows from a to-one dimension entity, `@default` on a measure for empty aggregates, plus the relative-date filter value — registered and loader-validated in all five ports; a report that declares a read-only `source.rdb @kind: view` is lowered to a SQL view by `meta migrate` and read by every port, and served by a generated read-only list route in every port, gated by the api-contract `report/` sub-corpus; the TypeScript `cube-model` reference generator writes the vocabulary as Cube data-model files, gated by the `fixtures/cube-model/` corpus and a `cube` lane that loads the output into a real Cube; no grid or form is generated for a report yet, see [docs/features/reporting.md](docs/features/reporting.md) and [docs/features/cube-export.md](docs/features/cube-export.md)).

**Latest release: 1.0.13** (2026-10-03) — npm `1.0.13`, PyPI `1.0.13`, NuGet `1.0.13`, Maven Central `8.0.13`. A PATCH: an already-plural entity name (`Stats`, `Settings`) no longer doubles in API-surface names (REST paths, hooks, finders, DbSets) in any port, while default physical table names stay frozen on the old rule; two entities that would share one API name are now a generation error; two Kotlin controller compile fixes (`field.inet` filter ops, `@dbColumnType: uuid` on a string field). Gated by a private `1.0.13-rc.1` build on the adopter estate (`rc-gate.sh` 7/7) and a full `--strict-toolchains` local CI run. The previous release, 1.0.12 (2026-10-02), added `fmt` in every CLI, a deprecated-reference `verify` advisory, the `onLocate` extract hook, and an Exposed 1.x Kotlin output mode.

Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ it until 1.1 ships._

### Added

- **TypeScript: the `cube-model` reference generator (FR-044 Plan 4).** `cubeModel()` writes the
reporting vocabulary as Cube data-model files, `model/cubes/<Cube>.yml`: a cube for each
table-backed entity that declares dimensions, measures or segments, a join for each to-one
reference between cubes, dimensions (a time dimension carries its `@grains` as
`meta.grains`), measures, segments, and a `rollup` pre-aggregation for each served report. A
report with a relative date (in its `@filter`, its `@segment` or a listed measure's condition)
gets no rollup, and a `<report>Scope` segment when it has a `@filter`. A served report that
declares `@spine` is a Cube view, `model/views/<Report>.yml`, rooted at the spine cube over a
`public: false` `<Report>Facts` cube whose own `sql` holds the report's scope (so a spine row
whose facts are all filtered out keeps its row), with no rollup; the ordinary cubes gain no
reverse join. It is opt-in
(`meta init` wires nothing), listed by `meta gen --list` in the `capability` layer, ejectable
(`meta eject cube-model`) and drift-checked by `meta verify --codegen`. It writes Postgres
and MySQL SQL and refuses `sqlite` and `d1`. What Cube cannot hold is an `ERR_CUBE_*`
generation error that names the node; the exporter never renames or drops. **No vocabulary
change:** `metamodelVersion` stays 1.1 and `expected-registry.json` is untouched. The report
view lowering and the exporter now share one SQL module, moved without changing a byte of
any view. A measure's `@default` is a `public: false` `<m>Raw` aggregate and the measure as
`COALESCE({<m>Raw}, n)` (a ratio's default wraps its quotient, and an operand's own default
reaches the ratio), so Cube reads the default where the view does. A MySQL tuple distinct
count is the view's own `COUNT(DISTINCT a, b)`. Gated by the new `fixtures/cube-model/` corpus
(44 cases and a canonical golden; the 29th shared corpus in `docs/CONFORMANCE.md`, run by
TypeScript only).
See [docs/features/cube-export.md](docs/features/cube-export.md).
- **A `cube` lane checks the exporter against a real Cube.** `scripts/ci-local.sh --only cube`
(also `scripts/integration-test.sh cube`, `bun run test:cube` in `integration-tests`, and a
`cube` entry in `integration-tests.yml`) loads the canonical model's output into
`cubejs/cube:v1.7.43` over a private `postgres:16-alpine`, requires the Cube query for each
served report to return the rows of its view (a `@spine` report through its Cube view, its
empty spine rows included), requires each of the 34 corpus cases that hold a tree to compile
(the two MySQL ones included), and reads the escaping case's literals and a ratio over a
defaulted operand back from Cube's `/v1/sql`. It owns a private Docker network and an ephemeral `127.0.0.1` port and
never uses the shared Postgres sidecar. Without Docker it is a SKIP behind a banner (a failure
under `--strict-toolchains`). The full `scripts/ci-local.sh` runs it; `--quick` and
`--no-integration` do not.
- **Python: a run-time validator runner, `run_validators`.** `metaobjects.runtime.run_validators(entity, data)`
validates a data mapping against an entity's metadata with no generated code and no database,
and `ObjectManager.validate(entity_name, data)` does the same for a loaded entity. It is the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ first-week wedge plan — and `meta init` picks up from there.
| Template-drift verify | Yes | Yes (`Verify.check`) | Yes (via Java) | Yes (`dotnet meta verify`) | Yes (`metaobjects.render.verify`) |
| YAML authoring (sigil-free → JSON) | Yes | Yes | Yes (via Java) | Yes | Yes |
| Capability requirements (`requirement.*`) | `meta verify` gate + authoring lint; `requirementTests()` stubs | `mvn metaobjects:verify` gate; JUnit Jupiter `requirement-tests` | Gate via the Java Maven goal; JUnit Jupiter `requirement-tests` | `dotnet meta verify` gate; xUnit `requirement-tests` | `metaobjects verify` gate; pytest `requirement-tests` |
| Reporting vocabulary (`dimension` / `measure` / `segment` / `object.report`, FR-044) | Registered; a view-backed report becomes a SQL view in `meta migrate`, is read by `ObjectManager`, and gets generated read-only list routes (no client hook yet) | Registered; OMDB reads a view-backed report; generates a read-only Spring controller, DTO, repository and filter allowlist | Registered (via Java); generates an Exposed table object, a row data class, a filter allowlist and a read-only Spring controller per view-backed report | Registered; generates a keyless EF Core row type, read-only routes and a filter allowlist per view-backed report | Registered; `ObjectManager` reads a view-backed report; generates a Pydantic row model, a read-only FastAPI router and a filter allowlist |
| Reporting vocabulary (`dimension` / `measure` / `segment` / `object.report`, FR-044) | Registered; a view-backed report becomes a SQL view in `meta migrate`, is read by `ObjectManager`, and gets generated read-only list routes and a TanStack list hook (no grid or form); the `cube-model` generator writes the vocabulary as Cube data-model files | Registered; OMDB reads a view-backed report; generates a read-only Spring controller, DTO, repository and filter allowlist | Registered (via Java); generates an Exposed table object, a row data class, a filter allowlist and a read-only Spring controller per view-backed report | Registered; generates a keyless EF Core row type, read-only routes and a filter allowlist per view-backed report | Registered; `ObjectManager` reads a view-backed report; generates a Pydantic row model, a read-only FastAPI router and a filter allowlist |
| Libraries (`libraries: [...]`) | Yes | Yes | Yes (via Java) | Yes | Yes |
| Metadata dependencies (`dependencies`) | Yes (`meta deps sync`, `path` transport) | Phase 2 | Phase 2 | Phase 2 | Yes (loads the synced snapshot) |
| Runtime metadata (ObjectManager-style) | Yes (`runtime-ts`) | Yes (OMDB) | Yes (via Java OMDB + Exposed) | Roadmap | Yes (ObjectManager) |
Expand Down
14 changes: 14 additions & 0 deletions agent-context/skills/metaobjects-authoring/references/reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ A concrete report whose read source is `@kind: view` is served by every port's g

The files each port generates are in the `metaobjects-codegen` skill's reference for that language ("Reports").

## Exporting to Cube

The TypeScript `cube-model` generator (`meta eject cube-model`; see the `metaobjects-codegen` skill's
TypeScript reference) writes this vocabulary as Cube data-model files: a cube for each table-backed
entity that declares dimensions, measures or segments, and a rollup for each served report. It keeps
your names as written, so an entity, dimension, measure or segment name must be one Cube accepts (a
letter first, then letters, digits and `_`, and not a Python keyword such as `from`, `class` or `in`);
otherwise the export fails with `ERR_CUBE_INVALID_NAME` and renames nothing. It also refuses a
dimension over an array, object or map field. A report with a relative date in its `@filter`,
its `@segment`'s filter or a listed measure's condition gets no rollup. Its `<report>Scope`
segment is written only when it has a `@filter`. A served report with `@spine` becomes a Cube view
with its scope inside a facts cube and no rollup, so the spine's empty rows survive, and a measure's
`@default` is a `COALESCE`, as in the view. A report that is not served contributes nothing. Reference: `docs/features/cube-export.md`.

## What a report does not have

In TypeScript a served report gets a generated list hook; no port generates a grid or a form for a report, and the other ports have no client tier, so there you get the route and the row type. There is no `measure.derived`, no query-time choice of dimensions or measures, and no time-zone vocabulary.
Loading
Loading