@metaobjectsdev/metadata 0.10.0 → 0.11.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/attr-schema-validate.js +12 -4
  2. package/dist/attr-schema-validate.js.map +1 -1
  3. package/dist/core/identity/identity-constants.d.ts.map +1 -1
  4. package/dist/core/identity/identity-constants.js +3 -0
  5. package/dist/core/identity/identity-constants.js.map +1 -1
  6. package/dist/core/identity/identity-definition.embedded.d.ts.map +1 -1
  7. package/dist/core/identity/identity-definition.embedded.js +2 -0
  8. package/dist/core/identity/identity-definition.embedded.js.map +1 -1
  9. package/dist/core/identity/meta-identity.d.ts.map +1 -1
  10. package/dist/core/identity/meta-identity.js +8 -1
  11. package/dist/core/identity/meta-identity.js.map +1 -1
  12. package/dist/core/validator/validator-constants.d.ts +14 -1
  13. package/dist/core/validator/validator-constants.d.ts.map +1 -1
  14. package/dist/core/validator/validator-constants.js +20 -1
  15. package/dist/core/validator/validator-constants.js.map +1 -1
  16. package/dist/core/validator/validator-definition.embedded.d.ts.map +1 -1
  17. package/dist/core/validator/validator-definition.embedded.js +121 -0
  18. package/dist/core/validator/validator-definition.embedded.js.map +1 -1
  19. package/dist/core-types.d.ts.map +1 -1
  20. package/dist/core-types.js +25 -2
  21. package/dist/core-types.js.map +1 -1
  22. package/dist/errors.d.ts +3 -3
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js +9 -0
  25. package/dist/errors.js.map +1 -1
  26. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  27. package/dist/loader/meta-data-loader.js +10 -0
  28. package/dist/loader/meta-data-loader.js.map +1 -1
  29. package/dist/loader/validation-passes.d.ts.map +1 -1
  30. package/dist/loader/validation-passes.js +6 -1
  31. package/dist/loader/validation-passes.js.map +1 -1
  32. package/dist/loader/validation-registry.d.ts +10 -0
  33. package/dist/loader/validation-registry.d.ts.map +1 -0
  34. package/dist/loader/validation-registry.js +84 -0
  35. package/dist/loader/validation-registry.js.map +1 -0
  36. package/dist/parser-core.js +10 -1
  37. package/dist/parser-core.js.map +1 -1
  38. package/dist/persistence/db/db-constants.d.ts +10 -0
  39. package/dist/persistence/db/db-constants.d.ts.map +1 -1
  40. package/dist/persistence/db/db-constants.js +14 -0
  41. package/dist/persistence/db/db-constants.js.map +1 -1
  42. package/dist/persistence/db/db-definition.embedded.d.ts.map +1 -1
  43. package/dist/persistence/db/db-definition.embedded.js +57 -0
  44. package/dist/persistence/db/db-definition.embedded.js.map +1 -1
  45. package/dist/provider-data.d.ts +15 -0
  46. package/dist/provider-data.d.ts.map +1 -1
  47. package/dist/provider-data.js +2 -0
  48. package/dist/provider-data.js.map +1 -1
  49. package/dist/registry.d.ts +16 -0
  50. package/dist/registry.d.ts.map +1 -1
  51. package/dist/registry.js.map +1 -1
  52. package/dist/validate-max-occurs.d.ts +5 -0
  53. package/dist/validate-max-occurs.d.ts.map +1 -0
  54. package/dist/validate-max-occurs.js +28 -0
  55. package/dist/validate-max-occurs.js.map +1 -0
  56. package/dist/validation-types.d.ts +36 -0
  57. package/dist/validation-types.d.ts.map +1 -0
  58. package/dist/validation-types.js +7 -0
  59. package/dist/validation-types.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/attr-schema-validate.ts +12 -4
  62. package/src/core/identity/identity-constants.ts +4 -0
  63. package/src/core/identity/identity-definition.embedded.ts +2 -0
  64. package/src/core/identity/meta-identity.ts +8 -1
  65. package/src/core/validator/validator-constants.ts +22 -1
  66. package/src/core/validator/validator-definition.embedded.ts +121 -0
  67. package/src/core-types.ts +26 -1
  68. package/src/errors.ts +11 -2
  69. package/src/loader/meta-data-loader.ts +12 -0
  70. package/src/loader/validation-passes.ts +11 -1
  71. package/src/loader/validation-registry.ts +93 -0
  72. package/src/parser-core.ts +10 -1
  73. package/src/persistence/db/db-constants.ts +16 -0
  74. package/src/persistence/db/db-definition.embedded.ts +57 -0
  75. package/src/provider-data.ts +17 -0
  76. package/src/registry.ts +16 -0
  77. package/src/validate-max-occurs.ts +39 -0
  78. package/src/validation-types.ts +57 -0
  79. package/dist/constants.d.ts +0 -208
  80. package/dist/constants.d.ts.map +0 -1
  81. package/dist/constants.js +0 -419
  82. package/dist/constants.js.map +0 -1
  83. package/dist/core/documentation/doc-schema.d.ts +0 -8
  84. package/dist/core/documentation/doc-schema.d.ts.map +0 -1
  85. package/dist/core/documentation/doc-schema.js +0 -61
  86. package/dist/core/documentation/doc-schema.js.map +0 -1
  87. package/dist/core/field/field-schema.d.ts +0 -6
  88. package/dist/core/field/field-schema.d.ts.map +0 -1
  89. package/dist/core/field/field-schema.js +0 -23
  90. package/dist/core/field/field-schema.js.map +0 -1
  91. package/dist/core/file-meta-data-loader.d.ts +0 -18
  92. package/dist/core/file-meta-data-loader.d.ts.map +0 -1
  93. package/dist/core/file-meta-data-loader.js +0 -81
  94. package/dist/core/file-meta-data-loader.js.map +0 -1
  95. package/dist/core/file-source.d.ts +0 -12
  96. package/dist/core/file-source.d.ts.map +0 -1
  97. package/dist/core/file-source.js +0 -46
  98. package/dist/core/file-source.js.map +0 -1
  99. package/dist/core/identity/identity-schema.d.ts +0 -6
  100. package/dist/core/identity/identity-schema.d.ts.map +0 -1
  101. package/dist/core/identity/identity-schema.js +0 -56
  102. package/dist/core/identity/identity-schema.js.map +0 -1
  103. package/dist/core/object/object-schema.d.ts +0 -4
  104. package/dist/core/object/object-schema.d.ts.map +0 -1
  105. package/dist/core/object/object-schema.js +0 -28
  106. package/dist/core/object/object-schema.js.map +0 -1
  107. package/dist/core/relationship/relationship-schema.d.ts +0 -4
  108. package/dist/core/relationship/relationship-schema.d.ts.map +0 -1
  109. package/dist/core/relationship/relationship-schema.js +0 -57
  110. package/dist/core/relationship/relationship-schema.js.map +0 -1
  111. package/dist/core/validator/validator-schema.d.ts +0 -4
  112. package/dist/core/validator/validator-schema.d.ts.map +0 -1
  113. package/dist/core/validator/validator-schema.js +0 -38
  114. package/dist/core/validator/validator-schema.js.map +0 -1
  115. package/dist/core-attr-schemas.d.ts +0 -22
  116. package/dist/core-attr-schemas.d.ts.map +0 -1
  117. package/dist/core-attr-schemas.js +0 -324
  118. package/dist/core-attr-schemas.js.map +0 -1
  119. package/dist/db/db-attr-schemas.d.ts +0 -8
  120. package/dist/db/db-attr-schemas.d.ts.map +0 -1
  121. package/dist/db/db-attr-schemas.js +0 -26
  122. package/dist/db/db-attr-schemas.js.map +0 -1
  123. package/dist/db/db-provider.d.ts +0 -3
  124. package/dist/db/db-provider.d.ts.map +0 -1
  125. package/dist/db/db-provider.js +0 -28
  126. package/dist/db/db-provider.js.map +0 -1
  127. package/dist/meta/find-reference.d.ts +0 -22
  128. package/dist/meta/find-reference.d.ts.map +0 -1
  129. package/dist/meta/find-reference.js +0 -29
  130. package/dist/meta/find-reference.js.map +0 -1
  131. package/dist/meta/meta-attr.d.ts +0 -8
  132. package/dist/meta/meta-attr.d.ts.map +0 -1
  133. package/dist/meta/meta-attr.js +0 -17
  134. package/dist/meta/meta-attr.js.map +0 -1
  135. package/dist/meta/meta-data.d.ts +0 -107
  136. package/dist/meta/meta-data.d.ts.map +0 -1
  137. package/dist/meta/meta-data.js +0 -302
  138. package/dist/meta/meta-data.js.map +0 -1
  139. package/dist/meta/meta-field.d.ts +0 -48
  140. package/dist/meta/meta-field.d.ts.map +0 -1
  141. package/dist/meta/meta-field.js +0 -94
  142. package/dist/meta/meta-field.js.map +0 -1
  143. package/dist/meta/meta-identity.d.ts +0 -71
  144. package/dist/meta/meta-identity.d.ts.map +0 -1
  145. package/dist/meta/meta-identity.js +0 -129
  146. package/dist/meta/meta-identity.js.map +0 -1
  147. package/dist/meta/meta-layout.d.ts +0 -23
  148. package/dist/meta/meta-layout.d.ts.map +0 -1
  149. package/dist/meta/meta-layout.js +0 -45
  150. package/dist/meta/meta-layout.js.map +0 -1
  151. package/dist/meta/meta-object.d.ts +0 -40
  152. package/dist/meta/meta-object.d.ts.map +0 -1
  153. package/dist/meta/meta-object.js +0 -81
  154. package/dist/meta/meta-object.js.map +0 -1
  155. package/dist/meta/meta-origin.d.ts +0 -32
  156. package/dist/meta/meta-origin.d.ts.map +0 -1
  157. package/dist/meta/meta-origin.js +0 -55
  158. package/dist/meta/meta-origin.js.map +0 -1
  159. package/dist/meta/meta-relationship.d.ts +0 -11
  160. package/dist/meta/meta-relationship.d.ts.map +0 -1
  161. package/dist/meta/meta-relationship.js +0 -27
  162. package/dist/meta/meta-relationship.js.map +0 -1
  163. package/dist/meta/meta-root.d.ts +0 -12
  164. package/dist/meta/meta-root.d.ts.map +0 -1
  165. package/dist/meta/meta-root.js +0 -24
  166. package/dist/meta/meta-root.js.map +0 -1
  167. package/dist/meta/meta-source.d.ts +0 -18
  168. package/dist/meta/meta-source.d.ts.map +0 -1
  169. package/dist/meta/meta-source.js +0 -31
  170. package/dist/meta/meta-source.js.map +0 -1
  171. package/dist/meta/meta-validator.d.ts +0 -29
  172. package/dist/meta/meta-validator.d.ts.map +0 -1
  173. package/dist/meta/meta-validator.js +0 -49
  174. package/dist/meta/meta-validator.js.map +0 -1
  175. package/dist/meta/meta-view.d.ts +0 -4
  176. package/dist/meta/meta-view.d.ts.map +0 -1
  177. package/dist/meta/meta-view.js +0 -8
  178. package/dist/meta/meta-view.js.map +0 -1
  179. package/dist/persistence/db/db-attr-schemas.d.ts +0 -8
  180. package/dist/persistence/db/db-attr-schemas.d.ts.map +0 -1
  181. package/dist/persistence/db/db-attr-schemas.js +0 -28
  182. package/dist/persistence/db/db-attr-schemas.js.map +0 -1
  183. package/dist/persistence/db/db-schema.d.ts +0 -28
  184. package/dist/persistence/db/db-schema.d.ts.map +0 -1
  185. package/dist/persistence/db/db-schema.js +0 -62
  186. package/dist/persistence/db/db-schema.js.map +0 -1
  187. package/dist/persistence/origin/origin-schema.d.ts +0 -4
  188. package/dist/persistence/origin/origin-schema.d.ts.map +0 -1
  189. package/dist/persistence/origin/origin-schema.js +0 -63
  190. package/dist/persistence/origin/origin-schema.js.map +0 -1
  191. package/dist/persistence/source/source-schema.d.ts +0 -4
  192. package/dist/persistence/source/source-schema.d.ts.map +0 -1
  193. package/dist/persistence/source/source-schema.js +0 -98
  194. package/dist/persistence/source/source-schema.js.map +0 -1
  195. package/dist/presentation/layout/layout-schema.d.ts +0 -4
  196. package/dist/presentation/layout/layout-schema.d.ts.map +0 -1
  197. package/dist/presentation/layout/layout-schema.js +0 -47
  198. package/dist/presentation/layout/layout-schema.js.map +0 -1
  199. package/dist/presentation/ui/ui-schema.d.ts +0 -10
  200. package/dist/presentation/ui/ui-schema.d.ts.map +0 -1
  201. package/dist/presentation/ui/ui-schema.js +0 -41
  202. package/dist/presentation/ui/ui-schema.js.map +0 -1
  203. package/dist/presentation/view/view-schema.d.ts +0 -4
  204. package/dist/presentation/view/view-schema.d.ts.map +0 -1
  205. package/dist/presentation/view/view-schema.js +0 -15
  206. package/dist/presentation/view/view-schema.js.map +0 -1
  207. package/dist/template/prompt-schema.d.ts +0 -20
  208. package/dist/template/prompt-schema.d.ts.map +0 -1
  209. package/dist/template/prompt-schema.js +0 -70
  210. package/dist/template/prompt-schema.js.map +0 -1
  211. package/dist/template/template-schema.d.ts +0 -3
  212. package/dist/template/template-schema.d.ts.map +0 -1
  213. package/dist/template/template-schema.js +0 -181
  214. package/dist/template/template-schema.js.map +0 -1
@@ -28,6 +28,10 @@ export const IDENTITY_REFERENCE_ATTR_REFERENCES = "references";
28
28
  /** Identity-reference attr: physical-enforcement flag. Default true → hard FK constraint; false → logical-only reference. */
29
29
  export const IDENTITY_REFERENCE_ATTR_ENFORCE = "enforce";
30
30
 
31
+ // NOTE: physical RDB-only identity attrs (@constraintName on identity.reference;
32
+ // @orders / @where on identity.secondary) are NOT core — they are contributed by
33
+ // the db provider and declared in persistence/db/db-constants.ts.
34
+
31
35
  // ---------------------------------------------------------------------------
32
36
  // Identity generation strategies (values for IDENTITY_ATTR_GENERATION)
33
37
  // ---------------------------------------------------------------------------
@@ -13,6 +13,8 @@ export const IDENTITY_DEFINITION: ProviderDefinition = {
13
13
  "type": "identity",
14
14
  "subType": "primary",
15
15
  "description": "The primary key — one per entity; @fields names its column(s), @generation the value strategy.",
16
+ "maxOccurs": 1,
17
+ "defaultName": "primary",
16
18
  "children": [
17
19
  {
18
20
  "type": "attr",
@@ -136,7 +136,14 @@ export class MetaReferenceIdentity extends MetaIdentity {
136
136
 
137
137
  const targetName = this.targetEntity;
138
138
  if (targetName === undefined) return undefined;
139
- const targetObj = root.findObject(targetName);
139
+ // targetEntity may be package-qualified (FQN); findObject is keyed by bare
140
+ // name, so fall back to the bare suffix after the last "::". Mirrors the
141
+ // resolveTargetTable fix; without it a cross-package reference resolves no
142
+ // target and the PK column wrongly defaults to "id".
143
+ const targetObj = root.findObject(targetName)
144
+ ?? (targetName.includes("::")
145
+ ? root.findObject(targetName.slice(targetName.lastIndexOf("::") + 2))
146
+ : undefined);
140
147
  if (!targetObj) return undefined;
141
148
 
142
149
  const primary = targetObj.primaryIdentity();
@@ -3,7 +3,7 @@
3
3
  import { SUBTYPE_BASE } from "../../shared/base-types.js";
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
- // Validator subtypes (6)
6
+ // Validator subtypes (10)
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
9
  export const VALIDATOR_SUBTYPE_REQUIRED = "required";
@@ -11,6 +11,11 @@ export const VALIDATOR_SUBTYPE_LENGTH = "length";
11
11
  export const VALIDATOR_SUBTYPE_REGEX = "regex";
12
12
  export const VALIDATOR_SUBTYPE_NUMERIC = "numeric";
13
13
  export const VALIDATOR_SUBTYPE_ARRAY = "array";
14
+ // Cross-field validators — entity-scoped, reference sibling fields by name.
15
+ export const VALIDATOR_SUBTYPE_COMPARISON = "comparison";
16
+ export const VALIDATOR_SUBTYPE_REQUIRED_WHEN = "requiredWhen";
17
+ export const VALIDATOR_SUBTYPE_PRESENT_IFF = "presentIff";
18
+ export const VALIDATOR_SUBTYPE_AT_LEAST_ONE = "atLeastOne";
14
19
 
15
20
  export const VALIDATOR_SUBTYPES = [
16
21
  SUBTYPE_BASE,
@@ -19,6 +24,10 @@ export const VALIDATOR_SUBTYPES = [
19
24
  VALIDATOR_SUBTYPE_REGEX,
20
25
  VALIDATOR_SUBTYPE_NUMERIC,
21
26
  VALIDATOR_SUBTYPE_ARRAY,
27
+ VALIDATOR_SUBTYPE_COMPARISON,
28
+ VALIDATOR_SUBTYPE_REQUIRED_WHEN,
29
+ VALIDATOR_SUBTYPE_PRESENT_IFF,
30
+ VALIDATOR_SUBTYPE_AT_LEAST_ONE,
22
31
  ] as const;
23
32
  export type ValidatorSubType = (typeof VALIDATOR_SUBTYPES)[number];
24
33
 
@@ -29,3 +38,15 @@ export type ValidatorSubType = (typeof VALIDATOR_SUBTYPES)[number];
29
38
  export const VALIDATOR_ATTR_PATTERN = "pattern";
30
39
  export const VALIDATOR_ATTR_MIN = "min";
31
40
  export const VALIDATOR_ATTR_MAX = "max";
41
+ // Cross-field validator attrs (field references by name + operator/value).
42
+ export const VALIDATOR_ATTR_LEFT = "left";
43
+ export const VALIDATOR_ATTR_OP = "op";
44
+ export const VALIDATOR_ATTR_RIGHT = "right";
45
+ export const VALIDATOR_ATTR_FIELD = "field";
46
+ export const VALIDATOR_ATTR_WHEN = "when";
47
+ export const VALIDATOR_ATTR_EQUALS = "equals";
48
+ export const VALIDATOR_ATTR_FIELDS = "fields";
49
+
50
+ // Comparison operators (@op allowed values) → relational operator.
51
+ export const VALIDATOR_COMPARISON_OPS = ["gt", "gte", "lt", "lte", "ne", "eq"] as const;
52
+ export type ComparisonOp = (typeof VALIDATOR_COMPARISON_OPS)[number];
@@ -136,6 +136,127 @@ export const VALIDATOR_DEFINITION: ProviderDefinition = {
136
136
  "description": "Maximum allowed value (length, numeric value, or array element count depending on the validator subtype)."
137
137
  }
138
138
  ]
139
+ },
140
+ {
141
+ "type": "validator",
142
+ "subType": "comparison",
143
+ "description": "Cross-field ordering: requires two sibling fields of the owning entity stand in a relational order (@left @op @right), e.g. current_hp <= max_hp or expires_at > created_at. Entity-scoped; references fields by name. Backends derive the rule (CHECK constraint, cross-field assertion) — no raw expression is stored.",
144
+ "rules": "@left and @right must name fields of the owning entity. @op is one of gt/gte/lt/lte/ne/eq. The comparison is null-tolerant where the backend's relational operator is (SQL: a NULL operand yields no violation).",
145
+ "children": [
146
+ {
147
+ "type": "attr",
148
+ "subType": "string",
149
+ "name": "left",
150
+ "min": 1,
151
+ "max": 1,
152
+ "description": "Name of the left-hand field of the owning entity."
153
+ },
154
+ {
155
+ "type": "attr",
156
+ "subType": "string",
157
+ "name": "op",
158
+ "min": 1,
159
+ "max": 1,
160
+ "allowedValues": [
161
+ "gt",
162
+ "gte",
163
+ "lt",
164
+ "lte",
165
+ "ne",
166
+ "eq"
167
+ ],
168
+ "description": "Relational operator: gt (>), gte (>=), lt (<), lte (<=), ne (<>), eq (=)."
169
+ },
170
+ {
171
+ "type": "attr",
172
+ "subType": "string",
173
+ "name": "right",
174
+ "min": 1,
175
+ "max": 1,
176
+ "description": "Name of the right-hand field of the owning entity."
177
+ }
178
+ ]
179
+ },
180
+ {
181
+ "type": "validator",
182
+ "subType": "requiredWhen",
183
+ "description": "One-directional conditional presence: when the gating field (@when) equals @equals, the target field (@field) must be present (NOT NULL); otherwise @field is unconstrained. Mirrors JSON Schema dependentRequired / Rails validates_presence_of :x, if:. Entity-scoped; references fields by name.",
184
+ "rules": "@field and @when must name fields of the owning entity. @equals is the gating value, compared against @when's value (rendered per @when's field subtype — boolean true/false, enum/string literal, numeric literal).",
185
+ "children": [
186
+ {
187
+ "type": "attr",
188
+ "subType": "string",
189
+ "name": "field",
190
+ "min": 1,
191
+ "max": 1,
192
+ "description": "Name of the field that becomes required when the condition holds."
193
+ },
194
+ {
195
+ "type": "attr",
196
+ "subType": "string",
197
+ "name": "when",
198
+ "min": 1,
199
+ "max": 1,
200
+ "description": "Name of the gating field whose value triggers the requirement."
201
+ },
202
+ {
203
+ "type": "attr",
204
+ "subType": "string",
205
+ "name": "equals",
206
+ "min": 1,
207
+ "max": 1,
208
+ "description": "The gating value; when @when equals this, @field must be present."
209
+ }
210
+ ]
211
+ },
212
+ {
213
+ "type": "validator",
214
+ "subType": "presentIff",
215
+ "description": "Biconditional presence: the target field (@field) is present (NOT NULL) if and only if the gating field (@when) equals @equals. Models paired flag/companion-column invariants, e.g. used_at present iff is_used=true. Entity-scoped; references fields by name.",
216
+ "rules": "@field and @when must name fields of the owning entity. @equals is rendered per @when's field subtype. Stricter than requiredWhen — also forbids @field when the condition is false.",
217
+ "children": [
218
+ {
219
+ "type": "attr",
220
+ "subType": "string",
221
+ "name": "field",
222
+ "min": 1,
223
+ "max": 1,
224
+ "description": "Name of the field whose presence is governed by the condition."
225
+ },
226
+ {
227
+ "type": "attr",
228
+ "subType": "string",
229
+ "name": "when",
230
+ "min": 1,
231
+ "max": 1,
232
+ "description": "Name of the gating field."
233
+ },
234
+ {
235
+ "type": "attr",
236
+ "subType": "string",
237
+ "name": "equals",
238
+ "min": 1,
239
+ "max": 1,
240
+ "description": "The gating value; @field is present exactly when @when equals this."
241
+ }
242
+ ]
243
+ },
244
+ {
245
+ "type": "validator",
246
+ "subType": "atLeastOne",
247
+ "description": "Cardinality of presence: at least one of the named fields (@fields) must be present (NOT NULL). Entity-scoped; references fields by name (same @fields-by-name pattern as identity.*).",
248
+ "rules": "@fields names two or more fields of the owning entity. Satisfied when any one of them is non-null.",
249
+ "children": [
250
+ {
251
+ "type": "attr",
252
+ "subType": "string",
253
+ "name": "fields",
254
+ "isArray": true,
255
+ "min": 1,
256
+ "max": 1,
257
+ "description": "Names of the candidate fields; at least one must be present."
258
+ }
259
+ ]
139
260
  }
140
261
  ]
141
262
  };
package/src/core-types.ts CHANGED
@@ -84,8 +84,9 @@ import {
84
84
  IDENTITY_SUBTYPE_PRIMARY,
85
85
  IDENTITY_SUBTYPE_SECONDARY,
86
86
  IDENTITY_SUBTYPE_REFERENCE,
87
+ IDENTITY_REFERENCE_ATTR_REFERENCES,
87
88
  } from "./core/identity/identity-constants.js";
88
- import { RELATIONSHIP_SUBTYPES } from "./core/relationship/relationship-constants.js";
89
+ import { RELATIONSHIP_SUBTYPES, RELATIONSHIP_ATTR_OBJECT_REF } from "./core/relationship/relationship-constants.js";
89
90
  import { LAYOUT_SUBTYPES } from "./presentation/layout/layout-constants.js";
90
91
  import { SOURCE_SUBTYPES } from "./persistence/source/source-constants.js";
91
92
  import {
@@ -462,6 +463,30 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
462
463
  registry.register(relationshipDef);
463
464
  }
464
465
 
466
+ // Declare the core cross-references ON their TypeDefinitions, so the loader's
467
+ // registry-derived validation resolves them generically (a dangling target fails the
468
+ // load). Set on the concrete subtypes the parser produces. (Production moves these into
469
+ // the embedded spec/metamodel JSON once every port's spec reader carries the field.)
470
+ for (const subType of RELATIONSHIP_SUBTYPES) {
471
+ const def = registry.find(TYPE_RELATIONSHIP, subType);
472
+ if (def) {
473
+ def.references = [
474
+ { attr: RELATIONSHIP_ATTR_OBJECT_REF, targetType: TYPE_OBJECT, errorCode: "ERR_INVALID_RELATIONSHIP" },
475
+ ];
476
+ }
477
+ }
478
+ const idRefDef = registry.find(TYPE_IDENTITY, IDENTITY_SUBTYPE_REFERENCE);
479
+ if (idRefDef) {
480
+ idRefDef.references = [
481
+ {
482
+ attr: IDENTITY_REFERENCE_ATTR_REFERENCES,
483
+ targetType: TYPE_OBJECT,
484
+ dottedFieldPath: true,
485
+ errorCode: "ERR_INVALID_REFERENCE",
486
+ },
487
+ ];
488
+ }
489
+
465
490
  // Default subTypes for YAML authoring sugar: a bare `metadata:` / `object:`
466
491
  // key resolves to these. `metadata` has exactly one subtype (root) so the
467
492
  // default is unambiguous; `object` defaults to `entity`, the common case.
package/src/errors.ts CHANGED
@@ -30,6 +30,9 @@ export const ERROR_CODES = [
30
30
  // author-chosen (e.g. "id"), so the dotted by-name extends form can
31
31
  // address them.
32
32
  "ERR_IDENTITY_NAME_REQUIRED",
33
+ // A `type.subType` declared with `maxOccurs` (e.g. identity.primary,
34
+ // maxOccurs:1) appears more times than allowed under one parent.
35
+ "ERR_TOO_MANY_OCCURRENCES",
33
36
  // FR-024 — an identity.* on an object.projection lacks `extends`; a
34
37
  // projection identity is a pass-through of an entity identity.
35
38
  "ERR_PROJECTION_IDENTITY_NOT_EXTENDED",
@@ -95,6 +98,9 @@ export const ERROR_CODES = [
95
98
  // junction declaring two identity.reference children; @sourceRefField must match
96
99
  // one of them; M:N attrs are invalid on a 1:N (@cardinality:one / no @through).
97
100
  "ERR_INVALID_RELATIONSHIP",
101
+ // identity.reference @references names an FK target that does not resolve to any
102
+ // object in the loaded tree (a dangling cross-reference between metadata).
103
+ "ERR_INVALID_REFERENCE",
98
104
  "ERR_VAR_NOT_ON_PAYLOAD",
99
105
  "ERR_PARTIAL_UNRESOLVED",
100
106
  "ERR_REQUIRED_SLOT_UNUSED",
@@ -189,7 +195,10 @@ export type ErrorCode = (typeof ERROR_CODES)[number];
189
195
  * envelope's `jsonPath` and `files` and have been dropped — see CHANGELOG.
190
196
  */
191
197
  export class ParseError extends Error implements LoaderError {
192
- readonly code: ErrorCode;
198
+ // Widened from the core ErrorCode union so a DOWNSTREAM provider's validator can emit its
199
+ // own codes (LoaderError.code is `string`; the envelope compares codes as strings). Known
200
+ // core codes still surface in editor suggestions via the `string & {}` idiom.
201
+ readonly code: ErrorCode | (string & {});
193
202
  readonly source: ErrorSource;
194
203
  readonly suggestions?: string[];
195
204
  readonly fixture?: string;
@@ -198,7 +207,7 @@ export class ParseError extends Error implements LoaderError {
198
207
  constructor(
199
208
  message: string,
200
209
  opts: {
201
- code: ErrorCode;
210
+ code: ErrorCode | (string & {});
202
211
  source: ErrorSource;
203
212
  suggestions?: string[];
204
213
  fixture?: string;
@@ -18,6 +18,7 @@ import type { LoaderWarning } from "../source.js";
18
18
  import { codeSource, resolvedSource } from "../source.js";
19
19
  import { parseJson } from "../parser-json.js";
20
20
  import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships } from "./validation-passes.js";
21
+ import { runRegisteredValidation } from "./validation-registry.js";
21
22
  import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
22
23
  import { validateSourcePhysicalNames } from "../persistence/source/validate-source-physical-names.js";
23
24
  import { validateSourceParameterRef } from "../persistence/source/validate-source-parameter-ref.js";
@@ -25,6 +26,7 @@ import { validateFieldReadOnly } from "../core/field/validate-field-readonly.js"
25
26
  import { validateDiscriminator } from "../core/object/validate-discriminator.js";
26
27
  import { resolveDeferredSupers } from "../super-resolve.js";
27
28
  import { validateSubtypeRules } from "../subtype-rules.js";
29
+ import { validateMaxOccurs } from "../validate-max-occurs.js";
28
30
  import { validateIdentityPassthrough } from "../core/identity/validate-identity-passthrough.js";
29
31
  import { validateAttrSchema } from "../attr-schema-validate.js";
30
32
  import type { MetaDataFormat, MetaDataSource } from "./meta-data-source.js";
@@ -452,6 +454,10 @@ export class MetaDataLoader {
452
454
  errors.push(...ruleResult.errors);
453
455
  warnings.push(...ruleResult.warnings);
454
456
 
457
+ // maxOccurs enforcement (config-driven singleton constraint, e.g. one
458
+ // identity.primary per entity) — the safety complement to defaultName.
459
+ errors.push(...validateMaxOccurs(root, this._registry));
460
+
455
461
  // FR-024 B3 — projection identity pass-through + key correspondence
456
462
  // (ERR_PROJECTION_IDENTITY_NOT_EXTENDED / ERR_IDENTITY_KEY_MISMATCH).
457
463
  errors.push(...validateIdentityPassthrough(root));
@@ -485,6 +491,12 @@ export class MetaDataLoader {
485
491
  // are invalid on a 1:N relationship.
486
492
  errors.push(...validateRelationships(root));
487
493
 
494
+ // Phase 2 — validation DERIVED FROM THE TYPE REGISTRY: each node's TypeDefinition
495
+ // carries its reference descriptors + imperative validator, run as one recursive walk
496
+ // over a built-once symbol table. A downstream provider's custom type validates itself
497
+ // simply by being in this registry — no separate wiring.
498
+ errors.push(...runRegisteredValidation(root, this._registry));
499
+
488
500
  // template.* validation — @payloadRef resolves to a known object;
489
501
  // @requiredSlots are real fields on it (FR-004 Plan #3, T2).
490
502
  errors.push(...validateTemplatePayloadRefs(root));
@@ -66,7 +66,10 @@ import {
66
66
  FIELD_SUBTYPE_OBJECT,
67
67
  } from "../core/field/field-constants.js";
68
68
  import { FIELD_ATTR_DB_INDEXED } from "../persistence/db/db-constants.js";
69
- import { IDENTITY_ATTR_FIELDS } from "../core/identity/identity-constants.js";
69
+ import {
70
+ IDENTITY_ATTR_FIELDS,
71
+ IDENTITY_SUBTYPE_REFERENCE,
72
+ } from "../core/identity/identity-constants.js";
70
73
  import {
71
74
  ORIGIN_SUBTYPE_PASSTHROUGH,
72
75
  ORIGIN_SUBTYPE_AGGREGATE,
@@ -1199,6 +1202,10 @@ export function validateRelationships(root: MetaData): ParseError[] {
1199
1202
  const isMany = cardinality === CARDINALITY_MANY;
1200
1203
  const isM2M = hasThrough && isMany;
1201
1204
 
1205
+ // NOTE: @objectRef existence resolution moved to the validation registry
1206
+ // (defaultValidationRegistry → a declarative reference descriptor). The M:N
1207
+ // slim-vocabulary rules below stay here for now (Phase 3 migrates them too).
1208
+
1202
1209
  // Rule (d): M:N-only attrs on a non-M:N relationship.
1203
1210
  if (!isM2M) {
1204
1211
  if (hasThrough) {
@@ -1292,6 +1299,9 @@ export function validateRelationships(root: MetaData): ParseError[] {
1292
1299
  return errors;
1293
1300
  }
1294
1301
 
1302
+ // NOTE: identity.reference @references resolution moved to the validation registry
1303
+ // (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
1304
+
1295
1305
  function checkFilterClauses(
1296
1306
  filter: Record<string, unknown>,
1297
1307
  allow: Map<string, readonly string[]>,
@@ -0,0 +1,93 @@
1
+ // The validation walk — implementation of the contract in ../validation-types.ts.
2
+ //
3
+ // Validation is DERIVED FROM THE TYPE REGISTRY: each node's TypeDefinition carries its
4
+ // reference descriptors + imperative validator, so a downstream provider's type validates
5
+ // itself with no separate registry and no core changes. One recursive walk over a
6
+ // built-once symbol table: per node, apply declared references, invoke the type's
7
+ // validator, recurse. See docs/superpowers/specs/2026-06-19-metadata-validation-architecture-design.md.
8
+
9
+ import type { MetaData } from "../shared/meta-data.js";
10
+ import { ParseError } from "../errors.js";
11
+ import { refMatchesObject } from "../naming-refs.js";
12
+ import { TYPE_OBJECT } from "../shared/base-types.js";
13
+ import type { TypeRegistry } from "../registry.js";
14
+ import type { LoaderCode, SymbolTable, ValidationContext } from "../validation-types.js";
15
+
16
+ /** A symbol table of every top-level object, built once per load (the binder analogue). */
17
+ class SymbolTableImpl implements SymbolTable {
18
+ private readonly byRef = new Map<string, MetaData>();
19
+
20
+ static build(root: MetaData): SymbolTableImpl {
21
+ const t = new SymbolTableImpl();
22
+ for (const child of root.ownChildren()) {
23
+ if (child.type !== TYPE_OBJECT) continue;
24
+ if (child.name) t.byRef.set(child.name, child);
25
+ t.byRef.set(child.fqn(), child);
26
+ t.byRef.set(child.resolutionKey(), child);
27
+ }
28
+ return t;
29
+ }
30
+
31
+ resolveObject(ref: string): MetaData | undefined {
32
+ const hit = this.byRef.get(ref);
33
+ if (hit) return hit;
34
+ for (const obj of this.byRef.values()) {
35
+ if (refMatchesObject(obj, ref)) return obj;
36
+ }
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ class ValidationContextImpl implements ValidationContext {
42
+ readonly errors: ParseError[] = [];
43
+ constructor(readonly symbols: SymbolTable) {}
44
+ error(code: LoaderCode, node: MetaData, message: string): void {
45
+ this.errors.push(new ParseError(message, { code, source: node.source }));
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Run validation derived from `registry` over the tree. Per node: look up its
51
+ * TypeDefinition, apply its declared reference descriptors (resolve against the symbol
52
+ * table), invoke its imperative validator, then recurse into own children.
53
+ */
54
+ export function runRegisteredValidation(root: MetaData, registry: TypeRegistry): ParseError[] {
55
+ const ctx = new ValidationContextImpl(SymbolTableImpl.build(root));
56
+ walk(root);
57
+ return ctx.errors;
58
+
59
+ function walk(node: MetaData): void {
60
+ const def = registry.find(node.type, node.subType);
61
+ if (def) {
62
+ for (const desc of def.references ?? []) {
63
+ const raw = node.ownAttr(desc.attr);
64
+ if (typeof raw !== "string" || raw === "") continue; // absence is the required-attr pass's job
65
+ const entityRef = desc.dottedFieldPath ? (raw.split(".")[0] ?? raw) : raw;
66
+ const target = ctx.symbols.resolveObject(entityRef);
67
+ // Qualify the node name with its owning entity (e.g. "Order.items") so the error is
68
+ // locatable from the message alone, not just the source envelope.
69
+ const qname = node.parent?.name ? `${node.parent.name}.${node.name}` : node.name;
70
+ if (!target) {
71
+ ctx.error(
72
+ desc.errorCode,
73
+ node,
74
+ `${node.type}.${node.subType} "${qname}" @${desc.attr} "${raw}" does not resolve to an object.`,
75
+ );
76
+ } else if (
77
+ target.type !== desc.targetType ||
78
+ (desc.targetSubType !== undefined && target.subType !== desc.targetSubType)
79
+ ) {
80
+ const want = desc.targetSubType ? `${desc.targetType}.${desc.targetSubType}` : desc.targetType;
81
+ ctx.error(
82
+ desc.errorCode,
83
+ node,
84
+ `${node.type}.${node.subType} "${qname}" @${desc.attr} "${raw}" resolves to ` +
85
+ `${target.type}.${target.subType}, not a ${want}.`,
86
+ );
87
+ }
88
+ }
89
+ def.validate?.(node, ctx);
90
+ }
91
+ for (const child of node.ownChildren()) walk(child);
92
+ }
93
+ }
@@ -519,10 +519,19 @@ function parseNodeFresh(
519
519
 
520
520
  // --- Determine name ---
521
521
  const rawName = nodeData[RESERVED_KEY_NAME];
522
- const name = typeof rawName === "string" ? rawName : "";
522
+ let name = typeof rawName === "string" ? rawName : "";
523
523
 
524
524
  // --- Create the model ---
525
525
  const def = registry.find(type, subType)!;
526
+ // Config-driven default name for a SINGLETON child type: when the node is
527
+ // declared with no name and its type definition is `maxOccurs: 1` with a
528
+ // `defaultName`, assign it (e.g. identity.primary → "primary"). Safe by
529
+ // construction — maxOccurs===1 guarantees no sibling can collide — and keeps
530
+ // the one-and-only node addressable. Multi-cardinality types carry no
531
+ // defaultName, so they still require an explicit name (FR-024).
532
+ if (name === "" && def.maxOccurs === 1 && def.defaultName !== undefined) {
533
+ name = def.defaultName;
534
+ }
526
535
  const model = def.factory(def.typeId, name);
527
536
  // FR5a — stamp the source provenance envelope using the parser's current
528
537
  // JSONPath stack + source id. setSource happens BEFORE freeze (the parser
@@ -10,6 +10,22 @@ export const FIELD_ATTR_COLUMN = "column";
10
10
  /** When true, suppress the @filterable-without-index Loader warning (Project D drift check). */
11
11
  export const FIELD_ATTR_DB_INDEXED = "db.indexed";
12
12
 
13
+ // --- Physical RDB index/constraint attrs the db provider adds to identity.* ---
14
+ // These EXTEND core identity subtypes (via registry.extend) rather than living on
15
+ // core, because they are pure physical-storage concerns (index ordering, partial-
16
+ // index predicate, FK constraint naming) with no logical-model meaning.
17
+
18
+ /** identity.secondary: per-field index-key sort direction array ('asc' | 'desc'), positional to @fields. Drives DESC-ordered index keys. */
19
+ export const IDENTITY_ATTR_ORDERS = "orders";
20
+ /** identity.secondary: a partial-index predicate (raw SQL). When set, the index covers only matching rows. */
21
+ export const IDENTITY_ATTR_WHERE = "where";
22
+ /** identity.secondary: a raw key EXPRESSION for a functional/expression index (e.g. "lower(email)"); used instead of @fields. */
23
+ export const IDENTITY_ATTR_EXPR = "expr";
24
+ /** identity.secondary: index access method (e.g. "gin", "gist"); default "btree", which is not rendered. */
25
+ export const IDENTITY_ATTR_USING = "using";
26
+ /** identity.reference: physical FK constraint-name override. Absent → the auto-derived `<table>_<firstFkColumn>_fk`. */
27
+ export const IDENTITY_ATTR_CONSTRAINT_NAME = "constraintName";
28
+
13
29
  /**
14
30
  * R6 Plan 2b: `@dbColumnType` — physical DB column-type override on a field.
15
31
  * Selects the DB column type WITHOUT changing the logical field type or its
@@ -172,6 +172,63 @@ export const DB_DEFINITION: ProviderDefinition = {
172
172
  "description": "FR-015: name or FQN of an object.value describing the input shape of this source's callable interface. Permitted on @kind: \"storedProc\" / \"tableFunction\"; rejected on non-callable kinds (table / view / materializedView). Field children of the referenced object.value become the call-site parameter list in declaration order. Symmetric with template.@payloadRef in FR-004 — the typed-input pattern reuses object.value rather than minting a new parameter.* node type."
173
173
  }
174
174
  ]
175
+ },
176
+ {
177
+ "type": "identity",
178
+ "subType": "secondary",
179
+ "children": [
180
+ {
181
+ "type": "attr",
182
+ "subType": "string",
183
+ "name": "orders",
184
+ "isArray": true,
185
+ "min": 0,
186
+ "max": 1,
187
+ "allowedValues": [
188
+ "asc",
189
+ "desc"
190
+ ],
191
+ "description": "Physical index-key sort direction, positional to @fields ('asc' | 'desc'). Omit for all-ascending (the default); a shorter array leaves trailing keys ascending. Drives DESC-ordered index keys (e.g. a recency index on a timestamp). RDB-physical — contributed by the db provider, not core identity."
192
+ },
193
+ {
194
+ "type": "attr",
195
+ "subType": "string",
196
+ "name": "where",
197
+ "min": 0,
198
+ "max": 1,
199
+ "description": "Partial-index predicate (raw SQL, e.g. \"delivered_at IS NULL\"). When set, the index covers only rows matching the predicate — smaller and cheaper for queries that always filter on it. Absent = a full index over every row. RDB-physical — contributed by the db provider."
200
+ },
201
+ {
202
+ "type": "attr",
203
+ "subType": "string",
204
+ "name": "expr",
205
+ "min": 0,
206
+ "max": 1,
207
+ "description": "Raw key EXPRESSION for a functional/expression index (e.g. \"lower(email)\"). Used INSTEAD of @fields — the index key is the expression rather than plain columns. RDB-physical — contributed by the db provider."
208
+ },
209
+ {
210
+ "type": "attr",
211
+ "subType": "string",
212
+ "name": "using",
213
+ "min": 0,
214
+ "max": 1,
215
+ "description": "Index access method (e.g. \"gin\", \"gist\", \"hash\"); default \"btree\" (not rendered). Pair with @expr for e.g. a GIN index over an array/jsonb expression. RDB-physical — contributed by the db provider."
216
+ }
217
+ ]
218
+ },
219
+ {
220
+ "type": "identity",
221
+ "subType": "reference",
222
+ "children": [
223
+ {
224
+ "type": "attr",
225
+ "subType": "string",
226
+ "name": "constraintName",
227
+ "min": 0,
228
+ "max": 1,
229
+ "description": "Physical foreign-key constraint name override. Absent → the backend's auto-derived default (e.g. `<table>_<firstFkColumn>_fk`). Lets a model adopt an existing database whose FK constraints follow a different naming convention without a destructive rename. RDB-physical — contributed by the db provider."
230
+ }
231
+ ]
175
232
  }
176
233
  ]
177
234
  };
@@ -97,6 +97,21 @@ export interface TypeDef {
97
97
  * registered output change) under S0.
98
98
  */
99
99
  extendsBase?: boolean;
100
+ /**
101
+ * Max number of children of this `type.subType` permitted under one parent.
102
+ * `1` marks a singleton child (e.g. `identity.primary` — one per entity). The
103
+ * loader enforces it. A singleton is the ONLY place a static `defaultName` is
104
+ * safe (no sibling can collide), so `defaultName` is honored only when
105
+ * `maxOccurs === 1`. Absent = unbounded.
106
+ */
107
+ maxOccurs?: number;
108
+ /**
109
+ * Author-omittable name for a singleton child. When a node of this type is
110
+ * declared with no `name` AND `maxOccurs === 1`, the loader assigns
111
+ * `defaultName` (e.g. `identity.primary` → `"primary"`). Keeps the one-and-only
112
+ * node addressable (`Entity.primary`) without forcing a hand-written name.
113
+ */
114
+ defaultName?: string;
100
115
  }
101
116
 
102
117
  /**
@@ -240,6 +255,8 @@ export function defineProviderFromData(
240
255
  ...(t.rules !== undefined ? { rules: t.rules } : {}),
241
256
  ...(t.example !== undefined ? { example: t.example } : {}),
242
257
  ...(t.whenToUse !== undefined ? { whenToUse: t.whenToUse } : {}),
258
+ ...(t.maxOccurs !== undefined ? { maxOccurs: t.maxOccurs } : {}),
259
+ ...(t.defaultName !== undefined ? { defaultName: t.defaultName } : {}),
243
260
  };
244
261
  });
245
262
  }
package/src/registry.ts CHANGED
@@ -3,6 +3,7 @@ import { SUBTYPE_BASE, TYPE_ATTR } from "./shared/base-types.js";
3
3
  import { CHILD_RULE_WILDCARD } from "./shared/structural.js";
4
4
  import { type AttrSubType } from "./core/attr/attr-constants.js";
5
5
  import type { DataType } from "./data-type.js";
6
+ import type { ReferenceDescriptor, NodeValidator } from "./validation-types.js";
6
7
  import { MetaModelError } from "./errors.js";
7
8
 
8
9
  export class TypeId {
@@ -107,6 +108,21 @@ export interface TypeDefinition {
107
108
  example?: string;
108
109
  /** FR-033 — guidance on when to reach for this type/subType. Optional. */
109
110
  whenToUse?: string;
111
+ /** Max children of this type.subType per parent (`1` = singleton). Loader-enforced. */
112
+ maxOccurs?: number;
113
+ /** Default name for a singleton (`maxOccurs===1`) child declared with no name. */
114
+ defaultName?: string;
115
+ /**
116
+ * Cross-references this node's attrs declare — resolved generically against the symbol
117
+ * table (a dangling/kind-mismatched target fails the load). Carried by the type's
118
+ * registration, so a downstream provider's references validate with no core changes.
119
+ */
120
+ references?: ReferenceDescriptor[];
121
+ /**
122
+ * The type's imperative validator (logic config can't express). Invoked by the recursive
123
+ * validation walk. Owned by the provider that owns the type.
124
+ */
125
+ validate?: NodeValidator;
110
126
  }
111
127
 
112
128
  export class TypeRegistry {