Skip to content

Commit a6daacc

Browse files
dmealingclaude
andcommitted
feat(metadata): FR-014 TPH discriminator metadata + cross-attribute validation
Adds the metamodel layer for FR-014 — the @Discriminator and @discriminatorValue attrs on object.entity that declare table-per-hierarchy single-table inheritance. This commit ships the loader / validation layer only; FR-014's per-port ORM emission (EF HasDiscriminator / JPA @DiscriminatorColumn / SQLAlchemy polymorphic_on / Kotlin sealed class / TS discriminated union) and the generator-stack design (FR-017 — polymorphic routes / hooks / forms / Spring controllers) are out of scope and remain queued on the metadata-codegen plan. Implementation (server/typescript/packages/metadata): - OBJECT_ATTR_DISCRIMINATOR + OBJECT_ATTR_DISCRIMINATOR_VALUE constants in core/object/object-constants.ts. - Two AttrSchema entries on objectAttrs (string, optional). - New validation pass core/object/validate-discriminator.ts emits: * ERR_DISCRIMINATOR_FIELD_NOT_FOUND — @Discriminator names a non-existent field on the entity (checked across the extends chain). * ERR_DISCRIMINATOR_VALUE_DUPLICATE — two subtypes of the same root claim the same @discriminatorValue. * ERR_DISCRIMINATOR_VALUE_MISSING — a concrete (non-abstract) entity extending a @discriminator-bearing root lacks @discriminatorValue. * ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH — value not in enum @values when the discriminator field is field.enum; non-numeric when field is int/long/etc. - 4 new codes registered in errors.ts + the shared ERROR-CODES.json registry. Conformance fixtures (7 under fixtures/conformance/): - 3 positive: tph-discriminator-enum-with-subtypes, tph-discriminator-string-no-subtypes (refactor-in-progress shape), tph-discriminator-multi-level (3-level abstract-mid hierarchy). - 4 error: error-tph-discriminator-field-not-found, error-tph-discriminator- value-duplicate, error-tph-discriminator-value-missing, error-tph- discriminator-value-type-mismatch. Cross-port enum parity (so other ports' enum-vs-corpus checks stay green): - Java ErrorCode, C# ErrorCode, Python ErrorCode: all 4 FR-014 codes added. - Java / C# / Python conformance ledgers updated to defer the 4 FR-014 error fixtures until each port ships the FR-014 validation pass. Tests: 15 dedicated unit tests in fr014-tph-discriminator.test.ts. Full TS metadata suite: 1477 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9db16fd commit a6daacc

27 files changed

Lines changed: 1105 additions & 7 deletions

File tree

fixtures/conformance/ERROR-CODES.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"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).",
4141
"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.",
4242
"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).",
43+
"ERR_DISCRIMINATOR_FIELD_NOT_FOUND": "FR-014: object.entity @discriminator names a field that does not exist on the entity (including inherited fields).",
44+
"ERR_DISCRIMINATOR_VALUE_DUPLICATE": "FR-014: two subtypes of the same @discriminator-bearing root declare the same @discriminatorValue.",
45+
"ERR_DISCRIMINATOR_VALUE_MISSING": "FR-014: a concrete entity extends a chain whose root declares @discriminator but the subtype lacks @discriminatorValue.",
46+
"ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH": "FR-014: @discriminatorValue is not a member of the discriminator field's @values (when the discriminator field is field.enum) or cannot be coerced to its subtype.",
4347
"ERR_YAML_COERCION": "A YAML 1.2 silent type coercion produced a value whose runtime type differs from the attribute's declared valueType (e.g. an unquoted `column: TRUE` parsed as boolean for a string-typed attr). Emitted by every port's YAML loader; canonical JSON is unaffected.",
4448
"ERR_MERGE_CONFLICT": "FR5c: multi-file overlay merge produced an attribute conflict — two contributing files declared the same @attr with different non-empty values on the same logical node. Envelope is `format: \"merged\"` with `contributors[]` naming both files.",
4549
"ERR_UNKNOWN": "An internal loader error with no stable error code."
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_DISCRIMINATOR_FIELD_NOT_FOUND",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.demo.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[0]['object.entity']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Auth",
8+
"@discriminator": "noSuchField",
9+
"children": [
10+
{ "source.rdb": { "@table": "auths" } },
11+
{ "field.long": { "name": "id" } },
12+
{ "identity.primary": { "@fields": "id" } }
13+
]
14+
}
15+
}
16+
]
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_DISCRIMINATOR_VALUE_DUPLICATE",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.demo.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[2]['object.entity']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Auth",
8+
"@discriminator": "type",
9+
"children": [
10+
{ "source.rdb": { "@table": "auths" } },
11+
{
12+
"field.enum": {
13+
"name": "type",
14+
"@values": ["Bridge", "Copay"]
15+
}
16+
},
17+
{ "field.long": { "name": "id" } },
18+
{ "identity.primary": { "@fields": "id" } }
19+
]
20+
}
21+
},
22+
{
23+
"object.entity": {
24+
"name": "BridgeA",
25+
"extends": "Auth",
26+
"@discriminatorValue": "Bridge",
27+
"children": []
28+
}
29+
},
30+
{
31+
"object.entity": {
32+
"name": "BridgeB",
33+
"extends": "Auth",
34+
"@discriminatorValue": "Bridge",
35+
"children": []
36+
}
37+
}
38+
]
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_DISCRIMINATOR_VALUE_MISSING",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.demo.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[1]['object.entity']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Auth",
8+
"@discriminator": "type",
9+
"children": [
10+
{ "source.rdb": { "@table": "auths" } },
11+
{
12+
"field.enum": {
13+
"name": "type",
14+
"@values": ["Bridge"]
15+
}
16+
},
17+
{ "field.long": { "name": "id" } },
18+
{ "identity.primary": { "@fields": "id" } }
19+
]
20+
}
21+
},
22+
{
23+
"object.entity": {
24+
"name": "ForgotValue",
25+
"extends": "Auth",
26+
"children": [
27+
{ "field.int": { "name": "q" } }
28+
]
29+
}
30+
}
31+
]
32+
}
33+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_DISCRIMINATOR_VALUE_TYPE_MISMATCH",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.demo.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[1]['object.entity']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Auth",
8+
"@discriminator": "type",
9+
"children": [
10+
{ "source.rdb": { "@table": "auths" } },
11+
{
12+
"field.enum": {
13+
"name": "type",
14+
"@values": ["Bridge", "Copay"]
15+
}
16+
},
17+
{ "field.long": { "name": "id" } },
18+
{ "identity.primary": { "@fields": "id" } }
19+
]
20+
}
21+
},
22+
{
23+
"object.entity": {
24+
"name": "Unknown",
25+
"extends": "Auth",
26+
"@discriminatorValue": "NotInEnum",
27+
"children": []
28+
}
29+
}
30+
]
31+
}
32+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "Auth",
8+
"@discriminator": "type",
9+
"children": [
10+
{
11+
"source.rdb": {
12+
"@table": "auths"
13+
}
14+
},
15+
{
16+
"field.enum": {
17+
"name": "type",
18+
"@values": [
19+
"Bridge",
20+
"Copay"
21+
]
22+
}
23+
},
24+
{
25+
"field.long": {
26+
"name": "id"
27+
}
28+
},
29+
{
30+
"identity.primary": {
31+
"@fields": [
32+
"id"
33+
]
34+
}
35+
}
36+
]
37+
}
38+
},
39+
{
40+
"object.entity": {
41+
"name": "BridgeAuth",
42+
"extends": "Auth",
43+
"@discriminatorValue": "Bridge",
44+
"children": [
45+
{
46+
"field.int": {
47+
"name": "quantity"
48+
}
49+
}
50+
]
51+
}
52+
},
53+
{
54+
"object.entity": {
55+
"name": "CopayAuth",
56+
"extends": "Auth",
57+
"@discriminatorValue": "Copay",
58+
"children": [
59+
{
60+
"field.decimal": {
61+
"name": "copayAmount",
62+
"@precision": 10,
63+
"@scale": 2
64+
}
65+
}
66+
]
67+
}
68+
}
69+
]
70+
}
71+
}

0 commit comments

Comments
 (0)