Skip to content

Commit efe12d4

Browse files
dmealingclaude
andcommitted
test(#271): pin sourceless projections cross-port; retire #210's falsifier
#271 assumed #248's persistability-from-source contract had never reached the four non-TS ports, so a sourceless object.projection would fall through their codegen fan-outs. Recon falsified that: Kotlin skips on a resolving `firstRdbSource(entity) ?: continue`, Java null-guards `firstRdbSource`, C# gates on `IsEntity() || DbView is not null`, and Python's `_primary_source_rdb` returns None with an explicit ADR-0039 note. The contract was already honored everywhere. What was missing was any pin on it. So this is the pins, not a port. `projection-sourceless` is the shared metamodel anchor; each non-TS port gets a codegen test asserting the DB-bound tier emits nothing for the shape. The Java one pins the source gate through a sourceless ENTITY rather than the projection, because appliesTo rejects a projection on its leading SUBTYPE_ENTITY check and never consults the source — asserting on the projection alone would stay green if the source guard were deleted. The substantive result is for #210: every port accepts a sourceless projection with NO new vocabulary, so the ADR-0023 escape hatch that ruling was gated on is not needed. That falsifier is retired, which is why #271 was sequenced first. Also corrects the conformance fixture count (260 -> 261) across CONFORMANCE.md, CLAUDE.md, README, roadmap and both llms files, adds the `projection-*` row the fixture-to-doc map never had, and picks up uv.lock, still pinned at 0.20.11 after the 0.20.14 cut bumped pyproject.toml. NOT included, and worth stating: an earlier draft of this work flipped C#'s FindPrimaryReadOnlySource()/IsReadOnlyProjection() from own-only to resolving, reading their doc comment's "the parent entity's writable source" rationale as a pre-FR-024 fossil. Independent review caught that this is wrong. Own-only is the deliberate cross-port contract: codegen-ts projection-detector.ts carries "ADR-0039: own — projection source-kind classification. Mirrors C# IsReadOnlyProjection()/projection OwnSources", and Java's findPrimaryReadOnlySource is own-only with the same javadoc. The flip would have created the divergence it claimed to remove. Reverted; C# is untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3msoGxjRMwx94PhKSLDuE
1 parent e6c26d8 commit efe12d4

16 files changed

Lines changed: 534 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Added — sourceless-projection conformance fixture (#271)
11+
12+
`projection-sourceless` pins, across all five ports, that an `object.projection`
13+
may carry no `source.*` at all — the shape #210 makes common when prompt payloads
14+
become projections. It complements the existing `projection-basic` (also
15+
sourceless) by adding two axes nothing else covers: a **self-declared** field
16+
(no `extends`, no `origin.*`) and **no identity at all**.
17+
18+
Running it retired #210's stated falsifier: every port accepts the shape with **no
19+
new vocabulary**, so the ADR-0023 escape hatch that ruling was gated on is not
20+
needed.
21+
22+
Each non-TS port gains a codegen test asserting the DB-bound tier emits nothing
23+
for it — behavior they already had via #248's source-derived contract, now
24+
regression-proof. The Java test pins the source gate through a sourceless
25+
**entity**, since `SpringRepositoryGenerator.appliesTo` rejects a projection on its
26+
leading subtype check before the source is ever consulted.
27+
28+
### Fixed — `server/python/uv.lock` still pinned `metaobjects` at `0.20.11`
29+
30+
The `0.20.14` cut bumped `pyproject.toml` without regenerating the lock.
31+
1032
## [0.20.14] — npm `0.20.14` · PyPI `0.20.14` · NuGet `0.20.14` · Maven `7.20.14`
1133

1234
**Scope: npm `0.20.14` · PyPI `0.20.14` · NuGet `0.20.14` · Maven Central `7.20.14`**

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ _Last refreshed 2026-08-05._
3232
- **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.
3333

3434
**Cross-port conformance corpora** (every port runs the shared corpus):
35-
- Metamodel: `fixtures/conformance/` (260 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.
35+
- Metamodel: `fixtures/conformance/` (261 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.
3636
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
3737
- 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".)
3838
- 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.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ metaobjects/
166166
├── CLAUDE.md # project instructions for Claude
167167
├── spec/ # canonical metamodel docs, ADRs, roadmap
168168
├── fixtures/ # 19 cross-language conformance corpora — the oracle
169-
│ ├── conformance/ # metamodel (loader + serializer + navigation), 260 fixtures
169+
│ ├── conformance/ # metamodel (loader + serializer + navigation), 261 fixtures
170170
│ ├── yaml-conformance/ # YAML authoring desugar
171171
│ ├── render-conformance/ # FR-004 byte-identical render oracle
172172
│ ├── verify-conformance/ # FR-004 template-drift gate

docs/CONFORMANCE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l`.
2525

2626
| Corpus | Fixtures | TS | Java | Kotlin | C# | Python |
2727
|---|---|---|---|---|---|---|
28-
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 260 ||| inherits via `metadata-ktx` |||
28+
| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 261 ||| inherits via `metadata-ktx` |||
2929
| [`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 |
3030
| [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 ||| inherits via Java |||
3131
| [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 ||||||
@@ -69,7 +69,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
6969

7070
## Fixture-to-doc mapping
7171

72-
### `fixtures/conformance/` — metamodel loader + canonical serializer (260)
72+
### `fixtures/conformance/` — metamodel loader + canonical serializer (261)
7373

7474
| Fixture prefix | Feature doc |
7575
|---|---|
@@ -89,6 +89,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker.
8989
| `relationship-*`, `error-unknown-relationship-*` | [features/relationships.md](features/relationships.md) |
9090
| `template-*`, `error-template-*` | [features/templates-and-payloads.md](features/templates-and-payloads.md) |
9191
| `origin-*`, `error-origin-*` | [features/templates-and-payloads.md](features/templates-and-payloads.md) (payload origins) |
92+
| `projection-*`, `error-projection-*`, `field-readonly-on-view-projection` | [features/source-kinds.md](features/source-kinds.md) (projections + the object taxonomy, ADR-0028) |
9293
| `smoke-empty-metadata` | [features/entities.md](features/entities.md) |
9394

9495
### `fixtures/yaml-conformance/` (15)

docs/llms/llms-full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Because the render is conformance-gated, the determinism guarantee holds in ever
9999
| C# | NuGet `0.20.14` (.NET tool) | Loader + canonical serializer + EF Core + ASP.NET codegen + render/verify. `dotnet meta` tool. |
100100
| Python | PyPI `0.20.14` | Loader + serializer + render + verify + codegen + `ObjectManager` runtime. Fully green across all corpora. |
101101

102-
Conformance fixtures live at [`fixtures/`](https://github.com/metaobjectsdev/metaobjects/tree/main/fixtures). Every port runs the shared corpus byte-identically: metamodel (`conformance/`, 260 fixtures), render, persistence (Testcontainers Postgres, with an `op: roundtrip` gate so every `field.*` subtype write+read round-trips on every port), api-contract (41 scenarios — 26 core plus TPH / M:N / jsonb / write-through — two lanes: a reference server AND each port's generated API booted over HTTP), registry (byte-matched metamodel-vocabulary manifest, live + green in all five ports), and yaml/verify.
102+
Conformance fixtures live at [`fixtures/`](https://github.com/metaobjectsdev/metaobjects/tree/main/fixtures). Every port runs the shared corpus byte-identically: metamodel (`conformance/`, 261 fixtures), render, persistence (Testcontainers Postgres, with an `op: roundtrip` gate so every `field.*` subtype write+read round-trips on every port), api-contract (41 scenarios — 26 core plus TPH / M:N / jsonb / write-through — two lanes: a reference server AND each port's generated API booted over HTTP), registry (byte-matched metamodel-vocabulary manifest, live + green in all five ports), and yaml/verify.
103103

104104
---
105105

docs/llms/llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This `llms.txt` is the short index; the deep, version-matched how-to is the scaf
3131
## Spec and standard
3232

3333
- [Specification (canonical, target-agnostic)](https://github.com/metaobjectsdev/metaobjects/tree/main/spec): the normative metadata schema and semantics that every implementation must conform to.
34-
- [Conformance fixtures](https://github.com/metaobjectsdev/metaobjects/tree/main/fixtures): cross-port corpora — `conformance/` (metamodel, 260 fixtures), `render-conformance/`, `persistence-conformance/` (against Testcontainers Postgres, including an `op: roundtrip` gate so every `field.*` subtype write+read round-trips on every port), `api-contract-conformance/` (41 scenarios — 26 core REST contract + filter operators, plus TPH / M:N / jsonb / write-through — run in two lanes: a reference server AND each port's generated API booted over HTTP), `registry-conformance/` (byte-matched metamodel-vocabulary manifest, live + green in all five ports), and `yaml-conformance/` / `verify-conformance/`. Every port runs the shared corpus byte-identically.
34+
- [Conformance fixtures](https://github.com/metaobjectsdev/metaobjects/tree/main/fixtures): cross-port corpora — `conformance/` (metamodel, 261 fixtures), `render-conformance/`, `persistence-conformance/` (against Testcontainers Postgres, including an `op: roundtrip` gate so every `field.*` subtype write+read round-trips on every port), `api-contract-conformance/` (41 scenarios — 26 core REST contract + filter operators, plus TPH / M:N / jsonb / write-through — run in two lanes: a reference server AND each port's generated API booted over HTTP), `registry-conformance/` (byte-matched metamodel-vocabulary manifest, live + green in all five ports), and `yaml-conformance/` / `verify-conformance/`. Every port runs the shared corpus byte-identically.
3535
- [Roadmap](https://github.com/metaobjectsdev/metaobjects/blob/main/spec/roadmap.md): current + planned work across all implementations.
3636

3737
## The four pillars
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Author",
8+
"children": [
9+
{
10+
"source.rdb": {
11+
"@table": "authors"
12+
}
13+
},
14+
{
15+
"field.uuid": {
16+
"name": "id"
17+
}
18+
},
19+
{
20+
"field.string": {
21+
"name": "name",
22+
"@maxLength": 200
23+
}
24+
},
25+
{
26+
"identity.primary": {
27+
"name": "pk",
28+
"@fields": [
29+
"id"
30+
]
31+
}
32+
}
33+
]
34+
}
35+
},
36+
{
37+
"object.projection": {
38+
"name": "AuthorPayload",
39+
"children": [
40+
{
41+
"field.string": {
42+
"name": "name",
43+
"extends": "demo::Author.name",
44+
"@maxLength": 200
45+
}
46+
},
47+
{
48+
"field.string": {
49+
"name": "summary"
50+
}
51+
}
52+
]
53+
}
54+
}
55+
]
56+
}
57+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Author",
8+
"children": [
9+
{
10+
"source.rdb": {
11+
"@table": "authors"
12+
}
13+
},
14+
{
15+
"field.uuid": {
16+
"name": "id"
17+
}
18+
},
19+
{
20+
"field.string": {
21+
"name": "name",
22+
"@maxLength": 200
23+
}
24+
},
25+
{
26+
"identity.primary": {
27+
"name": "pk",
28+
"@fields": [
29+
"id"
30+
]
31+
}
32+
}
33+
]
34+
}
35+
},
36+
{
37+
"object.projection": {
38+
"name": "AuthorPayload",
39+
"children": [
40+
{
41+
"field.string": {
42+
"name": "name",
43+
"extends": "demo::Author.name"
44+
}
45+
},
46+
{
47+
"field.string": {
48+
"name": "summary"
49+
}
50+
}
51+
]
52+
}
53+
}
54+
]
55+
}
56+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Author",
8+
"children": [
9+
{ "source.rdb": { "@table": "authors" } },
10+
{ "field.uuid": { "name": "id" } },
11+
{ "field.string": { "name": "name", "@maxLength": 200 } },
12+
{ "identity.primary": { "name": "pk", "@fields": ["id"] } }
13+
]
14+
}
15+
},
16+
{
17+
"object.projection": {
18+
"name": "AuthorPayload",
19+
"children": [
20+
{ "field.string": { "name": "name", "extends": "demo::Author.name" } },
21+
{ "field.string": { "name": "summary" } }
22+
]
23+
}
24+
}
25+
]
26+
}
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["metaobjects-core-types", "metaobjects-db"]

0 commit comments

Comments
 (0)