You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2f9f367
Browse filesBrowse the repository at this point in the historyBrowse files
fix: make the JSON wire form a project setting, defaulting to each language's standard (#454)
1.1.0 normalized every port's generated API onto one wire form with no opt-out:
decimals as canonical strings and value-object null members omitted. That broke
live C#, Java and Kotlin clients on a 1.x upgrade.
The output form is now `wire` in .metaobjects/config.json, read by every port's
codegen the way `naming` is (ADR-0019 Amendment 2). Both keys default to native,
the language's standard serializer output: C#/Java/Kotlin decimals are JSON
numbers again and null members are written; Python and TypeScript keep the
decimal's scale. `{"decimals": "string", "nullMembers": "omit"}` restores the
1.1.0 form, and the conformance corpora pin it in their own config. Input rules
are unchanged in every mode. Every port's codegen drift gate names the wire
block that reproduces committed output generated under another setting.
Also fixes the C# DecimalWire.g.cs / MetaNetValidation.g.cs collision when a
model binds more than one namespace.
Fixes#454
Copy file name to clipboardExpand all lines: .claude/rules/cross-language-porting.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,7 @@ Preserve the following contracts exactly across all language ports:
33
33
**Wire format:**
34
34
- Currency: integer minor units on the wire always. Float arithmetic for money is forbidden.
35
35
- Pagination: `?limit=N&offset=N` — identical across all generated endpoints.
36
+
-**Output form is a project setting, and the default is each language's own** (ADR-0019 Amendment 2): `wire` in `.metaobjects/config.json` (`decimals`: `native`|`string`; `nullMembers`: `native`|`emit`|`omit`), read by the same config reader as `naming` and carried on the loaded root. `native` means what that port's standard serializer writes. Never make a port's default match another port or a corpus. The corpora pin `{"decimals": "string", "nullMembers": "omit"}` in their own config, and a harness reads it from there. Input rules (W2, W3) hold in every mode. Shared cases: `fixtures/wire-conformance/`.
36
37
-**Runtime return types**: a port's runtime `ObjectManager` returns **native in-process language types** (`field.decimal`→`BigDecimal`/`decimal`/`Decimal`, TS `string`; temporal→native; jsonb→native map). Wire canonicalization (the bullets above + `normalization.md`) is applied at the **serialization boundary**, never inside the runtime query path. See [ADR-0019](spec/decisions/ADR-0019-runtime-return-type-contract.md).
Copy file name to clipboardExpand all lines: AGENTS.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,7 +72,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
72
72
-**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.
73
73
74
74
**Cross-port conformance corpora** (every port runs the shared corpus):
75
-
- Metamodel: `fixtures/conformance/` (410 fixtures; 30 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/` (410 fixtures; 31 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- 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".)
78
78
- 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.
@@ -389,6 +389,8 @@ These are the load-bearing principles that have emerged through implementation.
389
389
390
390
- **Expanding the metamodel vocabulary follows ONE decision procedure (ADR-0037) — driven by semantic behavior, not surface storage.** Don't ask "is X a string/number/date?"; ask "what does X *do*?" Ordered test: (0) **derivable** from existing subtype + attrs (`isArray`/`@maxLength`) + structure? → derive in codegen, add nothing; (1) **physical-only** (native type/meaning unchanged)? → the `@dbColumnType` escape hatch, not first-class vocab; (2) logical — **does X have its own native type, behavior, or attributes** (a *thing* that owns custom logic)? → **subtype** (the extension point: `field.uuid`, `field.uri`, `field.inet`); a **structural variant *within* a subtype** (changes generated shape, shares native type)? → **`@kind`** (the one chartered structural-variant axis: source table/view, uri url/urn — never a catch-all, never on a plain string); otherwise X just **modifies/validates/configures** an existing type → **attribute** (boolean flag `@localTime`; validation `@stringFormat` email/hostname; config `@maxLength`). The "string formats" set splits by behavior: url/uri→`field.uri`, ip→`field.inet` (native types + behavior), only email/hostname→`@stringFormat` (plain validated strings); uuid is already `field.uuid`. Self-documentation over economy; no same-name overloads (hence `@stringFormat`, not a third `@format`). Consult it every time. See [ADR-0037](spec/decisions/ADR-0037-metamodel-vocabulary-expansion-decision-framework.md).
391
391
392
+
- **Each port defaults to its own language's standard behaviour; identical cross-port output is opt-in.** Generated code writes what that language's standard serializer writes: C# decimals as JSON numbers, a value object's null member as `null`. Do not standardize the ports for the convenience of our own conformance tests. A cross-port form, such as `"wire": {"decimals": "string", "nullMembers": "omit"}`, is a project setting that adopters choose. The corpora pin it explicitly in their own config and never decide a default. 1.1.0 broke this rule by making the decimal string and null omission the only output, and 1.1.2 put it back. See [ADR-0019 Amendment 2](spec/decisions/ADR-0019-runtime-return-type-contract.md).
393
+
392
394
- **Pattern-derivable from metadata = codegen, never hand-code.** This is the metaobjects raison d'être. If you find yourself proposing that users hand-write something the metadata fully describes (FK references, basic CRUD, validator chains, type-safe finders, relations() blocks), stop. Codegen it. The only exception is what metadata genuinely cannot express (custom SQL views, regex patterns from outside metadata, business logic). When in doubt, generate.
393
395
394
396
- **Study reference implementations for subtle pipeline behavior; don't re-derive from spec.** For complex orchestration (loader, parser, super resolution, overlay/override merging, registry lifecycle), the spec describes WHAT but the implementation captures HOW — including edge cases and error handling. When porting, read existing implementations first. First-principles reasoning produces subtly wrong behavior that breaks cross-language interop.
Copy file name to clipboardExpand all lines: agent-context/skills/metaobjects-authoring/references/reporting.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -166,7 +166,7 @@ A concrete report whose read source is `@kind: view` is served by every port's g
166
166
-`<segment>` is the report name, snake_cased then pluralized, like any object: `InvoicesByMonth` is `/invoices_by_months`, `StoreTotals` is `/store_totals`. There is no report-specific spelling.
167
167
-**Every derived field with filter operators is filterable and sortable**, dimension and measure alike, with its type's own operators. You cannot narrow that set: a report has no field to put `@filterable` on. A field of the `@from` entity that the report does not expose is refused with a `400`.
168
168
- A time dimension at `day` or coarser is `YYYY-MM-DD`; a null measure is `null` with the key present (a measure with `@default` is never null); `currency` is integer minor units.
169
-
-**A decimal (`avg`, a ratio, a `sum` of a decimal) is one canonical string in every port** (ADR-0019 Amendment 1: `12.5`, never `12.50` or an exponent). Display formatting is a view's job. Do not parse it as a float to compare.
169
+
-**A decimal (`avg`, a ratio, a `sum` of a decimal) is written per the project's `wire` setting**: by default, the port's language-standard form (a JSON number in C#, Java and Kotlin; a string in Python and TypeScript), and under `"wire": { "decimals": "string" }` one canonical string in every port (`12.5`, never `12.50` or an exponent). Do not parse it as a float to compare.
170
170
-`meta gen` writes a model page for every report (`docsFile()`; a sourceless one is marked "Not served") and an API page for a served one (`apiDocsFile()`) — each an ordinary entry in `generators`.
171
171
172
172
The files each port generates are in the `metaobjects-codegen` skill's reference for that language ("Reports").
Copy file name to clipboardExpand all lines: agent-context/skills/metaobjects-codegen/SKILL.md
+15-2Lines changed: 15 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,6 +40,7 @@ exact config, generator names and commands for this project; read it. The other
40
40
|`references/choosing-layers.md`| the generated API does not fit, you need joined or derived data, or you are adopting onto existing code |
41
41
|`references/template-or-code.md`| choosing between a code generator and a Mustache template |
42
42
|`references/physical-names.md`| before you write a table, column or index name as a string |
43
+
|`references/wire-format.md`| a generated API's JSON for decimals or null value-object members is not what a client expects, or you are about to edit a JSON converter or annotation |
43
44
|`references/dependencies-and-shared-models.md`| the project consumes or publishes metadata across projects |
44
45
|`references/typescript-document-store.md`| a TypeScript project on MongoDB, or any store MetaObjects does not manage, wanting a tested repository generator |
45
46
|`references/typescript-mysql.md`| a TypeScript project on a MySQL database |
@@ -60,8 +61,11 @@ in order and every change that needs action.
60
61
args are removed.
61
62
- Documentation is a generator in the project's list, and the docs commands are removed. C#
62
63
and Python also need a generator list (`dotnet meta init`; `metaobjects.config.yaml`).
63
-
- A `field.decimal` crosses the API as one canonical string, and a fractional validator `@min`
64
-
or `@max` is refused (write an integer bound).
64
+
- Upgrade to 1.1.2 or later. With no `wire` setting, each port writes its language's standard
65
+
JSON (C#, Java and Kotlin keep their 1.0.x decimals and null members). A project whose clients
0 commit comments