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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added
- **`@convert` on `origin.passthrough` — acknowledge a deliberate passthrough type change (#185).** A new optional boolean attr. Absent/false (the default), a passthrough is type-preserving (see Changed, below); `@convert: true` opts a field out of the type-equality check when its type intentionally differs from its `@from` source. It is an **acknowledgement only — it does NOT generate a cast**; the value flows through unchanged and the consumer owns any coercion. Real type-converting projections remain `origin.expression`'s job (#159). Registered on `origin.passthrough` in all five ports (cross-port registry-conformance gated).

### Changed
- **BREAKING — `origin.passthrough` is now type-preserving: a passthrough field must match its `@from` source's type (#185).** A field carrying `origin.passthrough @from: "Entity.field"` forwards another field's value unchanged, so its declared `field.<subType>` and array-ness must be identical to the resolved source field. A divergence now fails load with `ERR_PASSTHROUGH_TYPE_MISMATCH` (e.g. a `field.uuid` source surfaced by a projection as `field.string` — the exact mismodeling that forces hand-written `String↔UUID` bridging and defeats a UUID migration). The check compares **subType and array-ness only — nullability is deliberately not judged** (a view over an outer join legitimately widens `NOT NULL` → nullable). Opt out of a deliberate type change with `@convert: true` (see Added). This **generalizes and retires** the narrow, stored-proc-parameter-ref-only `ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH` (FR-015) into one host-agnostic invariant covering projections, entities, values, and parameter refs alike. Enforced in the loader/verify in all five ports (TS / Python / C# / Java / Kotlin), cross-port conformance-gated. **Migration:** if load newly fails, the error names both the declared type and the source type — declare the source type (usually the fix — the projection was wrong), or add `@convert: true` if the divergence is intentional.

## [0.15.15] — 2026-07-07

_npm `@metaobjectsdev/sdk` + `@metaobjectsdev/cli` `0.15.15` (isolated patch; the other 12 packages stay `0.15.14`). Ships updated agent-context skills — a docs/content change bundled into the SDK and delivered through the CLI (`meta agent-docs` / `meta init`). No runtime or generated-code change; PyPI / NuGet / Maven are unaffected._
Expand Down
12 changes: 12 additions & 0 deletions agent-context/skills/metaobjects-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projectio
detection + view DDL off that read-only source, so without it `meta gen` emits
nothing for the projection.

**A `passthrough` field must match its `@from` source's type.** A passthrough
forwards the source value unchanged, so the projection field's `field.<subType>`
and array-ness must be identical to the source field's — a `field.uuid` source
declared as `field.string` on the projection fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` (this is exactly the mismodeling that leaves a
view `String`-typed over a `uuid` column and forces hand-written coercion).
Declare the source's type. If the type genuinely must differ on purpose, set
`@convert: true` on the `origin.passthrough` to acknowledge it — an
acknowledgement only, it does **not** generate a cast (you own any coercion).
Nullability may differ (an outer-join view legitimately widens `NOT NULL` →
nullable) — only subType + array-ness are checked.

## Abstracts + `extends` (deferred resolution) + `overlay`

An **abstract** node (`abstract: true`) describes a shape but is never emitted as
Expand Down
30 changes: 18 additions & 12 deletions docs/1.0-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY]
- ✅ **B2. `0.x → 1.0` migration guide written** —
[`docs/features/migrations/0.x-to-1.0.md`](features/migrations/0.x-to-1.0.md):
version re-baseline + verify strict-default, jsonb parsed-value, timestamp
instant-default, `@dbColumnType` slim, `index.*`/`@unique` removal, deprecated-export
removal. FR-024 deferred → no declared-API vocab to migrate.
instant-default, `@dbColumnType` slim, `index.*`/`@unique` removal, passthrough
type-preservation (`@convert`), deprecated-export removal. FR-024 deferred → no
declared-API vocab to migrate.

## C. Metamodel freeze (the durable spine)

Expand All @@ -57,12 +58,16 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY]
timestamps + `@localTime`, the `@dbColumnType` slim, reverse-nav finders — and
ADR-0037 now gives a durable framework for any future addition. Only A2 (FR-024)
remains open. Audit the registry for anything still experimental.
- 🔶 **C3. The last breaking metamodel/wire move landed in `0.15.1`/`7.7.1`** — the
`index.*` type + `identity.secondary` key-purity (ADR-0040; `@unique` removed →
`ERR_UNKNOWN_ATTR`). It followed `0.15.0`/`7.7.0` (the metamodel-1.0 vocabulary
program + the ADR-0039 own-accessor fix) and `0.14.0`/`7.6.0` (verify strict-default,
jsonb parsed-value). `index.*` **reset the quiet-period clock** — the first
no-breaking coordinated release must come *after* `0.15.1`/`7.7.1` (see G3).
- 🔶 **C3. The last breaking metamodel/wire move landed in the `0.15.x` line** —
`origin.passthrough` is now type-preserving (#185; a divergent `field.<subType>` or
array-ness fails load with `ERR_PASSTHROUGH_TYPE_MISMATCH`, generalizing/retiring the
FR-015 `ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH`; opt out with the acknowledgement-only
`@convert` on `origin.passthrough`). It followed `0.15.1`/`7.7.1` — the `index.*` type +
`identity.secondary` key-purity (ADR-0040; `@unique` removed → `ERR_UNKNOWN_ATTR`) — which
followed `0.15.0`/`7.7.0` (the metamodel-1.0 vocabulary program + the ADR-0039 own-accessor
fix) and `0.14.0`/`7.6.0` (verify strict-default, jsonb parsed-value). The passthrough move
**reset the quiet-period clock** — the first no-breaking coordinated release must come
*after* it (see G3).
- ✅ **C4. `metamodelVersion` marker SHIPPED** (PR #145, all 5 ports, `"0.9"`). A single **rolled-up**
spec-version string the loader/**registry** exposes and every port emits, asserted by
the conformance matrix — the artifact that lets packages version independently while
Expand Down Expand Up @@ -138,14 +143,15 @@ Status legend: ✅ done · 🔶 in progress · ⬜ not started · ❓ **[RATIFY]
`→8.0.0`, freeze the spec version at `Metamodel 1.0` (C4). Both forward; no
backwards move on any registry.
- ⬜ **G2. Remove the deprecated export** if A3=remove (one-time).
- ⬜ **G3. A quiet period** — at least one coordinated release after `0.15.1`/`7.7.1`
(the last breaking move, C3) with **no metamodel-breaking changes**, to prove the
rate has actually dropped. Requires A2/FR-024 to land additively or defer.
- ⬜ **G3. A quiet period** — at least one coordinated release after the last breaking
move (C3, the `0.15.x` passthrough type-preservation move) with **no
metamodel-breaking changes**, to prove the rate has actually dropped. Requires
A2/FR-024 to land additively or defer.
- ⬜ **G4. Cut Metamodel 1.0** through the `releasing` skill (RC → smoke → confirm →
promote), with the migration guide (B2) + compat policy (B1) published alongside.

---

_Last updated 2026-07-02 (post `0.15.1`/`7.7.1` + `0.15.2`; A1 ratified — decouple).
_Last updated 2026-07-08 (post `0.15.1`/`7.7.1` + `0.15.2`; the `0.15.x` passthrough type-preservation move (#185) reset the quiet-period clock — C3/G3; A1 ratified — decouple).
Owner: maintainer. Update the status marks as items land; ratify the remaining §A
decisions (A2–A4) to unblock the rest._
22 changes: 21 additions & 1 deletion docs/features/migrations/0.x-to-1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,27 @@ the new **`index.lookup`** type.
- **Full mechanical rewrite + the no-DDL-churn guarantee:**
[identity.secondary → index.lookup](identity-secondary-to-index-lookup.md).

## 6. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut)
## 6. `origin.passthrough` is type-preserving (shipped 0.15.x)

A field carrying `origin.passthrough @from: "Entity.field"` forwards the source value
unchanged, so its declared `field.<subType>` and array-ness must be **identical** to the
resolved source field. A divergence now fails load with `ERR_PASSTHROUGH_TYPE_MISMATCH`
(e.g. a `field.uuid` source surfaced by a projection as `field.string` — the exact
mismodeling that leaves a view `String`-typed over a `uuid` column and forces hand-written
`String↔UUID` coercion). The check compares **subType and array-ness only — nullability is
deliberately not judged** (a view over an outer join legitimately widens `NOT NULL` →
nullable). This generalizes and retires the narrow, stored-proc-parameter-ref-only
`ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH` (FR-015) into one host-agnostic invariant
covering projections, entities, values, and parameter refs alike.

- **If load newly fails:** the error names both the declared type and the source type.
Declare the source's type (usually the fix — the projection was wrong), or set
`@convert: true` on the `origin.passthrough` if the divergence is intentional.
- **`@convert` is an acknowledgement only — it does NOT generate a cast**; the value flows
through unchanged and the consumer owns any coercion. Real type-converting projections
remain `origin.expression`'s job (#159, post-1.0).

## 7. Deprecated `codegen-ts/generators` export removed (at the 1.0 cut)

Importing the built-in generators from `@metaobjectsdev/codegen-ts/generators`
(`entityFile` / `queriesFile` / `routesFile` / `barrel`) is **removed** at 1.0.
Expand Down
3 changes: 3 additions & 0 deletions docs/features/templates-and-payloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ The following conformance fixtures gate this feature's behavior across ports:
- [`fixtures/conformance/origin-collection-simple/`](../../fixtures/conformance/origin-collection-simple/) — `origin.collection` for repeated-row payloads
- [`fixtures/conformance/error-origin-bad-via-path/`](../../fixtures/conformance/error-origin-bad-via-path/) — unresolvable `@via` rejected
- [`fixtures/conformance/error-origin-bad-aggregate-fn/`](../../fixtures/conformance/error-origin-bad-aggregate-fn/) — unknown `@agg` rejected
- [`fixtures/conformance/error-origin-passthrough-type-mismatch/`](../../fixtures/conformance/error-origin-passthrough-type-mismatch/) — a `passthrough` field whose `field.<subType>` differs from its `@from` source fails with `ERR_PASSTHROUGH_TYPE_MISMATCH`
- [`fixtures/conformance/error-origin-passthrough-array-mismatch/`](../../fixtures/conformance/error-origin-passthrough-array-mismatch/) — a `passthrough` field whose array-ness differs from its `@from` source fails with `ERR_PASSTHROUGH_TYPE_MISMATCH`
- [`fixtures/conformance/origin-passthrough-convert-optout/`](../../fixtures/conformance/origin-passthrough-convert-optout/) — `@convert: true` acknowledges a deliberate type divergence (no cast generated)

**Render engine output (`fixtures/render-conformance/`)** — byte-identical Mustache output across ports

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projectio
detection + view DDL off that read-only source, so without it `meta gen` emits
nothing for the projection.

**A `passthrough` field must match its `@from` source's type.** A passthrough
forwards the source value unchanged, so the projection field's `field.<subType>`
and array-ness must be identical to the source field's — a `field.uuid` source
declared as `field.string` on the projection fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` (this is exactly the mismodeling that leaves a
view `String`-typed over a `uuid` column and forces hand-written coercion).
Declare the source's type. If the type genuinely must differ on purpose, set
`@convert: true` on the `origin.passthrough` to acknowledge it — an
acknowledgement only, it does **not** generate a cast (you own any coercion).
Nullability may differ (an outer-join view legitimately widens `NOT NULL` →
nullable) — only subType + array-ness are checked.

## Abstracts + `extends` (deferred resolution) + `overlay`

An **abstract** node (`abstract: true`) describes a shape but is never emitted as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projectio
detection + view DDL off that read-only source, so without it `meta gen` emits
nothing for the projection.

**A `passthrough` field must match its `@from` source's type.** A passthrough
forwards the source value unchanged, so the projection field's `field.<subType>`
and array-ness must be identical to the source field's — a `field.uuid` source
declared as `field.string` on the projection fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` (this is exactly the mismodeling that leaves a
view `String`-typed over a `uuid` column and forces hand-written coercion).
Declare the source's type. If the type genuinely must differ on purpose, set
`@convert: true` on the `origin.passthrough` to acknowledge it — an
acknowledgement only, it does **not** generate a cast (you own any coercion).
Nullability may differ (an outer-join view legitimately widens `NOT NULL` →
nullable) — only subType + array-ness are checked.

## Abstracts + `extends` (deferred resolution) + `overlay`

An **abstract** node (`abstract: true`) describes a shape but is never emitted as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projectio
detection + view DDL off that read-only source, so without it `meta gen` emits
nothing for the projection.

**A `passthrough` field must match its `@from` source's type.** A passthrough
forwards the source value unchanged, so the projection field's `field.<subType>`
and array-ness must be identical to the source field's — a `field.uuid` source
declared as `field.string` on the projection fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` (this is exactly the mismodeling that leaves a
view `String`-typed over a `uuid` column and forces hand-written coercion).
Declare the source's type. If the type genuinely must differ on purpose, set
`@convert: true` on the `origin.passthrough` to acknowledge it — an
acknowledgement only, it does **not** generate a cast (you own any coercion).
Nullability may differ (an outer-join view legitimately widens `NOT NULL` →
nullable) — only subType + array-ness are checked.

## Abstracts + `extends` (deferred resolution) + `overlay`

An **abstract** node (`abstract: true`) describes a shape but is never emitted as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projectio
detection + view DDL off that read-only source, so without it `meta gen` emits
nothing for the projection.

**A `passthrough` field must match its `@from` source's type.** A passthrough
forwards the source value unchanged, so the projection field's `field.<subType>`
and array-ness must be identical to the source field's — a `field.uuid` source
declared as `field.string` on the projection fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` (this is exactly the mismodeling that leaves a
view `String`-typed over a `uuid` column and forces hand-written coercion).
Declare the source's type. If the type genuinely must differ on purpose, set
`@convert: true` on the `origin.passthrough` to acknowledge it — an
acknowledgement only, it does **not** generate a cast (you own any coercion).
Nullability may differ (an outer-join view legitimately widens `NOT NULL` →
nullable) — only subType + array-ness are checked.

## Abstracts + `extends` (deferred resolution) + `overlay`

An **abstract** node (`abstract: true`) describes a shape but is never emitted as
Expand Down
2 changes: 1 addition & 1 deletion fixtures/conformance/ERROR-CODES.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"ERR_AMBIGUOUS_REF": "Cross-package reference contract — a BARE reference (no `::`) names an object that exists in more than one package and none is in the referrer's own package. The reference is ambiguous; qualify it with the package (FQN). Applies to every object-ref-bearing attribute (@objectRef, @references, @from/@of/@via heads, extends, @payloadRef/@responseRef, @through).",
"ERR_ORIGIN_CARDINALITY": "FR-024 (ADR-0029): origin cardinality contract broken — a passthrough @via path crosses a to-many hop (row-multiplying passthrough — you meant aggregate), or an aggregate @via path is to-one at every hop (aggregating over a to-one path — you meant passthrough). Checked on explicit AND inferred paths.",
"ERR_EXTENDS_ORIGIN_MISMATCH": "FR-024 (ADR-0029 decision 7): a field declares both an entity-nested extends (shape lineage) and an origin.passthrough @from (data lineage) and they disagree — the resolved @from target is not the field's resolved extends target (nor anywhere on its extends chain). Host-agnostic (projections, entities, values). origin.aggregate is never judged (it computes something new); a top-level abstract extends target is never judged (shape-only reuse makes no lineage claim).",
"ERR_PASSTHROUGH_TYPE_MISMATCH": "#185: a field carrying origin.passthrough @from: \"Entity.field\" declares a field.<subType> or array-ness that differs from its resolved source field — a passthrough forwards the value unchanged, so its type must be identical. Host-agnostic (projections, entities, values, stored-proc parameter refs). Nullability is NOT judged (a view over an outer join legitimately widens NOT NULL → nullable). Opt out of a deliberate type change with @convert: true on the origin.passthrough (an acknowledgement — it does not generate a cast); real type-converting projections are origin.expression's job (#159).",
"ERR_DERIVED_FIELD_NO_READ_SOURCE": "FR-024 (spec §7): an object.entity field carrying an origin.* child is derived (read-only) and must be providable — the entity must declare at least one source with a read-only @kind (view/materializedView/storedProc/tableFunction, e.g. a @role replica view beside the table primary). Table-only or source-less entities with origin-bearing fields error on the field. Projections and object.value hosts are exempt. Until the Phase-E B4b cutover removes view-primary entities, a read-only-kind PRIMARY source also counts as providable (legacy spelling).",
"ERR_INVALID_TEMPLATE": "A template (prompt/output) declares a @payloadRef that does not resolve, or @requiredSlots that are not fields on its payload.",
"ERR_INVALID_RELATIONSHIP": "FR-017: a M:N relationship's slim vocabulary is invalid — @through does not name a junction declaring two identity.reference children, @sourceRefField does not match one of them, or a M:N-only attr (@through/@sourceRefField/@symmetric) is set on a non-M:N (1:N / @cardinality:one) relationship; OR a relationship's @objectRef does not resolve to any object in the loaded tree (a dangling target).",
Expand All @@ -54,7 +55,6 @@
"ERR_PARAMETER_REF_UNRESOLVED": "FR-015: source.rdb @parameterRef names an object that does not exist in the loaded model.",
"ERR_PARAMETER_REF_NOT_VALUE_OBJECT": "FR-015: source.rdb @parameterRef references an object.entity instead of object.value. Input shapes are value-objects by definition (no identity).",
"ERR_PARAMETER_REF_ON_NON_CALLABLE_KIND": "FR-015: source.rdb @parameterRef is set with @kind: \"table\" / \"view\" / \"materializedView\". These kinds do not accept parameters; only \"storedProc\" / \"tableFunction\" do.",
"ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH": "FR-015: a parameter field uses origin.passthrough @from: \"Entity.field\" but the parameter's subtype does not match the referenced field's subtype (drift gate).",
"ERR_DISCRIMINATOR_FIELD_NOT_FOUND": "FR-014: object.entity @discriminator names a field that does not exist on the entity (including inherited fields).",
"ERR_DISCRIMINATOR_VALUE_DUPLICATE": "FR-014: two subtypes of the same @discriminator-bearing root declare the same @discriminatorValue.",
"ERR_DISCRIMINATOR_VALUE_MISSING": "FR-014: a concrete entity extends a chain whose root declares @discriminator but the subtype lacks @discriminatorValue.",
Expand Down
Loading
Loading