Skip to content

fix: canonicalize decimal wire and close 1.1.0 release review findings - #445

Merged
dmealing merged 4 commits into
mainfrom
fm/mo-1-1-0-rc7-fixes
Oct 21, 2026
Merged

dmealing merged 4 commits into
mainfrom
fm/mo-1-1-0-rc7-fixes

Conversation

@dmealing

@dmealing dmealing commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Decimal fields cross every generated API as one canonical string (ADR-0019 Amendment 1). Input with more fraction digits than @scale, or more integer digits than @precision - @scale, is rejected with 400 and never rounded. Decimal filter operands compare exactly, and a malformed operand returns 400 invalid_filter_value. Each port implements this idiomatically: TS decimal-wire and generated routes, C# DecimalWire.g.cs, Java CanonicalDecimal* and DeclaredDecimal, Kotlin MetaDecimalWire.kt, and Python decimal_wire.
  • Report scenarios run on SQLite and MySQL in every port, with D1 in TS. Java gains an OMDB SQLiteDriver, and Python gains SQLite and MySQL drivers. The Java in-memory report lane is replaced by database-backed OMDB lanes. TS routes give a TPH base the decimal columns that only a subtype declares.
  • Breaking generated-code changes are recorded in docs/features/migrations/upgrading-1.0-to-1.1.md. C# scalar and enum arrays become IList<T>. Kotlin gains per-package decimal and portable-type helper files. eject --list reports owned companion files. The publish-*.yml workflows gain the release-tag gate from scripts/check-release-tag.mjs.
Validation

Risk Assessment

✅ Low: The latest fix round makes Python coerce_decimal_fields and the Kotlin controller (create range check and PATCH bind) handle decimal arrays element by element, matching C#, Java and the TS runtime, and I found no defect in them.

Testing

Live-driven: Python list decimal coerce accepts and canonicalizes elements, and refuses an element past @scale (live=true); eject --list lists owned companion files (live=true). Not live-driven, so untested under the contract: Kotlin per-element W3 range check (harness only), TS decimal array canonicalization (direct function calls, not a running server), TS full regression (12017 pass in log, not driven live), Python full non-integration run (3 failures from missing 'metaobjects' install, not live-driven), fractional validator bound, and Python integration tests (pg8000 missing). Verdict inconclusive.

  • Live validation: ⚠️ inconclusive - 3 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Python: decimal array (isArray) accepted and canonicalized element-wise on write ✅ pass live py-decimal-array.txt: accept ['1.5','2'] -> wire ['1.5','2']
Python: decimal array element with too many fraction digits refused (400-class ValueError, not rounded) ✅ pass live py-decimal-array.txt: 'amounts: more than 2 fraction digit(s)'
Kotlin: generated decimal wire per-element W3 range check and canonical strings ⏸️ untested no Only the mvn -o test -Dtest=KotlinDecimalWireTest harness ran (13 tests, 0 failures). The generated code was not driven through a served generated controller, so no live result is established.
TS: decimal array write canonicalizes elements, no throw; read strips scale (100.5000 -> 100.5) ⏸️ untested no Only unit-level runs ran (runtime-ts type-coercer 12 pass, codegen-ts decimal-wire-e2e 8 pass). The ObjectManager coercer functions were called directly, not through a running server, so no live resul…
eject --list reports owned companion files after ejecting a generator ✅ pass live eject-list-owned.txt: codegen/templates/* and runtime files listed as identical to package
Fractional validator.numeric @min/@max fails to load (ERR_BAD_ATTR_VALUE) ⏸️ untested no Not driven; only documentation changed for this decision. Would need a meta.json with @min 0.5 loaded by meta verify.
Full regression: TS bun test (server/typescript) ⏸️ untested no Suite run is a unit and test-harness run, not driven against the live product. No live result is established.
Full regression: Python pytest non-integration (server/python) ⏸️ untested no py-suite.txt shows 3 failed, 3748 passed, EXIT=1, all PackageNotFoundError for 'metaobjects' in test_cli_surface. The failure was not driven live and was not re-run after an editable install, so no li…
Full regression: Python integration (server/python tests/integration) ⏸️ untested no Optional dependency missing; a disposable venv with pg8000 installed would allow the run.
Evidence: Python decimal array live transcript
accept: ['1.5', '2']
wire: ['1.5', '2']
reject: amounts: more than 2 fraction digit(s)
Evidence: eject --list owned copies live output
Ejectable generators (copy any of these into codegen/generators/ and own it):

@metaobjectsdev/codegen-ts:
  entity  [owned — identical to the reference]
  queries
  routes
  routes-hono
  barrel
  names
  prompt-render
  output-parser
  extractor
  output-prompt
  render-helper
  requirement-tests
  cube-model
  callable
  trace-helper
  shared-model
  docs
  api-docs
  requirements
  agent-docs
  metamodel-docs

@metaobjectsdev/codegen-ts-react:
  form

@metaobjectsdev/codegen-ts-tanstack:
  hooks
  grid
  grid-hook

HTTP adapter source this project owns (codegen/runtime/), against the installed @metaobjectsdev/runtime-ts:
  codegen/runtime/drizzle-fastify/filter-allowlist.ts  [identical to the package]

Template source this project owns (codegen/templates/), against the installed codegen packages:
  codegen/templates/date-time-regex.ts  [identical to the package]
  codegen/templates/decimal-wire-helper.ts  [identical to the package]
  codegen/templates/drizzle-schema.ts  [identical to the package]
  codegen/templates/entity-constants.ts  [identical to the package]
  codegen/templates/entity-file.ts  [identical to the package]
  codegen/templates/enum-int-codec.ts  [identical to the package]
  codegen/templates/enums-file.ts  [identical to the package]
  codegen/templates/field-meta.ts  [identical to the package]
  codegen/templates/filter-allowlist.ts  [identical to the package]
  codegen/templates/filter-shared.ts  [identical to the package]
  codegen/templates/filter-type.ts  [identical to the package]
  codegen/templates/inferred-types.ts  [identical to the package]
  codegen/templates/jsdoc.ts  [identical to the package]
  codegen/templates/net-regex.ts  [identical to the package]
  codegen/templates/projection-decl.ts  [identical to the package]
  codegen/templates/relations-block.ts  [identical to the package]
  codegen/templates/tph-discriminator.ts  [identical to the package]
  codegen/templates/value-object-file.ts  [identical to the package]
  codegen/templates/view-decl.ts  [identical to the package]
  codegen/templates/zod-enum.ts  [identical to the package]
  codegen/templates/zod-validators.ts  [identical to the package]

The docs site's theme (its templates and assets, into codegen/docs-site/):
  docs-site

Shipped libraries (copy one into your own sources and own it):
  ai
  iam

Run: meta eject <name>
Evidence: TS full suite output (EXIT=0)
bun test v1.3.14 (0d9b296a)

packages/metadata/test/registry-coverage.test.ts:

[registry-coverage] subtypes: 53/65 exercised (81.5%), 12 UNTESTED
[registry-coverage] untested subtypes:
  attr.boolean
  attr.class
  attr.double
  attr.expression
  attr.filter
  attr.int
  attr.intMap
  attr.long
  validator.atLeastOne
  validator.comparison
  validator.presentIff
  validator.requiredWhen
[registry-coverage] exercised subtypes with untested attrs: 27

packages/codegen-ts/test/format.test.ts:
[codegen-ts] Biome formatter: 2 diagnostics; first: expected `,` but instead found `'x'`

packages/cli/test/migrate-drop-unmanaged.test.ts:
meta: migrate: refusing to drop public.theirs — absent from the committed schema snapshot, so this toolchain never managed it and the migration could not replay against a database where it never existed. Re-run with '--allow drop-unmanaged' if the drop is intended.
meta migrate — sqlite, file:/tmp/drop-unmanaged-kamivN/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-kamivN/.metaobjects/migrations/20261021174124-x/up.sql
    /tmp/drop-unmanaged-kamivN/.metaobjects/migrations/20261021174124-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-z4vMpz/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-z4vMpz/.metaobjects/migrations/20261021174124-x/up.sql
    /tmp/drop-unmanaged-z4vMpz/.metaobjects/migrations/20261021174124-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-8ZzsfD/t.db

  Changes: 1 drop-table

  Written:
    /tmp/drop-unmanaged-8ZzsfD/.metaobjects/migrations/20261021174124-x/up.sql
    /tmp/drop-unmanaged-8ZzsfD/.metaobjects/migrations/20261021174124-x/down.sql

meta: migrate: refusing to drop public.orders.orders_job_id_fk — absent from the committed schema snapshot, so this toolchain never managed it and the migration could not replay against a database where it never existed. Re-run with '--allow drop-unmanaged' if the drop is intended.
meta migrate — sqlite, file:/tmp/drop-unmanaged-fk-muwTca/t.db

  Changes: 1 drop-fk

  Written:
    /tmp/drop-unmanaged-fk-muwTca/.metaobjects/migrations/20261021174124-x/up.sql
    /tmp/drop-unmanaged-fk-muwTca/.metaobjects/migrations/20261021174124-x/down.sql

meta migrate — sqlite, file:/tmp/drop-unmanaged-fk-mhSkmQ/t.db

  Changes: 1 drop-fk

  Written:
    /tmp/drop-unmanaged-fk-mhSkmQ/.metaobjects/migrations/20261021174124-x/up.sql
    /tmp/drop-unmanaged-fk-mhSkmQ/.metaobjects/migrations/20261021174124-x/down.sql


packages/cli/test/help-and-exit.test.ts:
meta gen — codegen TS targets from your declared metadata

USAGE:
  meta gen [<entity>...] [flags]

FLAGS:
  --dry-run             Compute and print, don't write
  --list                Print the generator CATALOG (name, layer, tier, what it emits,
                        what to install) and exit. Codegen is opt-in: nothing runs until
                        you wire it, and this is where you choose. No config or metadata
                        required — add --format json for the same catalog, machine-readable.
  --probe               With --list: construct every catalog generator and dry-run it
                        against YOUR model, reporting how many files each would emit.
                        A count per generator beats any category label — and it cannot go
                        stale, because it runs the generators. Needs a project.
  --baseline <default|adopt|fresh>
                        First-time-on-existing-file behavior. Default: refuse a file that
                        cannot be proved to be generated output. "adopt" records the files
                        you have as the merge base and writes NOTHING — the first run a
                        project with no committed .hashes.json can perform. "fresh"
                        overwrites them with fresh output and re-baselines (discards edits).
  --no-antipatterns     Suppress the advisory "hand-rolled what MetaObjects can model" pass
  --limit <n|all>       Advisory lines TEXT output prints before truncating (default 20)
  --generators <names>  Narrow the run to these DECLARED generators, by stable name, comma-
                        separated (e.g. docs,api-docs to write only the documentation). A
                        name metaobjects.config.ts does not declare is refused: it never adds one.
  --format <toon|json|text>   Output format (global flag; default toon off-TTY)
  <entity> [<entity>]   Positional filter on entity names
  --help, -h            Print this help

A real write run (not --dry-run) also runs an ADVISORY anti-pattern pass: it scans
your authored source for hand-rolled aggregates, money-as-float, and CHECK-IN enums
and points you at the construct that models them (origin.aggregate / field.currency /
field.enum). Warnings only — it never fails the build. Opt out with --no-antipatterns
or META_NO_ANTIPATTERNS=1.

Text output caps that list at --limit lines (default 20) and says how many it held
back. --format toon / --format json carry EVERY finding in the payload's
antiPatterns block, uncapped — --limit is a display cap for a terminal, never a
limit on the machine-readable result.

NOTE: outDir, dialect, dbImport, extStyle are read from metaobjects.config.ts

meta migrate — diff metadata vs live DB; emit migration SQL files

USAGE:
  meta migrate [baseline|apply-pending] [flags]

SUBCOMMANDS:
  baseline             Snapshot an EXISTING database's schema as the reference point
                       (use with --from-db). NOTE: for a brand-new/empty database use
                       the greenfield example below, NOT baseline — an offline baseline
                       records your metadata as already-applied and emits no CREATE TABLE.
  apply-pending        Replay committed migration files against --db (no diff).
                       Provisions a fresh/CI database when the chain BUILDS the
                       schema — 'meta verify --replay' proves that it does. A
                       database adopted via 'baseline --from-db' has no such
                       chain. postgres/sqlite only.

MIGRATE FLAGS:
  --db <url>           DB connection URL (required for live-introspect / --apply / --rollback)
                       Supports: file:, libsql:, postgres:, postgresql:
  --dialect sqlite|postgres|d1
                       Optional dialect override (auto-detected from URL scheme)
  --migration-format default|flyway
                       Migration file layout (default: default). 'flyway' emits
                       V<N>__<slug>.sql + U<N>__<slug>.sql for a Flyway runner;
                       --apply/apply-pending/--rollback are refused under it
                       because Flyway owns apply and flyway_schema_history.
                       Also settable once as migrate.format in
                       .metaobjects/config.json.
  --out-dir <path>     Migration directory (default: ./.metaobjects/migrations;
                       flyway: src/main/resources/db/migration)
  --slug <name>        Required to WRITE a migration (not for --dry-run) (e.g., --slug add-user-shipping)
  --allow <csv>        Comma-separated destructive-change permissions:
                       drop-column,drop-table,type-change,drop-index,drop-fk,
                       drop-check,drop-view,drop-view-cascade,
                       adopt-view,nullable-to-not-null,drop-identity-default,
                       drop-unmanaged
                       drop-column DELETES the column and every value in it. If
                       the column was RENAMED, use --rename-column instead: it
                       keeps the data.
                       drop-unmanaged permits dropping a table/view the committed
                       snapshot never contained — one this toolchain never managed.
                       Without it that drop is refused, because the migration it
                       writes cannot replay against a database where the object
                       never existed.
  --on-ambiguous abort|rename|drop-add
                       How to handle ambiguous renames (default: abort)
  --rename-table [schema.]old=new
                       Declar

... [244188 bytes truncated] ...

file": "out--site/site/acme/shop/StoreTotals.html",
      "status": "new"
    },
    {
      "file": "out--site/site/acme/shop/WorkoutEvent.html",
      "status": "new"
    },
    {
      "file": "out--site/site/coverage.html",
      "status": "new"
    },
    {
      "file": "out--site/site/assets/site.css",
      "status": "new"
    },
    {
      "file": "out--site/site/assets/site.js",
      "status": "new"
    },
    {
      "file": "out--site/site/assets/search-index.json",
      "status": "new"
    }
  ],
  "summary": "22 written",
  "help": [
    "typecheck the generated code with `npx tsc`",
    "create your database tables with `meta migrate --from-db --db <url> --dialect postgres --slug init --apply`"
  ],
  "antiPatterns": {
    "status": "ran",
    "total": 0,
    "rows": []
  },
  "warnings": [
    "1 wired generator(s) matched nothing and wrote no file: requirements-file. That is often correct — a generator whose metadata this model does not declare has nothing to emit. It is reported because a run that emitted nothing and a run that did the work otherwise look identical."
  ]
}

packages/cli/test/unit/migrate-d1.test.ts:
migrate: wrote /tmp/migrate-d1-cmd-JbdgjH/migrations/0001_init.sql
migrate: wrote /tmp/migrate-d1-cmd-JbdgjH/migrations/.down/0001_init.sql
  create-table: 1
migrate: wrote /tmp/migrate-d1-cmd-qz9mmA/migrations/0001_init-programs.sql
migrate: wrote /tmp/migrate-d1-cmd-qz9mmA/migrations/.down/0001_init-programs.sql
  create-table: 2
  add-fk: 1
  create-view: 1
meta: migrate: no wrangler.toml found in /tmp/migrate-d1-cmd-7WOw1p or parents; pass --d1 <binding> to bypass
meta: migrate: --db / DATABASE_URL is not used for dialect 'd1' — wrangler.toml owns connection

packages/cli/test/unit/verify-strict.test.ts:
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta: failed to load metadata: ERR_UNKNOWN_ATTR: Unknown attribute '@madeUpAttr' on field.string 'email' — not declared by any registered provider for field.string
  in meta.users.json at $['metadata.root'].children[0]['object.entity'].children[1]['field.string']
meta: meta verify is strict (ADR-0023): every authored @attr must be declared. Fix: register the attr on a metadata provider, OR move arbitrary author-supplied properties into an `attr.properties` bag, OR re-run with `meta verify --lax` to keep the legacy open-attr load.
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — no template.* nodes found; the template gate had nothing to check.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — no template.* nodes found; the template gate had nothing to check.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it

packages/cli/test/unit/verify-output.test.ts:
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta: failed to load metadata: ERR_INVALID_TEMPLATE: template "Broken" @payloadRef "DoesNotExist" does not resolve to an object.value or sourceless object.projection at root
  in meta.ai.json at $['metadata.root'].children[0]['template.output']
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta: [Welcome] (email/html) ERR_VAR_NOT_ON_PAYLOAD: nonExistentField (prompts/e/html:1)
meta: meta verify — 1 drift error(s) across 1 template(s).
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta verify — 1 template(s) clean.
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), --replay/--replay-snapshot (the committed migration chain replays from empty).
meta: [Doc] (document) ERR_VAR_NOT_ON_PAYLOAD: missingField (prompts/o/x:1)
meta: meta verify — 1 drift error(s) across 1 template(s).
meta verify — not run: schema (--db <url>), codegen (--codegen), deps (--deps), replay (--replay) — a bare 'meta verify' runs only the template gate (plus the requirement ledger); name each gate to run it

 12017 pass
 50 skip
 0 fail
 4 snapshots, 40880 expect() calls
Ran 12067 tests across 1071 files. [995.68s]
EXIT=0
  • Evidence: Python non-integration suite output (EXIT=1) (local file: ~/.no-mistakes/evidence/01M5FP64V387HRG1Y9N1EMNNDE/py-suite.txt)
  • Outcome: ⚠️ 2 warnings across 1 run (37m51s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ server/typescript/packages/runtime-ts/src/type-coercer.ts:314 - The TS ObjectManager write path now throws on any field.decimal declared @isArray (for example numeric[] on Postgres). bindDecimals loops over every decimal child with no resolvedIsArray() skip. It passes the whole array to parseDecimalInput, which only accepts a string or a number and returns undefined for an array, so the function throws "field.decimal '<name>' cannot be written ... not a decimal number". Decimal arrays are a supported shape (migrate-ts emits a numeric[] column for them), and this write used to pass through. Sibling inconsistencies of the same class: (1) canonicalizeDecimals (type-coercer.ts:292) also ignores arrays, so array elements are never canonicalized on read; (2) decimalFieldNames (codegen-ts/src/api-surface.ts:127) explicitly excludes arrays, so generated TS routes send decimal arrays as the driver returns them (Postgres &#34;100.5000&#34;). C# (DecimalListJsonConverter), Python (to_wire over lists) and Java (contentUsing) all emit canonical strings for array elements, so W1 is not cross-port for arrays. The smallest fix is to skip array fields in bindDecimals and map the element-wise rule over arrays in both coercers and in the route mount.
  • ⚠️ CHANGELOG.md:863 - Rule A tightens attr.int and attr.long in TS, C# and Python, so a fractional value such as @min: 0.5 is now ERR_BAD_ATTR_VALUE. validator.numeric declares @min and @max as attr.int, so a fractional bound on a decimal or double field no longer loads. Java already rejected it, so the ports now agree. This contradicts the CHANGELOG sentence at CHANGELOG.md:863, still in the 1.1.0 section: "No port changed its general attr.int check, so a fraction in a validator's @min or @max loads where it did." docs/features/migrations/upgrading-1.0-to-1.1.md does not mention it. Decide one of two things: (a) the rejection is intended, in which case fix the stale CHANGELOG sentence and add an upgrade-guide entry (fractional validator.numeric bounds now fail to load); or (b) make validator.numeric @min/@max accept fractions, since a bound like 0.01 is legitimate on a decimal field and is now inexpressible in every port. The choice is a product decision.

🔧 Fix applied.
1 warning still open:

  • ⚠️ server/python/src/metaobjects/codegen/runtime/decimal_wire.py:127 - The decimal-array fix is incomplete for Python. The fix round made the TS write path, the TS read path and the TS route mount element-wise, but the generated Python routers still reject any field.decimal @isArray on write. router_generator._decimal_field_names (router_generator.py:184) lists every decimal field, arrays included, in _DECIMAL_FIELDS. coerce_decimal_fields then calls parse_decimal(list), which raises ValueError(&#34;list is not a decimal&#34;), so the route answers 400 {&#34;error&#34;:&#34;validation&#34;}. I generated a router for an entity with field.decimal prices isArray @precision 12 @scale 4: it emits dto = coerce_decimal_fields(dto, _DECIMAL_FIELDS, _DECIMAL_LIMITS) and _DECIMAL_LIMITS = {&#34;prices&#34;: (12, 4)}. Calling coerce_decimal_fields({&#39;amounts&#39;:[&#39;1.5&#39;,&#39;2&#39;]}, frozenset({&#39;amounts&#39;}), {&#39;amounts&#39;:(10,2)}) raises ValueError: amounts: list is not a decimal. Before this change that write passed. Only the read side (to_wire over lists) is element-wise. This is the same defect class as review-1, left behind in a sibling port. Sibling site: Kotlin skips the W3 range check for array elements (KotlinSpringControllerGenerator.kt:1239, appendDecimalRangeCheck returns early for isArrayType), so an array element with more fraction digits than @scale is not refused there, although the TS fix and Java's bindDecimalList apply W3 per element. Fix: in coerce_decimal_fields, when the value is a list, parse each element (null stays None) and apply check_decimal_range to each. Add a Python behavioural test, and a Kotlin per-element range check, with canonical-string assertions.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 warnings
  • ⚠️ server/python/tests/codegen/test_cli_surface.py - Python non-integration suite: 3 failed, 3748 passed. The 3 failures are tests/codegen/test_cli_surface.py::test_a_version_flag_prints_the_bare_version[--version|-v|-V] raising PackageNotFoundError for 'metaobjects'. The package is not installed in this environment (no editable install). Environment setup gap, not a code defect. Not re-run in an editable venv.
  • ⚠️ live validation verdict: inconclusive (3 of 9 scenarios were driven live against the product); untested: Kotlin: generated decimal wire per-element W3 range check and canonical strings, TS: decimal array write canonicalizes elements, no throw; read strips scale (100.5000 -> 100.5), Fractional validator.numeric @min/@max fails to load (ERR_BAD_ATTR_VALUE), Full regression: TS bun test (server/typescript), Full regression: Python pytest non-integration (server/python), Full regression: Python integration (server/python tests/integration)
  • Live validation: ⚠️ inconclusive - 3 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Python: decimal array (isArray) accepted and canonicalized element-wise on write ✅ pass live py-decimal-array.txt: accept ['1.5','2'] -> wire ['1.5','2']
Python: decimal array element with too many fraction digits refused (400-class ValueError, not rounded) ✅ pass live py-decimal-array.txt: 'amounts: more than 2 fraction digit(s)'
Kotlin: generated decimal wire per-element W3 range check and canonical strings ⏸️ untested no Only the mvn -o test -Dtest=KotlinDecimalWireTest harness ran (13 tests, 0 failures). The generated code was not driven through a served generated controller, so no live result is established.
TS: decimal array write canonicalizes elements, no throw; read strips scale (100.5000 -> 100.5) ⏸️ untested no Only unit-level runs ran (runtime-ts type-coercer 12 pass, codegen-ts decimal-wire-e2e 8 pass). The ObjectManager coercer functions were called directly, not through a running server, so no live resul…
eject --list reports owned companion files after ejecting a generator ✅ pass live eject-list-owned.txt: codegen/templates/* and runtime files listed as identical to package
Fractional validator.numeric @min/@max fails to load (ERR_BAD_ATTR_VALUE) ⏸️ untested no Not driven; only documentation changed for this decision. Would need a meta.json with @min 0.5 loaded by meta verify.
Full regression: TS bun test (server/typescript) ⏸️ untested no Suite run is a unit and test-harness run, not driven against the live product. No live result is established.
Full regression: Python pytest non-integration (server/python) ⏸️ untested no py-suite.txt shows 3 failed, 3748 passed, EXIT=1, all PackageNotFoundError for 'metaobjects' in test_cli_surface. The failure was not driven live and was not re-run after an editable install, so no li…
Full regression: Python integration (server/python tests/integration) ⏸️ untested no Optional dependency missing; a disposable venv with pg8000 installed would allow the run.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test runtime-ts test/type-coercer.test.ts (12 pass)
  • bun test codegen-ts test/decimal-wire-e2e.test.ts (8 pass)
  • pytest server/python/tests/runtime/test_decimal_wire.py (79 pass)
  • live: python3 -I script calling coerce_decimal_fields with list and out-of-scale element (evidence py-decimal-array.txt)
  • mvn -o test -Dtest=KotlinDecimalWireTest in server/java/codegen-kotlin (13 pass)
  • live: bun server/typescript/packages/cli/bin/meta.ts init, eject entity, eject --list --format text in temp project (evidence eject-list-owned.txt)
  • bun test full server/typescript: 12017 pass, 50 skip, 0 fail, EXIT=0 (ts-full-suite.txt)
  • pytest server/python --ignore=tests/integration: 3748 pass, 3 fail (PackageNotFoundError: metaobjects in test_cli_surface version tests), EXIT=1 (py-suite.txt)
  • pytest server/python tests/integration blocked by missing pg8000
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Validation

  • No-mistakes validation runs: 1 (two review fix rounds inside it; no second run).
  • Before the gate, the full scripts/ci-local.sh (every port, the 5-port Testcontainers integration matrix and the Cube live check) was green on 06adeb34, so the gate's CI step was skipped as local CI.
  • Review found, and the gate fixed, decimal arrays (field.decimal @isArray): TS writes threw and elements were not canonicalized on read or in generated routes; the Python router refused array writes and Kotlin skipped the per-element @precision/@scale check. It also corrected the stale CHANGELOG sentence on fractional validator bounds (which now fail to load in TS, C# and Python, as in Java) and added the upgrade-guide entry.
  • Test step: approved exception, not a clean pass. The gate's live verdict was inconclusive (3 of 9 scenarios driven live; 3 Python failures came from the gate environment lacking an editable install). Every untested item was run locally on the gate head 532c123b: TS runtime-ts 819 and codegen-ts 2752 pass, including the decimal-array cases; Python 4115 pass including integration; Kotlin codegen 549, integration 361 and Exposed 1.x 4 pass, including KotlinDecimalWireTest; a fractional validator.numeric @min gives ERR_BAD_ATTR_VALUE.

Fixes everything the 1.1.0 release review found, plus the adopter-found
`eject --list` defect, and closes the test gaps it named.

- Decimals: one canonical string on the wire in every port (W1-W4), a
  shared decimal-wire corpus and an api-contract decimal/ sub-corpus, exact
  filter operands, and 400 validation past @precision/@scale. The stronger
  assertions found the TypeScript TPH polymorphic read sending a
  subtype-only decimal uncanonicalized; fixed in the generator.
- Report lanes: every port runs the report scenarios on SQLite and MySQL
  (Java OMDB SQLiteDriver, Python SQLite/MySQL drivers), the Java report
  lane is database-backed, and the Cube lane queries every exporter shape.
- Loader: four more report shapes refused (D5, M10, R10, R11), an integral
  float accepted where an int is declared, the requirement package and
  ERR_UNRESOLVED_SUPER hint fixed.
- eject --list reports owned companion files; JVM and Python naming
  discovery; TS forms step/inputMode; config unknown keys warn.
- Breaking (recorded in CHANGELOG and the 1.0-to-1.1 upgrade guide): C#
  scalar and enum arrays are IList<T>; a Java POJO redeclaring an inherited
  field with another subtype is standalone; Kotlin gains per-package
  MetaDecimalWire.kt and MetaPortableColumnTypes.kt.
…ew-2 CHANGELOG and upgrade-guide fixes were already present in the diff. Edited the reporting skill decimal line (source plus five fixture copies, which mirror it) to the canonical-string rule, and added the companion-file clause to the own-your-codegen eject --list bullet.
@dmealing
dmealing merged commit c8b168b into main Oct 21, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-1-1-0-rc7-fixes branch October 21, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant