Skip to content

Commit ea57ad0

Browse files
authored
feat: add exactlyOne validator, int-backed enum arrays and naming strategies (#554)
* feat: replay the C# app session's fixes, hooks and naming strategies onto main Brings the work from a downstream C# application session onto main, minus the reserved-vocabulary changes (they land separately, with the Java, Kotlin and Python registry catch-up): - C# EF model fixes: @default, assigned keys, TPH subtype fields and references, nested value objects, constraint names and principal keys, the int-backed enum write refusal, -sis plurals, a thread-safe node cache, a split OnModelCreating, the index vocabulary and @description, and @sequenceStart with a start-only change that never restarts. - C# generator hooks: one protected virtual per emitted line on the DbContext, entity and names generators. The golden files are main's pre-hook output plus only the lines the EF model fixes add, each named in the test. - The pascal column rule and application-registered naming strategies in every port (ADR-0058 Amendment 1), with a test-only reset. - Int-backed enum arrays load in every port. - An enum's @default on an untouched record; generated-name collisions scoped to what carries them; doc pages keyed by package; @Schema in the Python runtime; Kotlin index @using/@where; Java create defaults. - Port-scoped registry attrs ("ports"), with @dbSetName C#-only. Completed on main for every port: - @sequenceStart never moves the counter: meta migrate writes ALTER SEQUENCE ... START WITH n only (no setval). New persistence migration scenario sequence-start-change-keeps-counter. - @Schema reaches every mapping: TS Kysely and Drizzle, Java OMDB, Kotlin Exposed tables. The canonical model gains LedgerEntry and an archive-schema ArchivedLedgerEntry of the same table name; new query scenario schema-qualified-same-name (Postgres). - Java and Kotlin create defaults: a string default is a literal unless it is a database expression (shared isSqlExpressionDefault), escaped correctly; the Java switch uses the subtype constants. - New api-contract sub-corpus enum-default/, generated lane in all five ports. - Java/Kotlin api docs default to the package layout in the eject test. - Kotlin compile gates leave out the IntEnumArray controller and repository until enum arrays get an Exposed array column (KNOWN_GAPS.md). - Docs: CHANGELOG upgrade notes, CONFORMANCE counts and per-port status, api-contract "Defaults on create", regenerated metamodel docs. Conflicts resolved toward main's reworks: validator.exactlyOne, the native duration wire default with wire.durations "iso", the provider field in meta types JSON, and MappedBaseClause for the entity base class (the duplicate helper is dropped, the DbContext half is kept). The C# generator that wrote expression indexes as SQL is left out: schema DDL stays with meta migrate (ADR-0015). * no-mistakes(review): apply reviewer inline fixes * no-mistakes(review): Pin schema writes, share string escapers, fix rules * no-mistakes(test): Share Kotlin string escaper; refresh site outputs * fix(csharp-tests): find golden files from the repo root, not CallerFilePath A CI build (CI=true turns on ContinuousIntegrationBuild) maps source paths to /_/, so the [CallerFilePath]-based golden directory did not exist on the runner and the three golden tests failed with DirectoryNotFoundException. Use CorpusPaths.RepoRoot() like the other tests in this project. * no-mistakes(review): Correct CHANGELOG Kotlin enum array wording * no-mistakes(document): Docs current; removed two "Ruling:" labels from new C# doc comments.
1 parent 9abd963 commit ea57ad0

367 files changed

Lines changed: 17599 additions & 1511 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/rules/codegen-architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ origins live on `object.projection` only.
168168
Gated end-to-end by `fixtures/api-contract-conformance/projection/` (view-only)
169169
and `.../write-through/` (table + replica view).
170170

171-
**Column and table naming** is `naming` in `.metaobjects/config.json` (ADR-0058), read by every port: `columns` `snake_case` (default) | `literal` | `kebab-case`, `tables` `snake_case` (default) | `literal`. Generators read it as `ctx.columnNamingStrategy` (set from the loaded root); `metaobjects.config.ts` no longer takes it.
171+
**Column and table naming** is `naming` in `.metaobjects/config.json` (ADR-0058), read by every port: `columns` `snake_case` (default) | `literal` | `kebab-case` | `pascal` (upper-cases the first letter only) | a name the application registered (ADR-0058 Amendment 1), `tables` `snake_case` (default) | `literal`. Generators read it as `ctx.columnNamingStrategy` (set from the loaded root); `metaobjects.config.ts` no longer takes it.
172172

173173
### Currency (Project F)
174174

‎.claude/rules/cross-language-porting.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Preserve the following contracts exactly across all language ports:
2727
- Currency attrs: `@currency` (ISO 4217), `@locale` (BCP 47)
2828
- Schema attrs: `@schema` on `source.rdb` (DB schema name; Postgres default `public`, SQLite rejects non-default values)
2929
- Storage attrs: `@storage` on `field.object` (with `@objectRef`) — values `flattened` / `jsonb` / `subdocument`. Unifies "owned types" (flattened storage) and "structured JSONB" (jsonb storage). Defaults to single-jsonb-column when absent (back-compat).
30-
- Enum: `field.enum` is a first-class field subtype (peer of `currency`), string-backed. Required `@values` string-array attr (member symbols). Members must be a non-empty set, each matching `^[A-Za-z_][A-Za-z0-9_]*$`, no duplicates — every port's loader enforces this (own-only) emitting `ERR_BAD_ATTR_VALUE` (missing `@values` → `ERR_MISSING_REQUIRED_ATTR`). Reuse via abstract `field.enum` + `extends`. Codegen: TS union + `z.enum`, C# `enum` + EF `HasConversion<string>()`, DB `varchar` + `CHECK`. **Int-backed storage shipped** (`@intValueMap`, a `{memberSymbol: int}` map, switches the column to `integer` + int `CHECK` while the wire format, the generated type and every runtime value stay the member SYMBOL; a MAP rather than a positional array so reordering `@values` cannot silently re-map every member; `@isArray` + `@intValueMap` is a LOAD ERROR, and an unmapped stored integer THROWS in all five ports). Display labels and native PG enum remain deferred (see `docs/superpowers/specs/2026-05-23-enum-datatype-design.md`).
30+
- Enum: `field.enum` is a first-class field subtype (peer of `currency`), string-backed. Required `@values` string-array attr (member symbols). Members must be a non-empty set, each matching `^[A-Za-z_][A-Za-z0-9_]*$`, no duplicates — every port's loader enforces this (own-only) emitting `ERR_BAD_ATTR_VALUE` (missing `@values` → `ERR_MISSING_REQUIRED_ATTR`). Reuse via abstract `field.enum` + `extends`. Codegen: TS union + `z.enum`, C# `enum` + EF `HasConversion<string>()`, DB `varchar` + `CHECK`. **Int-backed storage shipped** (`@intValueMap`, a `{memberSymbol: int}` map, switches the column to `integer` + int `CHECK` while the wire format, the generated type and every runtime value stay the member SYMBOL; a MAP rather than a positional array so reordering `@values` cannot silently re-map every member; an int-backed enum array loads in every port and stores each element as its declared integer — TypeScript and C# persist it on every engine, Python on Postgres only, Java's OMDB refuses it at the codec, and Kotlin's Exposed generator emits no enum array column yet; an unmapped stored integer THROWS in all five ports). Display labels and native PG enum remain deferred (see `docs/superpowers/specs/2026-05-23-enum-datatype-design.md`).
3131
- Documentation common attrs (any node): `description`, `title`, `notes`, `deprecated`, `replacedBy`, `seeAlso`, `aliases`. Registered via the cross-language `commonAttrs` registry hook (`registerCommonAttrs` / `RegisterCommonAttrs` / `register_common_attrs` / Java `MetaDataRegistry.registerCommonAttribute` — wired in all four ports). `notes` is the internal-only rationale slot — never emitted to user-facing doc-gen (JSDoc / XML-doc / Postgres `COMMENT ON` / Mermaid prose). **The `description` / `notes` split is by CONTENT KIND, not audience** — an audience split invites writing the same content twice at two levels of politeness. `description` states what the element IS and COVERS (scope and boundary, derivable from the model); `notes` states what you had to look OUTSIDE the model to learn (evidence, citations, what breaks if it changes). Mechanical test: a sentence belongs in `notes` exactly when it would have to change because the IMPLEMENTATION changed while the model did not. `title` is a noun phrase, `summary` a one-line sentence. TS doc-gen ships all three tiers; C# ships XML-doc + COMMENT ON. See `docs/superpowers/specs/2026-05-24-documentation-provider-design.md`.
3232

3333
**Wire format:**

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
7171
- **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.
7272

7373
**Cross-port conformance corpora** (every port runs the shared corpus):
74-
- Metamodel: `fixtures/conformance/` (449 fixtures; 38 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
74+
- Metamodel: `fixtures/conformance/` (451 fixtures; 38 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
7575
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
7676
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015; the FR-044 `report-*` scenarios also run on SQLite, MySQL and D1 where a port has the engine, see [docs/CONFORMANCE.md](docs/CONFORMANCE.md)). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
7777
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.

0 commit comments

Comments
 (0)