Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
462727f
docs(#246): design spec — shared-enum cross-package (Kotlin import fi…
dmealing Aug 1, 2026
0440471
docs(#246): implementation plan — shared-enum cross-package (8 tasks)
dmealing Aug 1, 2026
e1300d5
fix(#246): Kotlin table generator emits cross-package shared-enum import
dmealing Aug 1, 2026
fd4dbba
test(#246): cover the TPH-fold cross-package enum import path
dmealing Aug 1, 2026
b58f919
feat(#246): register ERR_ENUM_EXTENDS_VALUES_CONFLICT in the cross-po…
dmealing Aug 1, 2026
bb30424
feat(#246): TS loader rejects extends-shared-enum with own values
dmealing Aug 1, 2026
5213418
test(#246): pin non-root-super LEGAL case for enum extends/values con…
dmealing Aug 1, 2026
0895da1
Merge remote-tracking branch 'origin/main' into fix/246-shared-enum-c…
dmealing Aug 2, 2026
3f090b0
feat(#246): Python loader rejects extends-shared-enum with own values
dmealing Aug 2, 2026
357c4eb
feat(#246): Java loader rejects extends-shared-enum with own values (…
dmealing Aug 2, 2026
243bfbc
feat(#246): C# loader rejects extends-shared-enum with own values
dmealing Aug 2, 2026
41dceb6
test(#246): cross-port conformance fixtures for extends-shared-enum v…
dmealing Aug 2, 2026
7dacf92
fix(#259): Kotlin projection enum resolves through two extends hops
dmealing Aug 2, 2026
d32772c
test(#259): depth-2 projection->entity->shared enum conformance fixture
dmealing Aug 2, 2026
2d93d84
docs(#246,#259): CHANGELOG + enum authoring guidance for shared/cross…
dmealing Aug 2, 2026
c37dfdb
docs(#246): address Fable whole-branch review — enum guidance + fixtu…
dmealing Aug 2, 2026
97cee42
no-mistakes(document): Sync stale 249→258 conformance fixture counts …
dmealing Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ here. The format follows [Keep a Changelog](https://keepachangelog.com/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
(pre-1.0; MINOR bumps may introduce breaking changes with notice).

## [Unreleased]

Shared-enum cross-package hardening (**#246** + its sibling **#259**). When cut this releases
as a coordinated PATCH — the loader change (#246) lands in all five ports, the Kotlin codegen
changes (#246 Bug 1, #259) land on Maven Central; no metadata vocabulary changes, byte-identical
output for any model that doesn't hit the specific cross-package/two-hop enum shapes below.

- **#246 — a `field.enum` may now be shared across packages, and a conflicting redeclaration is
rejected instead of silently dropped.** Two independent fixes:
- **Kotlin codegen (Bug 1).** The Exposed table generator dropped the cross-package import for a
shared `field.enum`: when two entities in different packages `extends` one abstract enum
declared in a common package, the generated `<Entity>Table` referenced the shared enum by its
simple name but never imported it (`Unresolved reference`). It now emits the cross-package
import for enum columns on both the vanilla and TPH-fold paths, mirroring the existing
FK-import machinery. Same-package models are byte-identical (an enum in the table's own
package adds no import). Gated by a new `enum-xpkg` fixture + a `KotlinCompilation` compile-gate.
- **Cross-port loader error `ERR_ENUM_EXTENDS_VALUES_CONFLICT`.** A `field.enum` that both
`extends` a shared package-level abstract enum **and** declares its own `@values` now fails to
load (was silently dropped — one shared enum type has one member set, so the own `@values`
would be discarded by the shared-enum codegen collapse). Enforced identically in all four
loaders (TypeScript, Python, Java, C#; Kotlin inherits the JVM loader), gated by a shared
conformance fixture with an exact cross-port error `jsonPath`. Extending a **concrete**
(non-shared) enum with your own `@values` is still legal.
- **#259 — a `field.enum` inheriting `@values` through TWO `extends` hops now generates correctly
(Kotlin codegen; sibling of #246).** A projection field extending an entity field, where that
entity field itself extends a shared abstract enum, generated **no** per-projection enum at all
— its type collapsed onto the shared enum because the collapse decision inspected the top-most
super rather than the immediate one, so every consumer of that column failed to resolve the
absent per-projection type. The collapse now keys on the **immediate** super: a field whose
direct `extends` target is a package-level abstract enum collapses onto the shared type; a field
whose direct super is a concrete entity/projection field gets its own `<Object><Field>` enum,
populated with the values it inherits (resolved across any number of hops). Byte-identical for
one-hop projections, shared-enum (FR-019) collapse, and entity-extends-shared. Also hardens the
enum emitter: a `field.enum` that resolves to no `@values` now fails loudly at generate-time
instead of silently emitting nothing (the exact silent no-emit #259 reported). Gated by a
two-hop `KotlinCompilation` compile-gate + a depth-2 cross-port conformance fixture.

The C# materialized-enum cross-namespace sibling and the enum-primary-key-as-`String` issue are
documented as out-of-scope in the design spec
(`docs/superpowers/specs/2026-07-31-shared-enum-cross-package-design.md`); a follow-up is the
Kotlin `enumTypeName` collapse gaining the `isAbstract` leg the other ports already carry (so a
root-level *concrete* enum extended with own `@values` gets a per-field enum on every port).

## [0.20.10] — 2026-08-02

**Coordinated PATCH** — npm `0.20.10` · PyPI `0.19.9` · NuGet `0.19.7` · Maven Central `7.11.7`.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ _Last refreshed 2026-08-02._
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.

**Cross-port conformance corpora** (every port runs the shared corpus):
- Metamodel: `fixtures/conformance/` (255 fixtures; 19 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Metamodel: `fixtures/conformance/` (258 fixtures; 19 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ metaobjects/
├── CLAUDE.md # project instructions for Claude
├── spec/ # canonical metamodel docs, ADRs, roadmap
├── fixtures/ # 19 cross-language conformance corpora — the oracle
│ ├── conformance/ # metamodel (loader + serializer + navigation), 249 fixtures
│ ├── conformance/ # metamodel (loader + serializer + navigation), 258 fixtures
│ ├── yaml-conformance/ # YAML authoring desugar
│ ├── render-conformance/ # FR-004 byte-identical render oracle
│ ├── verify-conformance/ # FR-004 template-drift gate
Expand Down
4 changes: 2 additions & 2 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l`.

| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
|---|---|---|---|---|---|---|
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 255 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 258 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ |
| [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 15 | 15 / 15 | 14 / 15 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 14 / 15 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 15 / 15 |
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 | ✓ | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ |
Expand Down Expand Up @@ -69,7 +69,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.

## Fixture-to-doc mapping

### `fixtures/conformance/` — metamodel loader + canonical serializer (255)
### `fixtures/conformance/` — metamodel loader + canonical serializer (258)

| Fixture prefix | Feature doc |
|---|---|
Expand Down
3 changes: 3 additions & 0 deletions docs/features/abstracts-and-inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ inheritance chain.
— `ERR_UNRESOLVED_SUPER` when the reference doesn't resolve
- [`enum-abstract-extends`](../../fixtures/conformance/enum-abstract-extends/) —
abstracts on enum fields specifically
- [`enum-extends-two-hop-projection`](../../fixtures/conformance/enum-extends-two-hop-projection/)
— inherited enum members resolve through two `extends` hops (projection → entity
→ shared abstract enum)

## See also

Expand Down
39 changes: 36 additions & 3 deletions docs/features/field-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,42 @@ must be a non-empty set of unique members matching `^[A-Za-z_][A-Za-z0-9_]*$`.
```

The loader enforces members own-only and emits `ERR_BAD_ATTR_VALUE` on a bad
member or `ERR_MISSING_REQUIRED_ATTR` on missing `@values`. Reuse via an abstract
`field.enum` + `extends`. Int-backed enums, display labels, and native Postgres
`ENUM` types are deferred (see
member or `ERR_MISSING_REQUIRED_ATTR` on missing `@values`. Int-backed enums,
display labels, and native Postgres `ENUM` types are deferred (see
[enum-datatype-design.md](../superpowers/specs/2026-05-23-enum-datatype-design.md)).

### Sharing one enum — abstract `field.enum` + `extends`

Reuse a constraint set across entities by declaring one **abstract** `field.enum`
(with the `@values`) and having each concrete field `extends` it — including
**across packages** (declare the shared enum in a common package and reference it
by FQN). Every field that extends the same abstract enum collapses onto one
generated enum type, so the members stay in exactly one place:

```jsonc
// meta.common.json — package acme::common
{ "field.enum": { "name": "RecordStatus", "abstract": true,
"@values": ["DRAFT", "ACTIVE", "CLOSED"] } }

// meta.orders.json — package acme::orders
{ "field.enum": { "name": "status", "extends": "acme::common::RecordStatus" } }
```

Two rules the loader/codegen enforce:

- **A field extending a shared abstract enum must NOT declare its own `@values`.**
A shared enum is one type with one member set, so an own `@values` on the
extending field would be silently dropped by the shared-enum collapse — it is a
load error, `ERR_ENUM_EXTENDS_VALUES_CONFLICT`. Remove the own `@values` to
inherit the shared set. If you need a **different** member set, declare a
separate `field.enum` with its own `@values` rather than extending the shared
one — extending is for *reuse* of one member set, not for overriding it.
- **Inherited members resolve through any number of `extends` hops.** A projection
field that extends an entity field which itself extends a shared abstract enum
gets the shared members transitively — the projection still materializes its own
per-projection enum type, populated from the inherited set (a projection is
self-contained: its enum lives in the projection's own package).

## Embedded value objects — `field.object` + `@storage`

`field.object` declares an embedded structured value backed by another `object`
Expand Down Expand Up @@ -236,6 +267,8 @@ The following conformance fixtures gate this feature's behavior across ports:
- [`fixtures/conformance/enum-inline/`](../../fixtures/conformance/enum-inline/) — inline `field.enum @values`
- [`fixtures/conformance/enum-array/`](../../fixtures/conformance/enum-array/) — array-of-enum field
- [`fixtures/conformance/enum-abstract-extends/`](../../fixtures/conformance/enum-abstract-extends/) — reuse via abstract `field.enum` + `extends`
- [`fixtures/conformance/enum-extends-two-hop-projection/`](../../fixtures/conformance/enum-extends-two-hop-projection/) — inherited members resolve through two `extends` hops (projection → entity → shared)
- [`fixtures/conformance/error-enum-extends-values-conflict/`](../../fixtures/conformance/error-enum-extends-values-conflict/) — `ERR_ENUM_EXTENDS_VALUES_CONFLICT` when extending a shared abstract enum and also declaring own `@values`
- [`fixtures/conformance/error-enum-missing-values/`](../../fixtures/conformance/error-enum-missing-values/) — `ERR_MISSING_REQUIRED_ATTR` when `@values` absent
- [`fixtures/conformance/error-enum-empty-values/`](../../fixtures/conformance/error-enum-empty-values/) — empty `@values` rejected
- [`fixtures/conformance/error-enum-duplicate-member/`](../../fixtures/conformance/error-enum-duplicate-member/) — duplicate member symbols rejected
Expand Down
Loading
Loading