diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index bfdaf3849..0eb888130 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -4,6 +4,15 @@ name: publish-npm # in tier order (a dependency never lands after its dependent). Mirrors # publish-csharp.yml / publish-python.yml: tag-triggered or manual. # +# WHICH packages, and in WHAT ORDER, is NOT decided here — `scripts/publish-set.mjs` +# is the single source of truth, shared with the local `bun run release` path. This +# workflow used to carry its own hardcoded list of 13 directories, which drifted from +# that derivation: @metaobjectsdev/docs-site is a runtime dependency of +# @metaobjectsdev/cli but was never in the list, so a release cut through here would +# have shipped a cli pinning a docs-site version nobody published — an uninstallable +# tarball, discoverable only by an external `npm install`. A list that is derived +# cannot drift from the derivation. +# # This publishes the versions ALREADY COMMITTED in each package.json at the tagged # commit — bump + commit them first (locally: `bun run release ` does the # bump/build/verify/publish in one shot WITH a confirm gate; this workflow is the @@ -38,6 +47,9 @@ jobs: bun install --frozen-lockfile bun run build + - name: Derive + verify the publish set + run: node scripts/publish-set.mjs --check + - name: Pack-verify the cli tarball pins siblings (no workspace:*) run: | set -euo pipefail @@ -60,15 +72,15 @@ jobs: run: | set -euo pipefail echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + trap 'rm -f ~/.npmrc' EXIT pub() { ( cd "$1" && bun publish --access public ) && echo " ✓ $1"; } - # tier 0 → 4 (deps before dependents) - for d in server/typescript/packages/metadata server/typescript/packages/render \ - server/typescript/packages/codegen-ts server/typescript/packages/runtime-ts \ - server/typescript/packages/migrate-ts server/typescript/packages/sdk \ - client/web/packages/runtime-web \ - server/typescript/packages/codegen-ts-react server/typescript/packages/codegen-ts-tanstack \ - client/web/packages/react client/web/packages/tanstack \ - server/typescript/packages/cli server/typescript/packages/ai-runtime ; do + # The set and its tier order (deps before dependents) come from the shared + # derivation, never from a list maintained here. Written to a file first so a + # non-zero exit fails the step instead of yielding a silently short list. + node scripts/publish-set.mjs > "${RUNNER_TEMP}/publish-set.txt" + mapfile -t DIRS < "${RUNNER_TEMP}/publish-set.txt" + [ "${#DIRS[@]}" -gt 0 ] || { echo "::error::derived publish set is empty"; exit 1; } + echo "publishing ${#DIRS[@]} packages in tier order" + for d in "${DIRS[@]}"; do pub "$d" done - rm -f ~/.npmrc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b16e43fe1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,592 @@ +# MetaObjects — Claude Context + +## What this project is + +MetaObjects is a **cross-language metadata standard** for declaring typed entity models that drive code generation, runtime metadata access, and drift detection — across TypeScript, C#, Java, Python, and Kotlin (Kotlin runs on the JVM via `metadata-ktx` + `codegen-kotlin`). + +The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, generated code is idiomatic per-language output that runs without any MetaObjects dependency at runtime. If `@metaobjectsdev/*` disappears tomorrow, you keep working code. + +## Four pillars + +Equal weight — all four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior: + +1. **Codegen** — emit idiomatic per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via `codegen-spring`, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin via `codegen-kotlin`). Hand-edit-preserving regen via three-way merge. +2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin), EF Core (C#). +3. **Drift detection** — `meta verify` catches divergence between code and metadata (covers entity codegen, prompt templates, output parsers, schema). Quality-of-life on top of codegen + runtime. +4. **Prompt construction** — a prompt is code, not a string scattered across services. Declare a prompt's payload as a typed projection (payload bloat becomes a diff), keep its text external and provider-resolved, and render it deterministically: snapshot-testable, cache-stable (no whitespace change silently breaking exact-prefix prompt-cache hits), and drift-checked at build time so a renamed field can't degrade a prompt. Conformance-gated, so the guarantee holds in every language port. **Render + payload-VO codegen + `verify` + `template.output` parser-on-receipt (FR-006) + the output-format prompt fragment & tolerant `extract` parser (FR-010) ship in all five ports today** — the library-side building blocks of the pillar are complete. The one remaining library-side piece is MCP exposure of declared prompts/tools (see `spec/roadmap.md`); the application-level consolidation (eval harness, end-to-end declared-prompt orchestration) and consumer adoption are exercised in adopter projects, not in this library repo. Designed in `docs/superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md`. + +## Status + +_Last refreshed 2026-08-16._ + +**TypeScript reference implementation** is **published to npm at `0.23.2`** (14 `@metaobjectsdev/*` publish candidates on the `latest` tag, full lockstep; the two `angular` packages are versioned on their own `0.6.x` line and are **NOT published to npm — source-only by decision** (ADR-0048): they build in-repo, and stay off the registry until they meet the ADR's promotion bar). C# at `0.23.2` (NuGet); Python at `0.23.2` (PyPI); Java / Kotlin at `7.23.2` (Maven Central). **All four registries share the same `minor.patch`, and stay locked in lockstep going forward** — the `0.20.15` cut moved Maven `7.20.12`→`7.20.15` to close the last per-registry drift. + +**The `0.23.2` line is a coordinated PATCH across all four registries** (npm `0.23.2` · PyPI `0.23.2` · NuGet `0.23.2` · Maven `7.23.2`) — **int-backed `field.enum` storage, and the versioning rule that says why this is a PATCH.** The rule change is the durable half: the old policy made ANY registry addition a MINOR, which had spent `0.22.0` and `0.23.0` on changes a project could not observe at all. `expected-registry.json` is an **internal** gate — five ports byte-matching one manifest is how the ports are kept from drifting — and it says nothing about whether an adopter's project changes. Vocabulary now sorts by consumer impact: **attribute ⇒ PATCH, top-level type ⇒ MINOR, subtype ⇒ PATCH when INERT** (nothing but authoring it can reach it; MINOR when it narrows something previously permitted, changes what existing metadata means, or headlines a release on purpose). The caret rule is not to be inverted — "`^0.22.x` resolves `<0.23.0`, so a MINOR is adopted deliberately" is a reason to CHOOSE minor when that gate is wanted, not a reason additive vocabulary must be minor. ADR-0035 Amendment 1; operative table in `docs/RELEASING.md`. **The feature:** `@intValueMap` (a `{memberSymbol: int}` map on `field.enum`) switches the column from `varchar` + string `CHECK` to `integer` + int `CHECK` while the wire format, the generated enum type and every runtime return value stay the member SYMBOL — the provenance is an integer-coded column an adopter already has, and it is a MAP rather than a positional array so reordering `@values` cannot silently re-map every member. Persistence ships in all five ports; **TypeScript needed TWO codecs** (a Drizzle `customType` for generated code, `ObjectManager` read/write/filter coercion for the metadata-driven runtime) and shipped only the first until the corpus caught it — generated code worked while `om.create()` bound the symbol into an integer column. **Two rulings close ways it could have shipped half-true:** `@isArray` + `@intValueMap` is now a LOAD ERROR (`ERR_ENUM_INT_VALUE_MAP_ARRAY`) because the original design assumed the element codec fell out of the scalar one and it does not — Python bound the symbol LIST into an `integer[]`, Java and Kotlin emitted a scalar codec, TS's sqlite branch stored symbols as JSON text, and **two ports composing while four silently get it wrong is the `field.byte`/`short`/`class` mistake**; and a stored integer mapping to no member now THROWS in all five (it previously behaved four different ways — Java surfaced `"7"`, Python the raw int, C# fell through to the LAST member, handing back `ARCHIVED` for a row that is not). Also: the filter-operator band is decided per FIELD not per subtype (an int-backed enum drops `like`), an FK into a table whose key carries `@column` stopped phantom-diffing forever, views in a table-less schema are no longer excluded from the migrate diff entirely, and a chained abstract `field.enum` is named for its OWN declaration in Kotlin. See `CHANGELOG.md` [0.23.2]. + +**The `0.23.1` line is a coordinated PATCH across all four registries** (npm `0.23.1` · PyPI `0.23.1` · NuGet `0.23.1` · Maven `7.23.1`) — every registry carries a real changed product file, so none is a version-parity bump. **The theme is a check that was confidently wrong:** not one of these is a missing feature; each is a guarantee the toolchain already made and quietly failed to keep. (1) **`{{#hasField}}` rendered as absent on a populated payload, in every port.** A prompt's conditional section is a DERIVED boolean over a declared field; the JVM has emitted `has()` onto generated payload records since 7.7.7 and accepted the section in `verify` — **no render engine implemented the other half**, so given the same payload *data* (a map, which is what the runtime and the corpus actually pass) all five ports dropped the block's contents silently, with no error. The JVM looked correct only because a generated RECORD answers `hasFoo()` by its own method, so the same payload rendered differently depending on whether it arrived as a record or as a map — the byte-identical-rendering promise failing quietly. `PayloadAccessors` now exists in all five ports with one naming rule and the JVM emitter's presence semantics (string → non-blank, collection → non-empty, reference → non-null, **number/boolean → no accessor**, since `{{#hasCount}}` over an int is drift rather than a conditional); an AUTHORED `hasFoo` always wins. Found by an adopter whose Node gate reported **157** `ERR_VAR_NOT_ON_PAYLOAD` against a JVM gate reporting none — now 0 on both. (2) **`@provided` flowed down an `extends` chain in TS/C#/Python** — it is a provenance fact about the declaration ITSELF, like `abstract`, so it must not be inherited; the resolving ports emitted a reference to a hand-written type **the adopter never declared**. ADR-0039 amended (`@provided` is chartered as the second deliberately own-only attribute; the member set it accompanies stays RESOLVING). (3) **`@verifiedBy` stopped deciding what a test file is** — its closed pattern list was a guess about someone else's repository and was wrong on Maven Failsafe (`*IT`) from the day it shipped, convicting a correct project with `ERR_REQUIREMENT_TEST_MISSING`; Failsafe's defaults are now built in, `verify.testFiles` lets a project declare its own, and an unrecognised convention WARNs instead of convicting. (4) **A requirement can now claim a `template.prompt`** — L4 means "a declared top-level model node", so the estate with the most to gain from a status (a retired prompt leaves nothing behind) can finally carry one. (5) **`verify` gates the committed schema snapshot** ([#292](https://github.com/metaobjectsdev/metaobjects/issues/292)) — that file decides what DDL the next migration contains and nothing checked it; the check is conditioned on metadata==DB, which is what makes it false-positive-free. (6) **Codegen and migrate named the same CHECK constraint two different ways** ([#293](https://github.com/metaobjectsdev/metaobjects/issues/293)) — codegen changed, not migrate, because migrate's suffix names are already in live databases. **The durable lesson is the shape they share: each survived because the thing that would have caught it did not exist** — the render corpus had NO fixture using a derived accessor at all, nothing ever compared codegen's constraint name to migrate's, and no gate read the snapshot. Every fix ships with the missing check, not just the corrected behaviour. See `CHANGELOG.md` [0.23.1]. + +**The `0.23.0` line is a coordinated MINOR across all four registries** (npm `0.23.0` · PyPI `0.23.0` · NuGet `0.23.0` · Maven `7.23.0`) — **the requirement vocabulary gains a second axis: what was DECIDED about a gap.** Cut as MINOR because it adds registered vocabulary and pre-1.0 `^0.22.x` resolves `<0.23.0`, so a consumer adopts it deliberately; everything in it is additive, and a project declaring no `requirement.*` nodes sees no change at all. `@status: partial` said there IS a gap but never what anyone decided about it, so a known-and-tolerated gap and a gap nobody had looked at were the same value. Three additions close that, on **both** subtypes in all five ports: **`@status: planned`** (intended, not built — its `@implementedBy` may dangle, it is exempt from the architectural universality check, and it **never counts toward object coverage**, because otherwise the cheapest way to clear an unclaimed-entity warning would be to declare an intention and the gate would measure ambition rather than work); **`@disposition`** (`accepted | deferred`, where **absent means UNDECIDED** — folding this into `@status` would make "there is a gap" and "we chose to live with it" the same fact); and **`@trackedBy`** (issue references, free-form and deliberately **never resolved** — `verify` has no network, and which sprint/who owns it lives in the tracker because two systems holding that answer will drift). Two new warnings: a `@disposition` on a status with no outstanding work, and `deferred` naming no ticket. **`meta verify` now prints a requirements summary on EVERY run** — a gate that says nothing when it passes cannot be told apart from a gate that checked nothing. **Architectural requirements can now nest**: `functional` declared a `requirement.*` child rule and `architectural` declared none, so an architectural node could nest under a *functional* parent but never under another architectural one, making a quality taxonomy inexpressible; **`@level` is now OPTIONAL on `architectural`** (absent keeps the flat object-independent form, so every existing ledger stays valid; present opts into a tree and the same nesting + link-floor rules apply). The levelling guidance is corrected everywhere it appears: **L1–L3 are levels of abstraction and ownership in the problem domain, never a directory, package, deployable or module** — if a behaviour-preserving refactor would force a node to move, its level is wrong. Separately, **`description` and `notes` are re-split by CONTENT KIND, not audience** (`description` = what the element is and covers, derivable from the model; `notes` = what you had to look outside the model to learn — *a sentence belongs in `notes` exactly when it would have to change because the IMPLEMENTATION changed while the model did not*): the old audience split made writing the same thing twice the honest way to fill both, and dogfooding put a number on it — **overlap on 72 of 245 entries**, the same overlap every time. **The cut also fixed a `main` that had been RED on four of five ports** since the vocabulary landed in TypeScript and in the byte-gated `expected-registry.json` without the port registrations — nobody ran the gate, because those lanes are release-tag/dispatch-only and `scripts/ci-local.sh` piped through `tail` reports the exit status of `tail`. See `CHANGELOG.md` [0.23.0]. + +**The `0.22.1` line is a coordinated PATCH across all four registries** (npm `0.22.1` · PyPI `0.22.1` · NuGet `0.22.1` · Maven `7.22.1`) completing 0.22.0's assigned-primary-key fix in the ports that shared its semantic half. **npm and NuGet are version-parity bumps** — TS shipped the fix in 0.22.0 and C# never had the defect. An entity whose PK carries no `identity.primary @generation` — a natural key, or an id issued upstream — had its create shape read the key's optionality off `@required` like any other field, so a key not marked `@required` was **optional in the generated create/validation artifact** and a create body carrying no primary key at all was accepted, to fail at the database. Now required (with no `@default`) in Python's `Create`, Java's `Dto` (`@NotNull`) and Kotlin's data class (a non-null property, so the body cannot bind); an `increment`/`uuid` key still stays optional-or-omitted and a `@default`-bearing key stays optional. **C# was already correct** (`CSharpNaming.IsRequired` treats any PK field as required) — it was initially mis-reported as affected by reading `RequiredCreateKeys` in the ROUTES generator, which is the raw-JSON presence check, a different tier with a different answer; the corpus settled it. **The durable lesson is the corpus's blindness, not the bug:** `validation-conformance`'s `Account` has a `@generation: increment` key that every port drops from the create shape entirely, so no existing case could express the opposite — and more generally **every model in this repo generates its primary keys**, which is why a defect in four of five ports survived every gate until the 0.22.0 external smoke test authored a model the repo does not contain. A second entity `Ledger` (assigned **string** key — a value-typed key cannot express "absent" vs "default" in a DataAnnotations-style artifact, so that case belongs to `api-contract-conformance`) plus an optional per-case `entity` key now gates it in all five runners. See `CHANGELOG.md` [0.22.1]. + +**The `0.22.0` line is a coordinated MINOR across all four registries** (npm `0.22.0` · PyPI `0.22.0` · NuGet `0.22.0` · Maven `7.22.0`) — **a new registered type family plus a registry tightening**, cut as MINOR because pre-1.0 `^0.21.x` resolves `<0.22.0`, so a consumer adopts it deliberately. (1) **Capability requirements are metadata**: `requirement.functional` / `requirement.architectural` are registered vocabulary in all five ports, declared in `metaobjects/` beside the entities they describe and loaded by the same loader — no side file, no bespoke parser. The two kinds carry **opposite checks** (`functional` fails when NOTHING implements it — existence; `architectural` fails when something VIOLATES it — universality, hence no level: a policy is object-independent by definition). **Hierarchy is nesting**, not a `parent` string, so regrouping moves a subtree and a requirement is addressable by the same dotted child-name path as every other node. Five levels with the `@implementedBy` link floor at **L4** (`ERR_REQUIREMENT_LINK_ABOVE_FLOOR`); `@status` is a closed enum the **LOADER** enforces (`live | partial | abandoned | superseded`) so a typo fails the load in every language rather than passing silently in four of them. `meta verify` owns what the loader cannot: **a dangling `@implementedBy` is an ERROR on `live`/`partial` and CORRECT on `abandoned`/`superseded`** — that asymmetry is the mechanism's whole point (those nodes are *supposed* to be gone; "fixing" it by deleting the entry destroys the record). `@verifiedBy` checks each named test exists and is not skipped, never runs them, and fails OPEN when no test files are visible. Object coverage ships as a **WARNING** deliberately — on a real 120-file repo carrying one requirement it reports 93 unclaimed entities, so promoting it to error would fail a project's first `verify` after authoring a single entry. Entirely opt-in: no `requirement.*` nodes ⇒ no diagnostics, and no codegen/migrate/runtime path reads the type. Adopter view: [`docs/features/requirements.md`](docs/features/requirements.md). (2) **`template.*` owns its attributes** (all five ports) — FR-033 had re-homed them, **including required `@payloadRef`/`@toolName`**, into the composable `metaobjects-prompt` concern provider, so composing without it left `template.prompt` registered with ZERO attributes and `ERR_MISSING_REQUIRED_ATTR` simply stopped firing: invalid metadata began loading clean. → **ADR-0050** (own vs projected attributes: OWN attrs travel with the type, PROJECTED attrs are another concern applied to someone else's complete type and must be optional). (3) **`ERR_EXTEND_REQUIRED_ATTR`** — projecting a REQUIRED attr onto a type another provider owns now throws in every registry, across **two** doors (`extend` and `registerCommonAttrs`); **potentially breaking for a downstream provider** (a sweep of all 54 spec files here found zero). (4) **12 more Java types reject undeclared attributes** (`object.*`, `source.*`, all three `identity.*`, `relationship.*`, `validator.*`, `layout.*`, `origin.*`, `index.lookup`) → **ADR-0051** (extension IS registration; ADR-0011 and ADR-0023 were never in conflict — a wildcard is worse than a permissive extension point because it also swallows a **typo'd core attribute**, silently, on one port only). `template.*`'s Java wildcard is the one deliberately RETAINED: removing it trips `ERR_BAD_ATTR_VALUE` on `abstract: true`, because Java models `isAbstract` as an ATTR while TS models it as a NATIVE FIELD — closing it needs `isAbstract` homed consistently, a metamodel decision. Two defects were caught by the release's own external smoke test and fixed before publish: an **assigned primary key** (no `identity.primary @generation`) emitted an InsertSchema marking the PK `.optional()` while its Drizzle column `text("id").primaryKey()` has no default and is insert-required, so generated code **did not compile** (TS2769) — now gated by a test compiling the entity AND queries files together, since the defect lived *between* them; and `cli` carried a **dead `yaml` runtime dependency** orphaned when the ledger became registered vocabulary. See `CHANGELOG.md` [0.22.0]. + +**The `0.21.6` line is a coordinated PATCH across all four registries** (npm `0.21.6` · PyPI `0.21.6` · NuGet `0.21.6` · Maven `7.21.6`). **Two changes alter runtime behaviour on an existing database** — they lead the changelog deliberately, because a PATCH is auto-adopted on `npm update`. (1) **`like` is now case-SENSITIVE on Postgres** (it dispatched `ILIKE`): FR-009 always specified SQL `LIKE`, FR-009 §7 puts `ilike` explicitly out of scope, and TS's own Kysely/Drizzle/in-memory drivers were ALREADY case-sensitive — only one branch of one HTTP parser disagreed, carrying a comment that wrongly claimed sqlite LIKE is case-sensitive. So a query returning extra rows before returns fewer now. TS lowers sqlite `like` to `GLOB` (SQLite's native LIKE folds ASCII case and `PRAGMA case_sensitive_like` is connection-global on a consumer-owned connection); C#'s conformance adapter, which reflected Npgsql `ILike` while the product shipped `EF.Functions.Like`, now gates what the product ships. Both corpora are **de-blinded** with case-mismatched probes — the persistence fixture's comment that its data was case-aligned "so the test passes whether a port wires LIKE or ILIKE" is deleted. `?search` stays deliberately insensitive; `ilike` remains unadded (ADR-0049). (2) **The first `meta migrate` after upgrading emits a migration ADDING `ON DELETE` actions to live foreign keys** — a parent-side `relationship.composition` was silently contributing no referential action, so the documented "composition ⇒ cascade" never fired and deleting a parent 500'd. Correlation is now package-aware at both tiers with `@through` excluded; `@onDelete`/`@onUpdate` are **registered on `identity.reference`** in all five ports (ADR-0047 — the ADR-0023 justification is that a reference-only FK and an M:N junction's FKs correlate with no relationship *by construction*). Pin `@onDelete: "no-action"` to keep current DB behaviour. Both are corrections of previously-wrong behaviour rather than contract changes, which is why this is a PATCH — the same call, on the same defect class, as the Java `LIKE` fix in 0.21.4. Also: the **ts-poet split-tree duplicate-import fix** (with a globally-installed or linked CLI, two physical ts-poet copies made `instanceof Code` fail across module instances, so each section stringified with its own import header — `npx tsc` reported TS2300; fixed at three layers, including a CLI alias that repairs existing scaffolds without re-scaffolding); **four adopter-audit blind-spot closures** — the persistence corpus could not see a port ignoring `sort`/`limit`/`offset` (all 24 fixtures sorted `id asc` over ascending-id seeds) or `in` combined with a second predicate, **55** vacuous `rejects.toThrow()` assertions (the sweep unmasked a real `BlockedChangesError` hint bug), the documented `like: "%@example.com"` example that always 400s, and nullable projection PKs; and **the Angular tier is source-only by decision** (ADR-0048) with a 6-point promotion bar, after an assessment found its grid's cell-renderer registry decorative and its codegen missing the endpoint guards and descriptor split the TanStack tier has. **ADR numbering:** three parallel agents each wrote `ADR-0047`; referential-actions KEEPS it because that string is baked into byte-gated registry attr descriptions, and the two documentation-only decisions renumbered to 0048/0049. See `CHANGELOG.md` [0.22.0]. + +**The `0.21.5` line is a coordinated PATCH across all four registries** (npm `0.21.5` · PyPI `0.21.5` · NuGet `0.21.5` · Maven `7.21.5`) — ten fixes; PyPI/NuGet/Maven are version-parity bumps. The theme is **a package promising a compatibility it never had.** (1) **Every peer range was unbounded `>=`.** `@tanstack/react-table: ">=8.20.0"` admitted **v9** — a rewrite that deleted `useReactTable`/`getCoreRowModel`, both imported by `entity-grid.tsx` — so a fresh install produced an unbundlable package with **no peer warning**; an adopting project hit it and pinned `^8.21.3` by hand. All ranges bounded (`drizzle-orm` was next: `1.0.0-rc.4` is published), gated by `scripts/check-peer-ranges.ts` — a range is unbounded exactly when it accepts `9999.0.0`. That gate reads **manifests** because a peer range is only exercised when a fresh resolver walks the registry, and this workspace pins its devDeps and freezes them in `bun.lock`. Also corrected `peerDependenciesMeta.optional`: **`react` was marked optional** on both React runtime packages while every entry hard-fails without it, suppressing the install warning that would have said so. (2) **`react-easy-crop` broke every non-Vite build** — an *optional* peer reachable from the package ROOT is fatal to webpack/Next/esbuild/Bun (bundlers resolve the whole graph before tree-shaking), so *any* consumer of generated forms failed; now a real dependency, still lazy-loaded. The cleaner `/image` subpath split is deferred to a MINOR — and the reusable lesson is that **a MINOR cannot reach the adopters a bug has already broken**, since `^0.21.x` will not resolve `0.22.0`. (3) **Instance artifacts for objects with no endpoint** — four generators emitted hooks/grids/forms for `object.value`, sourceless entities and sourceless projections; that output **never compiled** (TS2693 + TS2305 ×3), so there was nothing to regress. Fixed in the central guards via the routes tier's own #248 source predicate, deliberately NOT a subtype check. (4) **`meta init` left a project whose first `tsc` produced 94 errors** — no `"type": "module"` (npm writes `commonjs`; TS7 enables `verbatimModuleSyntax`) and no declaration of the ADR-0034 scaffold's own deps → **0**. (5) **Hono TPH routes returned and mutated OTHER subtypes' rows** (now fails closed — wrong data is worse than a missing endpoint) and **Hono write-through ignored its replica read view** (fixed via `readView`). (6) **`db.all(sql.raw(...))` 500'd on Postgres in BOTH read-only mounts** — `.all()` on the top-level db HANDLE, which `PgDatabase` lacks; needed a real dialect dispatch, and Fastify was broken here too despite being the reference for the *other* `.all()` shape. (7) **The UI tier asked a storage question** — gating on `hasAnyRdbSource` rather than "is there an endpoint?"; now one predicate (`servesReadApi`/`servesWriteApi`), the durable gate being that **no UI generator may name a storage predicate at all**. (8) **Drizzle out of browser bundles: 716 KB → 215 KB** via an additive `.meta.ts` carrying just the descriptor (`/* @__PURE__ */` was tried and does not help). (9) The **Angular packages were documented as published but 404** — and the mechanism is gated now (`check-publish-intent.sh`): a non-private package on its OWN version line matches neither branch of the lockstep rule, so it is skipped by every release, silently and forever. **Known and NOT fixed** (listed in `CHANGELOG.md`): the scaffolded queries generator emits `import { eq }` three times so `npx tsc` reports TS2300 on a fresh project; cross-port `like` semantics contradict each other and the corpus is case-aligned *by construction* so it cannot see it (since RULED + fixed on main post-0.21.5: `like` is case-SENSITIVE SQL LIKE uniformly, ADR-0049 — TS's Postgres ILIKE dispatch and C#'s adapter ILike were the defects, the corpora are de-blinded with case-mismatched probes, and TS lowers sqlite `like` to GLOB to stay case-sensitive); parent-side `relationship.composition` loses its cascade while its `@onDelete` escape hatch is unregistered, so a model that migrates cleanly fails strict `verify`. See `CHANGELOG.md` [0.21.5]. + +**The `0.21.4` line is a coordinated PATCH across all four registries** (npm `0.21.4` · PyPI `0.21.4` · NuGet `0.21.4` · Maven `7.21.4`) — ten fixes, most adopter-reported; PyPI and NuGet publish as **version-parity bumps** (no changed product file). Two were hard blockers: **[#287](https://github.com/metaobjectsdev/metaobjects/issues/287)** made the browser packages unbundleable outright (a single *value* import from `@metaobjectsdev/metadata`'s root barrel drags `MetaDataLoader` → `library/library-sources.ts` → `node:url` into the graph, and `runtime-web` had one — so **no client consuming the generated hooks could build**; fixed by a new pure-constants **`@metaobjectsdev/metadata/constants`** subpath that browser packages import values from, types still coming from the root since `import type` is erased), and **every sqlite table-rebuild migration was un-appliable by the tool that emits it** (the emitted script carries its own `BEGIN TRANSACTION` while the apply runner already owns one; the runner now strips transaction control and rewrites `PRAGMA foreign_keys = OFF` → `defer_foreign_keys = ON`, since the former is a no-op inside a transaction). Also: **[#286](https://github.com/metaobjectsdev/metaobjects/issues/286)** Hono CRUD 500ing on Postgres (`.all()`/`.get()` are libsql-only), **`meta gen --dry-run` actually previewing** (it had been writing every file), **[#285](https://github.com/metaobjectsdev/metaobjects/issues/285)** an un-appliable `DROP INDEX` for a constraint-backed index, a `metaobjects.config.ts` load permanently corrupting `Error.prepareStackTrace` for the rest of the process, **three silent-wrong-rows defects in the Java query lowering** (no case-sensitive `LIKE`; `in` hand-composed as an unparenthesized OR-chain, so `in` + any second predicate regrouped as `a=1 OR (a=2 AND b=3)`; `Range` documented 0-indexed while the drivers emit `OFFSET start-1`), and the **grid-discoverability half of #287** — `tanstackGrid()`/`tanstackGridHook()` emit only for an entity declaring a `layout.dataGrid`, which is intended but was documented nowhere, so each generator now emits one self-extinguishing `meta gen` warning naming the objects and the metadata that enables them (plus a TPH fix: the grid-hook filter lacked its sibling's TPH clause, emitting a `.grid.ts` whose `.columns.tsx` never exists). **The durable lesson of the cut: six of the ten were invisible to a gate that existed for them** — a bundle checked only under a test runner that never bundles; sqlite SQL proven statement-by-statement but never through the tool that applies it; a conformance fixture whose seed data is case-aligned "so the test passes whether a port wires `LIKE` or `ILIKE`"; and a CI lane (`ts-unit`) that ran the browser suites **without ever building them**, so the gate failed its own precondition on every clean checkout while passing on any warm developer box. That lane now builds what it gates and runs the **seven server packages that were gated by nothing at all** (`codegen-ts-{tanstack,react,angular}`, `sdk`, `ai-runtime`, `conformance`, `docs-site`). See `CHANGELOG.md` [0.21.4]. + +**The `0.21.0` line is a coordinated MINOR across all four registries — the pre-1.0 breaking slot** (npm `0.21.0` · PyPI `0.21.0` · NuGet `0.21.0` · Maven `7.21.0`), carrying **three** changes that make previously-valid metadata fail to load. **Migration guide:** [`docs/features/migrations/value-assembly-origins-and-source-role-shrink.md`](docs/features/migrations/value-assembly-origins-and-source-role-shrink.md). (1) **[#210](https://github.com/metaobjectsdev/metaobjects/issues/210) — assembly origins leave `object.value`.** `origin.aggregate`/`computed`/`collection`/`first` on a value-hosted field now fail with `ERR_SUBTYPE_RULE_VIOLATION`. **`origin.passthrough` stays legal on a value** — there it is FR-015 *parameter lineage*, not assembly (the loaders already drew that line via the FR-024 B5 value-host exemption), and retiring it would have silently dropped the `ERR_PASSTHROUGH_TYPE_MISMATCH` check on stored-proc arguments. The durable rule: **"passthrough on a value is lineage; assembly origins live on projections."** The migration path is **additive** — `@payloadRef`/`@responseRef` now accept a **sourceless `object.projection`**, so a payload that was assembling values re-hosts as a projection and keeps its origins. Implemented as one named `ASSEMBLY_ORIGIN_SUBTYPES` set hoisted above the origin dispatch in every loader, so cross-port coverage is a property of the constant rather than of four separate branches. (2) **[#212](https://github.com/metaobjectsdev/metaobjects/issues/212) — `source.rdb @role` shrinks to `primary | replica`**; `index`/`cache`/`publish`/`mirror` become **reserved-not-registered** (ADR-0040 treatment), a legacy use failing `ERR_BAD_ATTR_VALUE`. Justification: **no port ever built the role-routing dispatch these anticipated** — every read of `@role` in all five ports is an equality test against `primary`, so the consumed information was one bit; an adopter scan found zero uses. The re-entry bar is in ADR-0007 Amendment 2: *a role member enters the registry only when a shipping consumer dispatches on it.* (3) **A payload's nested `field.object @objectRef` must target an `object.value`** — TS, C# and Python previously accepted a non-value target *and emitted code from it*; now a fail-closed **loader** rule (one rule, four loaders) rather than five per-port codegen filters. Also: `runner.ts`'s `dbImport`/`dialect` guard now keys on `hasAnyRdbSource` instead of an object-subtype test — the last subtype-based persistability check in the codebase, and a false positive for the sourceless projections this release recommends. Cut as MINOR, not PATCH, because pre-1.0 `^0.20.x` resolves `<0.21.0`: a patch would be auto-adopted on a routine `npm update` and break adopters with no deliberate action. See `CHANGELOG.md` [0.21.0]. + +**The `0.20.16` line is a coordinated PATCH across all four registries** (npm `0.20.16` · PyPI `0.20.16` · NuGet `0.20.16` · Maven `7.20.16`) shipping **[#270](https://github.com/metaobjectsdev/metaobjects/issues/270)** — **a prompt payload's field types are declared-authoritative, never origin-derived.** The prompt pillar's contract is that a payload is a typed projection the author *declares*, so payload bloat shows up as a diff; the **Kotlin, Python and Java** payload-VO generators broke it by typing a payload field from its `origin.*` child instead. Worst arm: `origin.collection` **discarded the field's declared `@objectRef`** and substituted the `@via` relationship's target entity, so a declared *curated* value-object silently became the **full entity** — invisible in a diff, because the metadata still read as curated. `@agg count` also hardwired a long type over the declared subtype, and `computed`/`first` forced nullability the declaration never asked for. All three ports now type **only** from declared `field.` + `@isArray` + `@objectRef`, never derive nullability from origin semantics, and walk the nested-payload closure **only** over declared `field.object @objectRef` edges; the `origin.collection` edge is deleted in lockstep from the ADR-0044 name-map closure each port shares with its extract tier (#228). **TypeScript and C# were already origin-blind and are the reference** — their product code is untouched (npm + NuGet are version-parity bumps), but each gained a regression pin, since both were previously ungated and that is exactly how the divergence survived long enough to be written into a ruling as settled fact. The issue as filed named only Kotlin and Python; **Java was found to carry the identical bug during pre-merge review** — it matters because #210 makes payloads `object.projection`s and projections legitimately carry assembly origins, so an origin-dispatching emitter would fire on the shape about to become the norm. Adopter-visible: a Java payload component declaring a plain scalar array changes from `T` to `java.util.List` (array-ness was being dropped), and Kotlin's `origin.first`/`origin.computed` properties are no longer nullable. Also fixes two raw `NUL` bytes embedded in Java string literals (composite map-key delimiters written as literal `0x00` instead of the `\0` escape) — runtime-identical, but they made the file test as *binary*, so binary-skipping search tools silently ignored it, which is why Java was mis-recorded as origin-blind in the first place. Closes the long-standing `codegen-spring` "payload `origin.*` resolution" open question as **moot**. See `CHANGELOG.md` [0.20.16]. + +**The `0.20.15` line is a coordinated PATCH across all four registries** (npm `0.20.15` · PyPI `0.20.15` · NuGet `0.20.15` · Maven `7.20.15`), coordinated because **`WARN_ENUM_NORMALIZE_AMBIGUOUS`** is a cross-port loader change: a `field.enum` whose `@values` contains a member equal to the concatenation of two or more others is ambiguous under the default `@normalize: strip` (which erases separators), so a delimited value silently coerces to a wrong-but-valid member and is reported EXTRACTED rather than MALFORMED. All four loaders now warn at declaration time; `collapse` is immune and is the documented fix. npm additionally ships **#192** (the ADR-0015 Flyway output adapter — `meta migrate --migration-format flyway` emits `V__`/`U__` for a Flyway runner, restoring the metadata→migration path JVM/Flyway consumers lost when the `meta:migrate --flyway` mojo was removed; apply stays Flyway's job, so `--apply`/`apply-pending`/`--rollback` are refused), **#266** (byte-identical duplicate emissions collapse instead of failing the build, so a shared `enums.ts` no longer breaks a multi-`entityFile()` config), **#194** (non-`object` reference resolution + optional `dbImport`/`dialect`) and **#232** (gen-state engine-version stamp); Maven additionally ships the `metaobjects:verify` per-unique-`outputDir` fix. See `CHANGELOG.md` [0.20.15]. **The `7.20.12` line is a Maven-only PATCH** (Maven Central only; npm/PyPI/NuGet stay at `0.20.11` — the fix is Java-only) shipping **[#233](https://github.com/metaobjectsdev/metaobjects/issues/233)**: a multi-module Maven reactor building `metaobjects-maven-plugin` in **parallel** (`mvn -T`) deadlocked (serial always worked). A new `RegistryBootstrap.warmUpDefaults()` deterministically warms the independently-locked process-global registry singletons (`defaultLoaderRegistry` / `getInstance` via `ConstraintEnforcer` / `ServiceRegistryFactory`) on one thread before any parallel first-init can race their locks (called from `MetaDataLoader.initWithConcurrencyProtection` + the mojo `execute()`s); `MetaDataLoader.buildLoaderKey()` gains a process-unique instance id so two reactor modules sharing a `` name no longer collide on the static `activeLoaders` dedup (module B was getting module A's loader, leaving its own tree UNINITIALIZED); and `generate`/`verify`/`docs` are marked `@Mojo(threadSafe = true)` (honest labeling — Maven 3.x only warns, it does not serialize non-threadSafe mojos under `-T`). Existing single-module `metaobjects:generate` output is byte-identical; verified with a 3-module `-T4` reactor before/after. See `CHANGELOG.md` [7.20.12]. **The `0.20.11` line is a coordinated PATCH across all four registries** (npm `0.20.11` · PyPI `0.20.11` · NuGet `0.20.11` · Maven `7.20.11`) — the coordinated cut also **re-baselines the version numbers** so all four registries share `20.11` (PyPI `0.19.9`→`0.20.11`, NuGet `0.19.7`→`0.20.11`, Maven `7.11.7`→`7.20.11`; going forward coordinated releases keep the shared minor in lockstep and npm may run a patch ahead between cuts). It ships **#246** (shared `field.enum` across packages — the Kotlin table generator now emits the cross-package shared-enum import, and a new cross-port loader error `ERR_ENUM_EXTENDS_VALUES_CONFLICT` rejects a `field.enum` that both `extends` a shared package-level abstract enum and declares its own `@values`; the loader change lands in all five ports, the Kotlin codegen on Maven), its sibling **#259** (a `field.enum` inheriting `@values` through two `extends` hops — projection→entity→shared — now materializes its own per-projection enum instead of generating nothing; Kotlin codegen), and an **npm-only** **#258** (`meta migrate` refuses a primary-key move with a clear error — detect-and-refuse — instead of silently emitting an un-appliable migration that drops the PK and breaks referencing FKs). See `CHANGELOG.md` [0.20.11]. **The prior `0.20.10` line was a coordinated PATCH across all four registries** (npm `0.20.10` · PyPI `0.19.9` · Maven `7.11.7` · NuGet `0.19.7`) — **#228** (all five ports, the reason for the coordinated release) fixes the extract/output-parser tier and build-time `@payloadRef`/`@responseRef` resolution under a cross-package payload-name collision: ADR-0044 (#219/#220) gave every port's payload-record emitter collision-scoped naming, but the extract tier (the `template.output`/`template.toolcall` parser that reads a rendered payload back off an LLM response) still named/imported nested value-object classes by bare short name — latent, since the only shipped collision fixture used `@format: html`, which extract never runs against — and several build-time `@payloadRef`/`@responseRef` resolvers (feeding extract, the render-helper/output-prompt generators, and the JVM `metaobjects:verify` template-drift check) resolved a bare ref package-blind against the loader's package-local ADR-0042 contract, while TS/Python/C#'s generated output-parser resolved its own `@payloadRef` via a bare runtime lookup; all now resolve package-locally / bake the FQN, byte-identical absent a genuine cross-package bare-name collision. Bundled with two **npm-only** fixes (`migrate-ts` + `codegen-ts`; PyPI / NuGet / Maven Central unchanged — schema/DDL is TS-owned, ADR-0015): **#248** makes persistability derive from a declared/inherited `source.*` child (the loader's own already-published contract) instead of a hardcoded object-subtype check — a sourceless custom `object` subtype no longer gets a phantom `CREATE TABLE` / FK-target eligibility in `migrate-ts`, nor broken queries/routes in `codegen-ts`; and **#255** reorders `migrate-ts`'s SQL emitter to drop constraints/indexes (`drop-fk`/`drop-check`/`drop-index`) ahead of `DROP COLUMN`, fixing an un-appliable migration when dropping a column a still-present foreign key or index referenced. See `CHANGELOG.md` [0.20.10]. **The prior `0.20.9` line was an npm-only patch** (`migrate-ts` + `codegen-ts`; PyPI / NuGet / Maven Central unchanged) — two correctness fixes found while hardening the D1 migrate path. **#244** makes cross-package references bind by **fully-qualified name**: when two packages declared a same-bare-named `object.entity`, a fully-qualified `@references` / projection `origin.passthrough @from` / `@via` silently bound the wrong package's entity (load-order-dependent) — a foreign key against the wrong table (silent), and an invalid projection view whose `SELECT`/`JOIN` read the wrong table (fails at apply). Both the schema builder (`migrate-ts`, FK targets) and the projection view-spec (`codegen-ts`, `@from`/`@via`/`@of`/`extends` join resolution) now resolve package-aware via the loader's `resolveObjectRef` / `resolutionKey()` contract (FQN binds exactly; bare binds the referrer's package then root-level; ambiguous bare resolves to nothing), plus a build-time `ERR_DUPLICATE_SQL_NAME` guard when two distinct objects generate the same database name. **#243** drops and recreates a dependent view around a table rebuilt by a CHECK / foreign-key / evolved `field.enum @values` change (or a table pulled into the D1 FK-cascade as a referrer) — previously the view was stranded across the recreate-and-copy DROP/RENAME and the migration failed at apply. Each half was reproduced on a real engine before fixing; generated view SQL is byte-identical for existing single-package models. See `CHANGELOG.md` [0.20.9]. **The prior `0.20.8` line was an npm-only patch** (the changed code was in `migrate-ts` plus a small `cli` call-site threading change; PyPI / NuGet / Maven Central unchanged) — **#241** replaces #226's refusal with an **auto-generated, appliable foreign-key cascade**: `meta migrate --dialect d1` now rebuilds an FK-referenced table on remote D1 by DROPping referrers first, RENAMEing parents into place (parents-first), and wrapping the sequence in `PRAGMA defer_foreign_keys = ON` (D1's implicit transaction makes `PRAGMA foreign_keys = OFF` a no-op, which is why #226 could only refuse rather than fix). It also closes #226's residual under-refuse gap by keying the affected-set on the **union** of the actual and expected FK graphs. Multi-table FK cycles are still refused (unresolvable without a cascade), self-references are handled, and existing `meta gen` / `meta migrate` output is byte-identical for migrations that don't rebuild an FK-referenced table on D1. (#243, the dependent-view interaction it opened, was fixed in 0.20.9 above.) See `CHANGELOG.md` [0.20.8]. **The prior `0.20.7` line was an npm-only patch** (the changed code was all in `cli` / `migrate-ts`; PyPI / NuGet / Maven Central unchanged) — **#226** made `meta migrate --dialect d1` **refuse at generation time** (instead of silently emitting an un-appliable migration) when a change would rebuild a table that another table's foreign key references: on remote D1 the SQLite rebuild recipe's `PRAGMA foreign_keys = OFF` is a no-op inside D1's implicit transaction, so dropping the referenced table failed with `FOREIGN KEY constraint failed` against populated production DBs (self-references included; the auto-cascade fix landed in #241 above); and **#242** added **`meta migrate apply-pending`**, a first-class subcommand that replays the committed migration files against `--db` (ledger-tracked, no diff, no metadata load) — the fresh-DB / CI provisioning path the diff-first `--apply` can't serve. Existing `meta gen` / `meta migrate` output was byte-identical for migrations that didn't rebuild an FK-referenced table on D1. See `CHANGELOG.md` [0.20.7]. **The prior `0.20.6` line is a coordinated PATCH across all four registries** (npm `0.20.6` · PyPI `0.19.8` · Maven `7.11.6` · NuGet `0.19.6`) — **#234** ships the **`@lenient` opt-out on `field.uri` / `field.inet`** and pins **strict URI/inet well-formedness identical across all five ports**. A `field.uri` must be an absolute, scheme-bearing URI and a `field.inet` an IPv4/IPv6 literal — previously C#/Java/Kotlin accepted relative URIs, and the JVM ports resolved hostnames into `field.inet` via a **blocking DNS lookup on the request path with a silent value rewrite** (both now fixed, a bug/security fix). **`@lenient: true`** (a new optional boolean on `field.uri` / `field.inet`, registered cross-port + gated by `registry-conformance`) opts a field **out** of strict enforcement: codegen binds a plain string, and a `field.inet @lenient` uses a plain `text` column (so toggling it is schema-affecting). Strict remains the default (attr absent/false → byte-identical output). Also fixes a latent Zod-4 bug — `field.inet` emitted the removed `z.string().ip()`, now a version-agnostic IPv4/IPv6 regex union. Gated by the shared `validation-conformance` corpus + a real-Postgres lenient-inet idempotence round-trip; an adversarial pre-release review caught two cross-port parser divergences the probe set missed (H1 leading-zero IPv4, H2 IPv4-mapped IPv6). See `CHANGELOG.md` [0.20.6]. **The prior `0.20.5` line is a coordinated release across all four registries** (npm `0.20.5` · PyPI `0.19.7` · Maven `7.11.5` · NuGet `0.19.5`) — two cross-port fixes plus two npm-only `migrate-ts` fixes, all additive (PATCH). **#237** (metamodel, all five ports) registers **`@maxTokens` on `template.toolcall`** — the vendor-agnostic per-call token budget already core on `template.prompt` — as an ADR-0037 §2c configuration attribute (optional `int`), gated by `expected-registry.json` + the `template-toolcall-maxtokens` conformance fixture; **`@fallback` is deliberately NOT promoted** (ADR-0011 charters retry/fallback as consumer-provider, its value a vendor-specific object). **#236** (loader, all four ports; Kotlin inherits the JVM loader) **exempts an abstract node from the generic required-attr check**, so an abstract `template.prompt` may hoist shared children yet leave a required `@payloadRef` to its concrete subtypes (enforcement stays at the concrete level — a concrete node missing `@payloadRef`, inherited or own, still errors `ERR_MISSING_REQUIRED_ATTR`), gated by `abstract-template-prompt-hoists-payloadref`. The two **npm-only `migrate-ts`** fixes: **#235** keeps an empty-string column `@default: ""` on sqlite/d1 (only `undefined` now means no-default; a falsy `.length` guard had it perpetually drifting into a destructive recreate), and **#240** stops the OFFLINE `--allow adopt-view` path emitting an illegal `CREATE OR REPLACE VIEW` for a structurally-changed projection (the legal/illegal decision now keys on the **expected** view's columns only, so a known projection drop+creates offline too). Existing `meta gen` output is byte-identical. See `CHANGELOG.md` [0.20.5]. The prior npm `0.20.4` line was an npm-only bug-fix patch (NuGet / PyPI / Maven unchanged — schema migrations are TS-owned, ADR-0015, so no other port has a migrate engine to fix): `meta migrate --allow adopt-view` no longer emits an illegal `CREATE OR REPLACE VIEW` when adopting an UNMANAGED Postgres view (no fingerprint — created before view stamping, or hand-written) that was ALSO structurally changed (column rename / reorder / mid-insert) in the same migration — Postgres rejected that DDL at apply time (`cannot change name of view column …`), aborting the migration on any DB that already held the prior view (invisible until apply). The adopt branch now runs the same `viewReplaceIsLegal` decision the managed path makes: a legal append stays a non-destructive `replace-view`, a PROVABLY illegal structural change (known columns, non-prefix) becomes `drop-view` + `create-view` (re-stamped, so the next migrate converges), and an opaque `@sql` view (columns unknown) keeps its non-destructive replace. Adopting still requires `allow.adoptView` in both cases (the drop half carries the gate before the recreate-pair auto-allow, so it can't silently clobber hand-written SQL). Gated by unit + emit tests and a real-Postgres round-trip. See `CHANGELOG.md` [0.20.4] (#239). The prior `0.20.3` line is a coordinated additive patch (npm `0.20.3` · PyPI `0.19.6` · Maven `7.11.4` · NuGet `0.19.4`): npm ships the `meta docs` prompt-text + prompt data-flow site feature (TS-only), and all four ports ship #238 (ADR-0046) — an `object.value` may now carry a navigation-only `identity.reference` with explicit `@enforce: false` (a DTO/message/wire shape referencing an entity by id), the already-chartered logical-reference form. Zero new registry vocabulary — a child-licensing relaxation (ADR-0037 step 0), so `registry-conformance` is unaffected; the loader resolves the `@references` target (dangling → `ERR_INVALID_REFERENCE`) and codegen emits no FK/DDL, while a value's OWN identity (primary/secondary) and any enforced reference stay banned (`ERR_SUBTYPE_RULE_VIOLATION`). It also makes the M:N `@through`-junction-must-be-`object.entity` guard explicit (value purity used to imply it — a value can't be a join table). Existing `meta gen` output is byte-identical. Cross-port loader change (TS/Java/Python/C#; Kotlin inherits the JVM loader), gated by four new shared conformance fixtures. See `CHANGELOG.md` [0.20.3].** The prior `0.20.2` line is a bug-fix patch (NuGet / PyPI / Maven unchanged — D1 is a TS-only dialect; no changed product file elsewhere): `meta verify --dialect d1` no longer reports permanent, unfixable false schema drift for a hand-migrated D1/SQLite database whose schema genuinely matches its metadata. Four fixes to the shared migrate diff, all sqlite/d1-scoped (Postgres unchanged): `json` and cosmetic `VARCHAR(N)` length no longer read as type drift (SQLite has one text storage class); anonymous inline `CHECK (…)` constraints reconcile by normalized expression (with comments/literals masked so a `CHECK (` inside them can't produce a phantom check, and comma spacing normalized outside literals only); and a bare SQL `NULL` default — which the D1/wrangler runner stringifies to the string `"null"` — is correctly no-default rather than a literal on every no-default column (was permanent, destructive-recreate false drift). Surfaced by a public reference-impl consumer; an adversarial review caught two regressions in the fix (a literal-unaware comma-collapse; the relaxed CHECK finder) — both fixed + pinned. See `CHANGELOG.md` [0.20.2].** The prior `0.20.1` line fixes two first-touch TS quickstart blockers surfaced by a fresh-external-install pressure test (NuGet / PyPI / Maven unchanged — no changed product file): (1) the `meta migrate baseline` greenfield trap — an offline baseline recorded the metadata's desired schema as already-applied, so on an empty DB no `CREATE TABLE` was ever emitted and the server 500'd `no such table` while the CLI reported success; `baseline` now refuses when it proves the target `--db` is empty and every hint (`meta gen`, no-snapshot, `--help`) routes to the working `meta migrate --from-db --db --dialect --slug init --apply`; (2) un-extensioned generated relative imports failed `TS2835` under a stock `tsc --init` (nodenext) — the `codegen.extStyle` default flips `none`→`js` (`./Author.js`, resolves under both nodenext AND bundler) and `meta init` scaffolds `extStyle: "js"`, so generated code type-checks OOTB (gated by a nodenext compile gate). See `CHANGELOG.md` [0.20.1].** The prior `0.19.4` / `7.11.3` line extends the [ADR-0045](spec/decisions/ADR-0045-generated-api-surface-owns-write-semantics.md) `field.timestamp @autoSet` stamping guarantee from vanilla entities to the TPH (single-table discriminator) per-subtype API surface** ([#203](https://github.com/metaobjectsdev/metaobjects/issues/203)/[#229](https://github.com/metaobjectsdev/metaobjects/issues/229)) — a **Maven + PyPI** release (npm + NuGet product code unchanged; only their cross-port test lanes were touched, no re-release). The generated **Java, Kotlin and Python** TPH per-subtype controllers/routers now stamp `@autoSet` — each was a SEPARATE unstamped code path (Java's `emitTph` delegated raw to the consumer repository interface with no `stampForInsert`, Kotlin bound the columns straight from the DTO, Python omitted the stamp lines); **C# and TypeScript already stamped** on the TPH path (EF route stamping / Zod schema transforms) and were verified unchanged. On create both `@autoSet` columns stamp from one captured `now()` (equal); a PATCH bumps every `onUpdate` column and never rewrites `onCreate`; caller-supplied values are ignored; `@autoSet` is excluded from each port's per-subtype settable set. A subtype declaring its OWN `@autoSet` column (rather than inheriting from the shared base) is a documented compile-safe non-goal. Gated cross-port by a new `tph-autoset-patch` scenario on the shared `api-contract-conformance/tph` corpus (every port's generated TPH lane + the TS/C# reference lanes); byte-identical output for a TPH hierarchy with no `@autoSet` field. Also fixes the **vanilla (non-TPH) Java `Patch` write-once `@autoSet` bug** — the settable set now excludes `@autoSet` so an HTTP PATCH can't overwrite a write-once `onCreate` timestamp (mirrors the Python fix in `0.19.3`). See `CHANGELOG.md` [0.19.4]. **The prior `0.19.3` line completes the ADR-0044 payload-record collision-naming rollout ([#219](https://github.com/metaobjectsdev/metaobjects/issues/219)) across the remaining ports** (no breaking changes, no new vocabulary): when two `object.value`s share a bare short name across packages (both reachable from one payload by FQN `@objectRef` — valid since ADR-0041/0042), the **Python** payload generator deduped by `fqn()` (bare when a loaded object's own package is unset) and collapsed both `Note`s into one class dropping the second shape, while **Java** and **Kotlin** (one-file-per-record) wrote both records to the same `NotePayload` path — second clobbers first, last-wins. All three now run ADR-0044's three-pass pipeline (FQN-keyed closure walk → collision-scoped naming, `AcmeAlphaNotePayload` on collision, `ERR_PAYLOAD_NAME_COLLISION` backstop → emit through the name map); non-colliding output is byte-identical, per-port gated (the Kotlin xpkg conformance test [#220] upgraded from "a file exists" to compile+assert two distinct classes). `ERR_PAYLOAD_NAME_COLLISION` is promoted to the shared error-code ledger + the central registries that gate it (TS `errors.ts` exact-bidirectional, Python `errors.py` superset, Java `ErrorCode.java`). Per-registry scope: **PyPI** carries the Python fix; **Maven** the Java + Kotlin fixes; **npm** the additive `errors.ts` ledger entry only (the TS payload fix shipped in `0.19.2`); **NuGet** is a version-parity bump (the C# fix + its local code shipped in `0.19.2`, C# unchanged here). See `CHANGELOG.md` [0.19.3]. **The prior `0.19.2` line was an npm + NuGet patch** (PyPI and Maven Central unchanged — neither port had a changed file; no breaking changes, no new vocabulary): generated `Form`s validated **every** submit against `InsertSchema`, whose optionals reject `null`, so editing any row holding a NULL optional column was blocked outright — errors surfaced on fields the user never touched, `handleSubmit` never fired, and the save silently did nothing; the resolver now switches on `defaultValues` presence to `UpdateSchema`, which is also the semantically right pairing (an edit submits a PATCH) (#227); three CLI/codegen fixes surfaced by building the canonical advanced-modeling example — `verify --codegen`'s throwaway regen root not carrying the project's `templates/` dir forward (spurious drift failure for any template-output project), a relative `outDir` resolving against ambient `process.cwd()` instead of the resolved `--cwd` (both `gen`'s write path and `verify --codegen`'s read path), and a `render-helper.ts` `@payloadRef` missing `stripPackage()` before emission; and **#219 stage 1 / ADR-0044** — payload record emission is now FQN-keyed with collision-scoped naming in **TypeScript + C#** (two same-short-name `object.value`s across packages previously collapsed: TS deduped by bare name and silently dropped the second, C# stripped the package before resolution and picked whichever its bare-name scan hit first — the wrong shape). Not breaking: non-colliding output is byte-identical, pinned by no-churn tests. **Python, Java and Kotlin remain affected** by the same bug class through different mechanisms (Python class-shadowing, Java/Kotlin file clobber) — tracked as the remaining stages on #219. See `CHANGELOG.md` [0.19.2]. **The prior `0.19.1` line was a coordinated patch** (no breaking changes, no new vocabulary): an explicitly authored `validator.length @min` is now authoritative over the FR-036 Pin 1 implicit non-empty floor — every port previously clamped an authored `@min` with `max(@min, 1)` and silently discarded it, so `@min: 0` on a `@required` string was inexpressible; now `@min: 0` restores "must be provided, may be empty" and any explicit `@min` always wins (#224); the `@required` vocabulary (spec files, embedded definitions, registry manifest, `metaobjects-authoring` skill) now matches that shipped behavior (#224 §A5); and `meta verify --dialect d1 --d1 [--remote]` can target Cloudflare D1 through the existing wrangler transport, with `--db file:` into `.wrangler/state/**/d1/**` now warning it is verifying the local database rather than silently doing so (#225). See `CHANGELOG.md` [0.19.1] (and the separate PyPI [0.19.1] entry for the Python-only hotfix folded into this line's `0.19.2`). **The prior `0.19.0` / `7.11.0` line was a coordinated additive minor** (no breaking changes): **image support** — a metadata-driven `view.image` form control on `field.string` (storage is an opaque key; no image bytes cross the wire), whose generated `Form` (`codegen-ts-react`) renders an upload/crop `` widget via a react-hook-form ``, backed by a consumer-supplied `ImageUploadAdapter` (`upload`/`imageUrl`) over React context — plus a new **`metaobjects-ui-web`** TS-applied concern provider (`spec/metamodel/ui-web.json`) that now owns `view.image`'s five presentation attrs (`@aspectRatio`, `@maxEdge`, `@store`, `@accept`, `@maxBytes`) and the previously-deferred **`@rows`** on `view.textarea` (core `view.json` still registers zero view attrs, so the FR-033 invariant holds; non-TS ports mirror the spec file but never apply it — TS-web-only). New client surface: `@metaobjectsdev/runtime-web` (`canvasToJpegBlob`/`reencodeJpeg` + adapter types) and `@metaobjectsdev/react` (``, ``/`useImageUploadAdapter()`, `cropToBlob`, an optional `./form.css` export; `react-easy-crop` is an optional lazy-loaded peer). Also folds in a `0.18.0` test-only fixup (the `@formExclude` registration had left two full-suite tests red on `main`; regenerated, no re-release — published `0.18.0` product code was correct). See `CHANGELOG.md` [0.19.0]. **The prior `0.18.0` / `7.10.0` line was a coordinated additive minor** (no breaking changes): **view-kind form-control dispatch** in `codegen-ts-react` — the generated `Form` now renders the right control per field view (enum→`