@metaobjectsdev/metadata 0.5.0 → 0.6.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 (168) hide show
  1. package/dist/attr-schema-validate.d.ts.map +1 -1
  2. package/dist/attr-schema-validate.js +66 -11
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/documentation/doc-constants.d.ts +11 -0
  5. package/dist/core/documentation/doc-constants.d.ts.map +1 -0
  6. package/dist/core/documentation/doc-constants.js +20 -0
  7. package/dist/core/documentation/doc-constants.js.map +1 -0
  8. package/dist/core/documentation/doc-provider.d.ts +3 -0
  9. package/dist/core/documentation/doc-provider.d.ts.map +1 -0
  10. package/dist/core/documentation/doc-provider.js +10 -0
  11. package/dist/core/documentation/doc-provider.js.map +1 -0
  12. package/dist/core/documentation/doc-schema.d.ts +8 -0
  13. package/dist/core/documentation/doc-schema.d.ts.map +1 -0
  14. package/dist/core/documentation/doc-schema.js +53 -0
  15. package/dist/core/documentation/doc-schema.js.map +1 -0
  16. package/dist/core/field/field-constants.d.ts +25 -1
  17. package/dist/core/field/field-constants.d.ts.map +1 -1
  18. package/dist/core/field/field-constants.js +32 -1
  19. package/dist/core/field/field-constants.js.map +1 -1
  20. package/dist/core/field/field-schema.d.ts +2 -0
  21. package/dist/core/field/field-schema.d.ts.map +1 -1
  22. package/dist/core/field/field-schema.js +20 -2
  23. package/dist/core/field/field-schema.js.map +1 -1
  24. package/dist/core/field/meta-field.d.ts +2 -1
  25. package/dist/core/field/meta-field.d.ts.map +1 -1
  26. package/dist/core/field/meta-field.js +6 -4
  27. package/dist/core/field/meta-field.js.map +1 -1
  28. package/dist/core/object/meta-object.d.ts.map +1 -1
  29. package/dist/core/object/meta-object.js +6 -5
  30. package/dist/core/object/meta-object.js.map +1 -1
  31. package/dist/core/parser-yaml.d.ts.map +1 -1
  32. package/dist/core/parser-yaml.js +9 -2
  33. package/dist/core/parser-yaml.js.map +1 -1
  34. package/dist/core/relationship/meta-relationship.d.ts +4 -0
  35. package/dist/core/relationship/meta-relationship.d.ts.map +1 -1
  36. package/dist/core/relationship/meta-relationship.js +11 -1
  37. package/dist/core/relationship/meta-relationship.js.map +1 -1
  38. package/dist/core/relationship/relationship-constants.d.ts +9 -0
  39. package/dist/core/relationship/relationship-constants.d.ts.map +1 -1
  40. package/dist/core/relationship/relationship-constants.js +15 -0
  41. package/dist/core/relationship/relationship-constants.js.map +1 -1
  42. package/dist/core/relationship/relationship-schema.d.ts.map +1 -1
  43. package/dist/core/relationship/relationship-schema.js +15 -1
  44. package/dist/core/relationship/relationship-schema.js.map +1 -1
  45. package/dist/core/yaml-desugar.d.ts +8 -1
  46. package/dist/core/yaml-desugar.d.ts.map +1 -1
  47. package/dist/core/yaml-desugar.js +166 -15
  48. package/dist/core/yaml-desugar.js.map +1 -1
  49. package/dist/core-types.d.ts.map +1 -1
  50. package/dist/core-types.js +28 -11
  51. package/dist/core-types.js.map +1 -1
  52. package/dist/errors.d.ts +1 -1
  53. package/dist/errors.d.ts.map +1 -1
  54. package/dist/errors.js +21 -0
  55. package/dist/errors.js.map +1 -1
  56. package/dist/index.d.ts +6 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +7 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  61. package/dist/loader/meta-data-loader.js +12 -1
  62. package/dist/loader/meta-data-loader.js.map +1 -1
  63. package/dist/loader/validation-passes.d.ts +2 -0
  64. package/dist/loader/validation-passes.d.ts.map +1 -1
  65. package/dist/loader/validation-passes.js +69 -3
  66. package/dist/loader/validation-passes.js.map +1 -1
  67. package/dist/naming.d.ts +3 -2
  68. package/dist/naming.d.ts.map +1 -1
  69. package/dist/naming.js +14 -12
  70. package/dist/naming.js.map +1 -1
  71. package/dist/parser-core.d.ts.map +1 -1
  72. package/dist/parser-core.js +91 -48
  73. package/dist/parser-core.js.map +1 -1
  74. package/dist/persistence/db/db-constants.d.ts +2 -2
  75. package/dist/persistence/db/db-constants.d.ts.map +1 -1
  76. package/dist/persistence/db/db-constants.js +2 -2
  77. package/dist/persistence/db/db-constants.js.map +1 -1
  78. package/dist/persistence/db/db-provider.d.ts.map +1 -1
  79. package/dist/persistence/db/db-provider.js +10 -13
  80. package/dist/persistence/db/db-provider.js.map +1 -1
  81. package/dist/persistence/db/db-schema.d.ts +2 -4
  82. package/dist/persistence/db/db-schema.d.ts.map +1 -1
  83. package/dist/persistence/db/db-schema.js +5 -13
  84. package/dist/persistence/db/db-schema.js.map +1 -1
  85. package/dist/persistence/origin/meta-origin.d.ts +10 -0
  86. package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
  87. package/dist/persistence/origin/meta-origin.js +14 -1
  88. package/dist/persistence/origin/meta-origin.js.map +1 -1
  89. package/dist/persistence/origin/origin-constants.d.ts +3 -1
  90. package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
  91. package/dist/persistence/origin/origin-constants.js +6 -0
  92. package/dist/persistence/origin/origin-constants.js.map +1 -1
  93. package/dist/persistence/origin/origin-schema.d.ts +1 -1
  94. package/dist/persistence/origin/origin-schema.d.ts.map +1 -1
  95. package/dist/persistence/origin/origin-schema.js +12 -2
  96. package/dist/persistence/origin/origin-schema.js.map +1 -1
  97. package/dist/persistence/source/meta-source.d.ts +11 -9
  98. package/dist/persistence/source/meta-source.d.ts.map +1 -1
  99. package/dist/persistence/source/meta-source.js +23 -15
  100. package/dist/persistence/source/meta-source.js.map +1 -1
  101. package/dist/persistence/source/source-constants.d.ts +32 -11
  102. package/dist/persistence/source/source-constants.d.ts.map +1 -1
  103. package/dist/persistence/source/source-constants.js +55 -16
  104. package/dist/persistence/source/source-constants.js.map +1 -1
  105. package/dist/persistence/source/source-schema.d.ts +4 -0
  106. package/dist/persistence/source/source-schema.d.ts.map +1 -0
  107. package/dist/persistence/source/source-schema.js +37 -0
  108. package/dist/persistence/source/source-schema.js.map +1 -0
  109. package/dist/persistence/source/validate-source-roles.d.ts +12 -0
  110. package/dist/persistence/source/validate-source-roles.d.ts.map +1 -0
  111. package/dist/persistence/source/validate-source-roles.js +38 -0
  112. package/dist/persistence/source/validate-source-roles.js.map +1 -0
  113. package/dist/registry.d.ts +13 -0
  114. package/dist/registry.d.ts.map +1 -1
  115. package/dist/registry.js +26 -0
  116. package/dist/registry.js.map +1 -1
  117. package/dist/shared/base-types.d.ts +2 -1
  118. package/dist/shared/base-types.d.ts.map +1 -1
  119. package/dist/shared/base-types.js +5 -2
  120. package/dist/shared/base-types.js.map +1 -1
  121. package/dist/template/meta-template.d.ts +13 -0
  122. package/dist/template/meta-template.d.ts.map +1 -0
  123. package/dist/template/meta-template.js +13 -0
  124. package/dist/template/meta-template.js.map +1 -0
  125. package/dist/template/template-constants.d.ts +17 -0
  126. package/dist/template/template-constants.d.ts.map +1 -0
  127. package/dist/template/template-constants.js +46 -0
  128. package/dist/template/template-constants.js.map +1 -0
  129. package/dist/template/template-schema.d.ts +3 -0
  130. package/dist/template/template-schema.d.ts.map +1 -0
  131. package/dist/template/template-schema.js +84 -0
  132. package/dist/template/template-schema.js.map +1 -0
  133. package/package.json +1 -1
  134. package/src/attr-schema-validate.ts +89 -9
  135. package/src/core/documentation/doc-constants.ts +22 -0
  136. package/src/core/documentation/doc-provider.ts +12 -0
  137. package/src/core/documentation/doc-schema.ts +64 -0
  138. package/src/core/field/field-constants.ts +41 -1
  139. package/src/core/field/field-schema.ts +25 -0
  140. package/src/core/field/meta-field.ts +6 -3
  141. package/src/core/object/meta-object.ts +6 -6
  142. package/src/core/parser-yaml.ts +11 -3
  143. package/src/core/relationship/meta-relationship.ts +14 -0
  144. package/src/core/relationship/relationship-constants.ts +20 -0
  145. package/src/core/relationship/relationship-schema.ts +18 -0
  146. package/src/core/yaml-desugar.ts +206 -24
  147. package/src/core-types.ts +31 -9
  148. package/src/errors.ts +21 -0
  149. package/src/index.ts +7 -0
  150. package/src/loader/meta-data-loader.ts +15 -1
  151. package/src/loader/validation-passes.ts +98 -1
  152. package/src/naming.ts +15 -13
  153. package/src/parser-core.ts +119 -73
  154. package/src/persistence/db/db-constants.ts +2 -2
  155. package/src/persistence/db/db-provider.ts +10 -16
  156. package/src/persistence/db/db-schema.ts +5 -15
  157. package/src/persistence/origin/meta-origin.ts +15 -0
  158. package/src/persistence/origin/origin-constants.ts +7 -0
  159. package/src/persistence/origin/origin-schema.ts +15 -1
  160. package/src/persistence/source/meta-source.ts +30 -17
  161. package/src/persistence/source/source-constants.ts +66 -17
  162. package/src/persistence/source/source-schema.ts +54 -0
  163. package/src/persistence/source/validate-source-roles.ts +53 -0
  164. package/src/registry.ts +31 -0
  165. package/src/shared/base-types.ts +5 -2
  166. package/src/template/meta-template.ts +12 -0
  167. package/src/template/template-constants.ts +53 -0
  168. package/src/template/template-schema.ts +106 -0
@@ -36,3 +36,23 @@ export const CARDINALITY_MANY = "many";
36
36
 
37
37
  export const CARDINALITY_VALUES = [CARDINALITY_ONE, CARDINALITY_MANY] as const;
38
38
  export type CardinalityValue = (typeof CARDINALITY_VALUES)[number];
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Referential action attrs (@onDelete / @onUpdate)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export const RELATIONSHIP_ATTR_ON_DELETE = "onDelete";
45
+ export const RELATIONSHIP_ATTR_ON_UPDATE = "onUpdate";
46
+
47
+ /** Referential actions — the canonical cross-language set (kebab-case, no setDefault).
48
+ * MUST equal migrate-ts's FkAction union (server/typescript/packages/migrate-ts/src/types.ts). */
49
+ export const REFERENTIAL_ACTIONS = ["cascade", "set-null", "restrict", "no-action"] as const;
50
+ export type ReferentialAction = (typeof REFERENTIAL_ACTIONS)[number];
51
+
52
+ /** Default @onDelete per relationship subtype (rollout decided defaults). */
53
+ export const ON_DELETE_DEFAULT_BY_SUBTYPE: Readonly<Record<string, ReferentialAction>> = {
54
+ [RELATIONSHIP_SUBTYPE_COMPOSITION]: "cascade",
55
+ [RELATIONSHIP_SUBTYPE_AGGREGATION]: "set-null",
56
+ [RELATIONSHIP_SUBTYPE_ASSOCIATION]: "restrict",
57
+ };
58
+ export const ON_UPDATE_DEFAULT: ReferentialAction = "cascade";
@@ -11,6 +11,9 @@ import {
11
11
  RELATIONSHIP_ATTR_OBJECT_REF,
12
12
  RELATIONSHIP_ATTR_JOIN_ENTITY,
13
13
  RELATIONSHIP_ATTR_JOIN_FIELDS,
14
+ RELATIONSHIP_ATTR_ON_DELETE,
15
+ RELATIONSHIP_ATTR_ON_UPDATE,
16
+ REFERENTIAL_ACTIONS,
14
17
  } from "./relationship-constants.js";
15
18
 
16
19
  /** Attrs common to every relationship subtype. */
@@ -46,4 +49,19 @@ export const relationshipAttrs: AttrSchema[] = [
46
49
  required: false,
47
50
  description: "Join-table column names for N:M relationships.",
48
51
  },
52
+ {
53
+ name: RELATIONSHIP_ATTR_ON_DELETE,
54
+ valueType: ATTR_SUBTYPE_STRING,
55
+ required: false,
56
+ allowedValues: [...REFERENTIAL_ACTIONS],
57
+ description:
58
+ "Referential action on parent delete. Default derives from subtype (composition→cascade, aggregation→set-null, association→restrict).",
59
+ },
60
+ {
61
+ name: RELATIONSHIP_ATTR_ON_UPDATE,
62
+ valueType: ATTR_SUBTYPE_STRING,
63
+ required: false,
64
+ allowedValues: [...REFERENTIAL_ACTIONS],
65
+ description: "Referential action on key update. Default cascade.",
66
+ },
49
67
  ];
@@ -2,37 +2,71 @@
2
2
  //
3
3
  // desugar() turns the sugared authoring object (from yaml.parse) into the
4
4
  // canonical-shaped object that buildTree (parser-core.ts) consumes. It applies
5
- // the four format-spec sugar rules:
5
+ // the five format-spec sugar rules (ADR-0006):
6
6
  // 1. Fused key, subType omittable — a bare `type` key resolves to the type's
7
7
  // registry default subType.
8
8
  // 2. Scalar-or-map body — a scalar body becomes { name: <scalar> }.
9
9
  // 3. Omit empties — absent keys stay absent; the desugar invents nothing.
10
10
  // 4. `[]` arrays — a trailing `[]` on the key strips to isArray: true.
11
+ // 5. Sigil-free attributes (ADR-0006 D1) — every body key not in
12
+ // RESERVED_KEYS is treated as an inline attribute and re-prefixed with `@`
13
+ // when lowering to canonical JSON. Keys already prefixed with `@` are
14
+ // left as-is (backward-compat). Reserved structural keywords (name,
15
+ // package, extends, abstract, overlay, isArray, children, value) stay
16
+ // bare. Note: an already-`@`-prefixed reserved word remains an error
17
+ // downstream — parser-core's ERR_RESERVED_ATTR check fires.
11
18
  //
12
- // Pure and total: it never throws. Malformed fragments are collected as error
13
- // strings and a safe placeholder is substituted so buildTree does not
14
- // double-report.
19
+ // In addition, the desugar runs the ADR-0006 D2 type-coercion guard: for every
20
+ // inline attr whose owning (type, subType) has a declared schema, if the raw
21
+ // JS value's type does not match the declared valueType AND the JS value is
22
+ // one of YAML 1.2's silently coerced shapes (boolean/number/null), the
23
+ // desugar collects an ERR_YAML_COERCION error telling the author to quote the
24
+ // value. The check protects against the classic YAML footgun (e.g. an unquoted
25
+ // `column: TRUE` becoming the boolean `true` instead of the string "TRUE").
26
+ //
27
+ // Pure and total: it never throws. Malformed fragments are collected as
28
+ // CollectedError entries (a string message + optional stable error code) and a
29
+ // safe placeholder is substituted so buildTree does not double-report.
15
30
 
16
- import type { TypeRegistry } from "../registry.js";
31
+ import type { TypeRegistry, AttrSchema } from "../registry.js";
32
+ import type { ErrorCode } from "../errors.js";
17
33
  import {
34
+ ATTR_PREFIX,
35
+ RESERVED_KEYS,
18
36
  RESERVED_KEY_CHILDREN,
19
37
  RESERVED_KEY_NAME,
20
38
  RESERVED_KEY_IS_ARRAY,
21
39
  TYPE_SUBTYPE_SEPARATOR,
22
40
  } from "../shared/structural.js";
41
+ import {
42
+ ATTR_SUBTYPE_STRING,
43
+ ATTR_SUBTYPE_CLASS,
44
+ ATTR_SUBTYPE_INT,
45
+ ATTR_SUBTYPE_LONG,
46
+ ATTR_SUBTYPE_DOUBLE,
47
+ ATTR_SUBTYPE_BOOLEAN,
48
+ ATTR_SUBTYPE_STRINGARRAY,
49
+ } from "./attr/attr-constants.js";
23
50
 
24
51
  const ARRAY_SUFFIX = "[]";
25
52
 
53
+ /** A collected desugar problem — message plus optional stable error code. */
54
+ export interface CollectedError {
55
+ message: string;
56
+ /** Optional stable code; absent values map to ERR_MALFORMED_YAML in parser-yaml.ts. */
57
+ code?: ErrorCode;
58
+ }
59
+
26
60
  export interface DesugarResult {
27
61
  /** The canonical-shaped object; `{}` when the document was unusable. */
28
62
  canonical: Record<string, unknown>;
29
63
  /** Collected desugar problems (never thrown). */
30
- errors: string[];
64
+ errors: CollectedError[];
31
65
  }
32
66
 
33
67
  /** Desugar a parsed-YAML authoring document into a canonical-shaped object. */
34
68
  export function desugar(input: unknown, registry: TypeRegistry): DesugarResult {
35
- const errors: string[] = [];
69
+ const errors: CollectedError[] = [];
36
70
  const node = desugarNode(input, registry, errors, "<root>");
37
71
  return { canonical: node ?? {}, errors };
38
72
  }
@@ -43,21 +77,22 @@ export function desugar(input: unknown, registry: TypeRegistry): DesugarResult {
43
77
  function desugarNode(
44
78
  input: unknown,
45
79
  registry: TypeRegistry,
46
- errors: string[],
80
+ errors: CollectedError[],
47
81
  path: string,
48
82
  ): Record<string, unknown> | undefined {
49
83
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
50
- errors.push(`Node at ${path} must be a mapping with one type key`);
84
+ errors.push({ message: `Node at ${path} must be a mapping with one type key` });
51
85
  return undefined;
52
86
  }
53
87
 
54
88
  const entries = Object.keys(input as Record<string, unknown>);
55
89
  if (entries.length !== 1) {
56
- errors.push(
57
- `Node at ${path} must have exactly one type key (found: ${
58
- entries.length === 0 ? "none" : entries.join(", ")
59
- })`,
60
- );
90
+ errors.push({
91
+ message:
92
+ `Node at ${path} must have exactly one type key (found: ${
93
+ entries.length === 0 ? "none" : entries.join(", ")
94
+ })`,
95
+ });
61
96
  return undefined;
62
97
  }
63
98
 
@@ -76,7 +111,7 @@ function desugarNode(
76
111
  const canonicalKey = resolveKey(key, registry, errors, path);
77
112
 
78
113
  // Rule 2: a scalar body → { name: <scalar> }.
79
- const body = desugarBody(rawBody, errors, path);
114
+ const body = desugarBody(rawBody, registry, canonicalKey, errors, path);
80
115
 
81
116
  // Rule 4 (cont.): stamp isArray onto the canonical body.
82
117
  if (isArray) body[RESERVED_KEY_IS_ARRAY] = true;
@@ -103,25 +138,36 @@ function desugarNode(
103
138
  function resolveKey(
104
139
  key: string,
105
140
  registry: TypeRegistry,
106
- errors: string[],
141
+ errors: CollectedError[],
107
142
  path: string,
108
143
  ): string {
109
144
  if (key.includes(TYPE_SUBTYPE_SEPARATOR)) return key; // already fused
110
145
  const subType = registry.defaultSubTypeOf(key);
111
146
  if (subType === undefined) {
112
- errors.push(
113
- `Cannot resolve subType for bare type key '${key}' at ${path} — ` +
147
+ errors.push({
148
+ message:
149
+ `Cannot resolve subType for bare type key '${key}' at ${path} — ` +
114
150
  `type '${key}' has no default subType; write the full 'type.subType'`,
115
- );
151
+ });
116
152
  return key; // pass through; buildTree reports the unknown type
117
153
  }
118
154
  return `${key}${TYPE_SUBTYPE_SEPARATOR}${subType}`;
119
155
  }
120
156
 
121
- // Rule 2 — normalize a node body into a canonical mapping.
157
+ // Rule 2 + 5 — normalize a node body into a canonical mapping. Reserved
158
+ // structural keys stay bare; every other key is treated as an inline attribute
159
+ // and `@`-prefixed (Rule 5 / ADR-0006 D1). Keys already starting with `@` are
160
+ // kept as-authored so the awkward "@column: foo" form remains accepted.
161
+ //
162
+ // Also runs the D2 type-coercion guard: for each inline attr that the owning
163
+ // (type, subType) declares with a typed `valueType`, if the raw JS value's
164
+ // type was silently coerced by YAML 1.2 to something incompatible (e.g. a
165
+ // `boolean` for a `string`-declared attr), an ERR_YAML_COERCION is collected.
122
166
  function desugarBody(
123
167
  rawBody: unknown,
124
- errors: string[],
168
+ registry: TypeRegistry,
169
+ canonicalKey: string,
170
+ errors: CollectedError[],
125
171
  path: string,
126
172
  ): Record<string, unknown> {
127
173
  if (
@@ -136,10 +182,146 @@ function desugarBody(
136
182
  return {};
137
183
  }
138
184
  if (Array.isArray(rawBody)) {
139
- errors.push(`Node body at ${path} must be a scalar or mapping, not a list`);
185
+ errors.push({
186
+ message: `Node body at ${path} must be a scalar or mapping, not a list`,
187
+ });
140
188
  return {};
141
189
  }
142
190
  // A mapping — shallow-copy so isArray / children replacement do not mutate
143
- // the caller's parsed-YAML object.
144
- return { ...(rawBody as Record<string, unknown>) };
191
+ // the caller's parsed-YAML object, AND apply Rule 5 (sigil-free attrs) +
192
+ // Rule D2 (type-coercion guard).
193
+ const src = rawBody as Record<string, unknown>;
194
+ const out: Record<string, unknown> = {};
195
+ const schemaIndex = attrSchemaIndex(registry, canonicalKey);
196
+ for (const key of Object.keys(src)) {
197
+ if (RESERVED_KEYS.has(key) || key.startsWith(ATTR_PREFIX)) {
198
+ out[key] = src[key];
199
+ // D2 also applies to author-written @-keys (the awkward form).
200
+ const attrName = key.startsWith(ATTR_PREFIX) ? key.slice(ATTR_PREFIX.length) : "";
201
+ if (attrName !== "" && !RESERVED_KEYS.has(attrName)) {
202
+ checkCoercion(attrName, src[key], schemaIndex, errors, path);
203
+ }
204
+ } else {
205
+ out[`${ATTR_PREFIX}${key}`] = src[key];
206
+ checkCoercion(key, src[key], schemaIndex, errors, path);
207
+ }
208
+ }
209
+ return out;
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // D2 — YAML type-coercion guard
214
+ // ---------------------------------------------------------------------------
215
+
216
+ // Build a name → AttrSchema map for the given canonical key (type.subType).
217
+ // Returns undefined when the key has no declared attrs (open schema).
218
+ function attrSchemaIndex(
219
+ registry: TypeRegistry,
220
+ canonicalKey: string,
221
+ ): Map<string, AttrSchema> | undefined {
222
+ // canonicalKey is "type.subType" — split on the FIRST dot only so a subType
223
+ // that happens to contain a dot still works.
224
+ const dot = canonicalKey.indexOf(TYPE_SUBTYPE_SEPARATOR);
225
+ if (dot < 0) return undefined;
226
+ const type = canonicalKey.slice(0, dot);
227
+ const subType = canonicalKey.slice(dot + 1);
228
+ const schema = registry.attrsOf(type, subType);
229
+ if (schema.length === 0) return undefined;
230
+ const idx = new Map<string, AttrSchema>();
231
+ for (const spec of schema) idx.set(spec.name, spec);
232
+ return idx;
233
+ }
234
+
235
+ // Check a single attr value against its declared schema's `valueType`. Emits
236
+ // an ERR_YAML_COERCION when the JS type was silently changed by YAML 1.2's
237
+ // core schema (boolean/number/null where a string/stringarray was declared,
238
+ // or vice versa for booleans/numbers).
239
+ function checkCoercion(
240
+ attrName: string,
241
+ raw: unknown,
242
+ schemaIndex: Map<string, AttrSchema> | undefined,
243
+ errors: CollectedError[],
244
+ path: string,
245
+ ): void {
246
+ if (schemaIndex === undefined) return;
247
+ const spec = schemaIndex.get(attrName);
248
+ if (spec === undefined || spec.valueType === undefined) return;
249
+
250
+ switch (spec.valueType) {
251
+ case ATTR_SUBTYPE_STRING:
252
+ case ATTR_SUBTYPE_CLASS:
253
+ if (typeof raw !== "string") emitCoercion(attrName, raw, "string", errors, path);
254
+ return;
255
+ case ATTR_SUBTYPE_BOOLEAN:
256
+ if (typeof raw !== "boolean") emitCoercion(attrName, raw, "boolean", errors, path);
257
+ return;
258
+ case ATTR_SUBTYPE_INT:
259
+ case ATTR_SUBTYPE_LONG:
260
+ case ATTR_SUBTYPE_DOUBLE:
261
+ if (typeof raw !== "number") emitCoercion(attrName, raw, "number", errors, path);
262
+ return;
263
+ case ATTR_SUBTYPE_STRINGARRAY:
264
+ // A bare string at the value position is the legitimate one-element
265
+ // authoring shorthand for a string-array attr (StringArrayAttr.coerce
266
+ // wraps it into a one-element array). It is NOT a coercion. A
267
+ // non-string non-array scalar (boolean/number/null), however, is.
268
+ if (typeof raw === "string") return;
269
+ if (!Array.isArray(raw)) {
270
+ emitCoercion(attrName, raw, "string-array (or single string)", errors, path);
271
+ return;
272
+ }
273
+ // For an array, check every element. A non-string element is a YAML
274
+ // coercion (e.g. unquoted `true` in a string-array list).
275
+ for (let i = 0; i < raw.length; i++) {
276
+ if (typeof raw[i] !== "string") {
277
+ emitCoercion(
278
+ `${attrName}[${i}]`,
279
+ raw[i],
280
+ "string (in string-array)",
281
+ errors,
282
+ path,
283
+ );
284
+ }
285
+ }
286
+ return;
287
+ default:
288
+ // Object-shaped attrs (properties, filter) — accept any object/array,
289
+ // no YAML coercion path applies.
290
+ return;
291
+ }
292
+ }
293
+
294
+ // Build the "quote this value" error. The shape is intentionally explicit so
295
+ // AI authors can act on it: it identifies the attr, the (coerced) JS value,
296
+ // its JS type, the declared expected type, and a one-line fix hint.
297
+ function emitCoercion(
298
+ attrName: string,
299
+ raw: unknown,
300
+ expected: string,
301
+ errors: CollectedError[],
302
+ path: string,
303
+ ): void {
304
+ const actualType = coercedTypeName(raw);
305
+ const literal = literalRepr(raw);
306
+ errors.push({
307
+ message:
308
+ `Attribute '@${attrName}' at ${path}: expected ${expected} but got ${actualType} (${literal}). ` +
309
+ `YAML 1.2 silently coerced an unquoted value — quote it in YAML: ` +
310
+ `'@${attrName}: "${literal}"' not '@${attrName}: ${literal}'.`,
311
+ code: "ERR_YAML_COERCION",
312
+ });
313
+ }
314
+
315
+ function coercedTypeName(raw: unknown): string {
316
+ if (raw === null) return "null";
317
+ if (Array.isArray(raw)) return "array";
318
+ return typeof raw;
319
+ }
320
+
321
+ function literalRepr(raw: unknown): string {
322
+ if (raw === null) return "null";
323
+ if (typeof raw === "boolean") return raw ? "true" : "false";
324
+ if (typeof raw === "number") return String(raw);
325
+ if (typeof raw === "string") return raw;
326
+ return JSON.stringify(raw);
145
327
  }
package/src/core-types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { TypeId, type AttrSchema, type ChildRule, type TypeDefinition, TypeRegistry } from "./registry.js";
3
3
  import type { MetaDataTypeProvider } from "./provider.js";
4
4
  import { dbProvider } from "./persistence/db/db-provider.js";
5
+ import { docProvider } from "./core/documentation/doc-provider.js";
5
6
  import { type DataType } from "./data-type.js";
6
7
  import type { MetaData } from "./shared/meta-data.js";
7
8
  import { MetaRoot } from "./shared/meta-root.js";
@@ -33,8 +34,8 @@ import {
33
34
  import { MetaRelationship } from "./core/relationship/meta-relationship.js";
34
35
  import { MetaLayout } from "./presentation/layout/meta-layout.js";
35
36
  import { MetaSource } from "./persistence/source/meta-source.js";
36
- import { MetaOrigin, MetaPassthroughOrigin, MetaAggregateOrigin } from "./persistence/origin/meta-origin.js";
37
- import { commonFieldAttrs, currencyFieldAttr } from "./core/field/field-schema.js";
37
+ import { MetaOrigin, MetaPassthroughOrigin, MetaAggregateOrigin, MetaCollectionOrigin } from "./persistence/origin/meta-origin.js";
38
+ import { commonFieldAttrs, currencyFieldAttr, enumFieldAttr } from "./core/field/field-schema.js";
38
39
  import { objectAttrs } from "./core/object/object-schema.js";
39
40
  import { relationshipAttrs } from "./core/relationship/relationship-schema.js";
40
41
  import { identityFieldsAttr, IDENTITY_ATTRS_MAP } from "./core/identity/identity-schema.js";
@@ -42,6 +43,9 @@ import { VALIDATOR_ATTRS_MAP } from "./core/validator/validator-schema.js";
42
43
  import { currencyViewAttrs } from "./presentation/view/view-schema.js";
43
44
  import { dataGridLayoutAttrs } from "./presentation/layout/layout-schema.js";
44
45
  import { ORIGIN_ATTRS_MAP } from "./persistence/origin/origin-schema.js";
46
+ import { MetaTemplate } from "./template/meta-template.js";
47
+ import { TEMPLATE_ATTRS_MAP } from "./template/template-schema.js";
48
+ import { TEMPLATE_SUBTYPES } from "./template/template-constants.js";
45
49
  import {
46
50
  TYPE_METADATA,
47
51
  TYPE_OBJECT,
@@ -54,11 +58,12 @@ import {
54
58
  TYPE_LAYOUT,
55
59
  TYPE_SOURCE,
56
60
  TYPE_ORIGIN,
61
+ TYPE_TEMPLATE,
57
62
  SUBTYPE_ROOT,
58
63
  } from "./shared/base-types.js";
59
64
  import { CHILD_RULE_WILDCARD } from "./shared/structural.js";
60
65
  import { OBJECT_SUBTYPES, OBJECT_SUBTYPE_ENTITY } from "./core/object/object-constants.js";
61
- import { FIELD_SUBTYPES, FIELD_SUBTYPE_CURRENCY } from "./core/field/field-constants.js";
66
+ import { FIELD_SUBTYPES, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_ENUM } from "./core/field/field-constants.js";
62
67
  import { ATTR_SUBTYPES } from "./core/attr/attr-constants.js";
63
68
  import {
64
69
  VALIDATOR_SUBTYPES,
@@ -85,6 +90,7 @@ import {
85
90
  ORIGIN_SUBTYPES,
86
91
  ORIGIN_SUBTYPE_PASSTHROUGH,
87
92
  ORIGIN_SUBTYPE_AGGREGATE,
93
+ ORIGIN_SUBTYPE_COLLECTION,
88
94
  } from "./persistence/origin/origin-constants.js";
89
95
 
90
96
  // ---------------------------------------------------------------------------
@@ -146,6 +152,7 @@ const IDENTITY_CLASS_MAP = new Map<string, NodeConstructor>([
146
152
  const ORIGIN_CLASS_MAP = new Map<string, NodeConstructor>([
147
153
  [ORIGIN_SUBTYPE_PASSTHROUGH, MetaPassthroughOrigin],
148
154
  [ORIGIN_SUBTYPE_AGGREGATE, MetaAggregateOrigin],
155
+ [ORIGIN_SUBTYPE_COLLECTION, MetaCollectionOrigin],
149
156
  ]);
150
157
 
151
158
  // ATTR_CLASS_MAP + attrClassFor live in the leaf module ./attr-class-map.ts
@@ -161,6 +168,7 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
161
168
  wildcard(TYPE_FIELD),
162
169
  wildcard(TYPE_ATTR),
163
170
  wildcard(TYPE_VALIDATOR),
171
+ wildcard(TYPE_TEMPLATE),
164
172
  ], MetaRoot),
165
173
  );
166
174
 
@@ -188,12 +196,14 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
188
196
  wildcard(TYPE_ORIGIN),
189
197
  ];
190
198
  for (const subType of FIELD_SUBTYPES) {
191
- // field.currency additionally carries @currency; all other field subtypes
192
- // share the common codegen/filter attrs only.
199
+ // field.currency additionally carries @currency; field.enum additionally
200
+ // carries @values; all other field subtypes share the common attrs only.
193
201
  const fieldAttrs =
194
202
  subType === FIELD_SUBTYPE_CURRENCY
195
203
  ? [...commonFieldAttrs, { ...currencyFieldAttr }]
196
- : [...commonFieldAttrs];
204
+ : subType === FIELD_SUBTYPE_ENUM
205
+ ? [...commonFieldAttrs, { ...enumFieldAttr }]
206
+ : [...commonFieldAttrs];
197
207
  registry.register(
198
208
  def(TYPE_FIELD, subType, `Field of type ${subType}`, fieldRules, MetaField, fieldAttrs,
199
209
  new MetaField(new TypeId(TYPE_FIELD, subType), "").dataType),
@@ -247,9 +257,10 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
247
257
  );
248
258
  }
249
259
 
250
- // source — declares where an object's data lives (dbTable, dbView, ...).
260
+ // source — declares where an object's data lives (rdb, ... per ADR-0007).
251
261
  // Only attr children; sources carry only configuration, never nested structure.
252
- // DB-domain attrs (@name) are added by dbProvider via TypeRegistry.extend.
262
+ // Per-subtype attrs (@table/@kind/@role/@schema for rdb) are added by
263
+ // dbProvider via TypeRegistry.extend.
253
264
  for (const subType of SOURCE_SUBTYPES) {
254
265
  registry.register(
255
266
  def(TYPE_SOURCE, subType, `Source (${subType})`, [wildcard(TYPE_ATTR)], MetaSource, []),
@@ -268,6 +279,17 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
268
279
  );
269
280
  }
270
281
 
282
+ // template — renderable text artifacts (FR-004). prompt + output; attr-only
283
+ // children. A single MetaTemplate class backs both subtypes (mirrors source);
284
+ // per-subtype attr schemas drive validation (both require @payloadRef +
285
+ // @textRef; @format is a closed enum; template.prompt adds the LLM overlay).
286
+ for (const subType of TEMPLATE_SUBTYPES) {
287
+ const templateAttrs = TEMPLATE_ATTRS_MAP.get(subType) ?? [];
288
+ registry.register(
289
+ def(TYPE_TEMPLATE, subType, `Template (${subType})`, [wildcard(TYPE_ATTR)], MetaTemplate, templateAttrs),
290
+ );
291
+ }
292
+
271
293
  // identity — 2 subtypes (no base; Java doesn't register one).
272
294
  // Subtype→class dispatch for TYPE_IDENTITY (formerly handled by metaOf()):
273
295
  // primary → MetaPrimaryIdentity, secondary → MetaSecondaryIdentity,
@@ -318,7 +340,7 @@ export const coreTypesProvider: MetaDataTypeProvider = {
318
340
 
319
341
  /** The default provider bundle — core metamodel types plus DB-domain attrs.
320
342
  * Spread it to add more: `[...coreProviders, mine]`. */
321
- export const coreProviders: readonly MetaDataTypeProvider[] = [coreTypesProvider, dbProvider];
343
+ export const coreProviders: readonly MetaDataTypeProvider[] = [coreTypesProvider, dbProvider, docProvider];
322
344
 
323
345
  /**
324
346
  * Register the core metamodel into an existing registry. Thin convenience
package/src/errors.ts CHANGED
@@ -7,6 +7,13 @@
7
7
  // - ERR_MISSING_SUBTYPE: missing subType is resolved to the registry default, never an error.
8
8
  // - ERR_INVALID_SUBTYPE_CHILD: no child-rule validation pass exists yet.
9
9
  // Cross-language conformance consumers should not expect these codes from the TS adapter.
10
+ //
11
+ // FR-004 build-time `verify` codes (ERR_VAR_NOT_ON_PAYLOAD, ERR_PARTIAL_UNRESOLVED,
12
+ // ERR_REQUIRED_SLOT_UNUSED, ERR_OUTPUT_TAG_MISSING) are emitted by `meta verify` / the
13
+ // zero-core-dependency @metaobjectsdev/render engine — NOT by the loader. They are
14
+ // registered here (and in fixtures/conformance/ERROR-CODES.json) only to keep the code
15
+ // vocabulary single-sourced across languages; render re-declares them locally to avoid
16
+ // importing this package.
10
17
  export const ERROR_CODES = [
11
18
  "ERR_TOP_LEVEL_NOT_OBJECT",
12
19
  "ERR_UNKNOWN_TYPE",
@@ -28,7 +35,21 @@ export const ERROR_CODES = [
28
35
  "ERR_OVERLAY_NO_TARGET",
29
36
  "ERR_MALFORMED_YAML",
30
37
  "ERR_INVALID_ORIGIN",
38
+ "ERR_INVALID_TEMPLATE",
39
+ "ERR_VAR_NOT_ON_PAYLOAD",
40
+ "ERR_PARTIAL_UNRESOLVED",
41
+ "ERR_REQUIRED_SLOT_UNUSED",
42
+ "ERR_OUTPUT_TAG_MISSING",
31
43
  "ERR_BAD_ATTR_FILTER",
44
+ "ERR_STORAGE_FLATTENED_ARRAY",
45
+ "ERR_STORAGE_WITHOUT_OBJECT_REF",
46
+ // Source v2 (ADR-0007) error codes — enforcement added during the source-v2 rollout.
47
+ "ERR_RESERVED_ATTR",
48
+ "ERR_SOURCE_NO_PRIMARY",
49
+ "ERR_SOURCE_MULTIPLE_PRIMARY",
50
+ // ADR-0006 D2 — YAML type-coercion guard. Emitted by every port's YAML
51
+ // loader when a coerced scalar mismatches the schema-declared type.
52
+ "ERR_YAML_COERCION",
32
53
  "ERR_UNKNOWN",
33
54
  ] as const;
34
55
 
package/src/index.ts CHANGED
@@ -27,12 +27,14 @@ export * from "./shared/structural.js";
27
27
  export * from "./core/object/object-constants.js";
28
28
  export * from "./core/field/field-constants.js";
29
29
  export * from "./core/attr/attr-constants.js";
30
+ export * from "./core/documentation/doc-constants.js";
30
31
  export * from "./core/validator/validator-constants.js";
31
32
  export * from "./core/identity/identity-constants.js";
32
33
  export * from "./core/relationship/relationship-constants.js";
33
34
  export * from "./core/query/query-constants.js";
34
35
  export * from "./persistence/source/source-constants.js";
35
36
  export * from "./persistence/origin/origin-constants.js";
37
+ export * from "./template/template-constants.js";
36
38
  export * from "./persistence/db/db-constants.js";
37
39
  export * from "./presentation/view/view-constants.js";
38
40
  export * from "./presentation/layout/layout-constants.js";
@@ -78,7 +80,10 @@ export {
78
80
  MetaOrigin,
79
81
  MetaPassthroughOrigin,
80
82
  MetaAggregateOrigin,
83
+ MetaCollectionOrigin,
81
84
  } from "./persistence/origin/meta-origin.js";
85
+ // Template: single class backs both subtypes (FR-004)
86
+ export { MetaTemplate } from "./template/meta-template.js";
82
87
 
83
88
  // Presentation node classes
84
89
  export { MetaView } from "./presentation/view/meta-view.js";
@@ -108,6 +113,8 @@ export { TypeId, TypeRegistry, childRuleMatches } from "./registry.js";
108
113
  export type { AttrSchema, ChildRule, TypeDefinition } from "./registry.js";
109
114
  export { registerCoreTypes, coreTypesProvider, coreProviders } from "./core-types.js";
110
115
  export { dbProvider } from "./persistence/db/db-provider.js";
116
+ export { commonDocAttrs } from "./core/documentation/doc-schema.js";
117
+ export { docProvider } from "./core/documentation/doc-provider.js";
111
118
 
112
119
  // Type provider model
113
120
  export { composeRegistry } from "./provider.js";
@@ -15,7 +15,8 @@ import { composeRegistry } from "../provider.js";
15
15
  import { TYPE_METADATA, SUBTYPE_ROOT } from "../shared/base-types.js";
16
16
  import { ParseError } from "../errors.js";
17
17
  import { parseJson } from "../parser-json.js";
18
- import { validateDataGridSortFields, validateFilterableHasIndex, validateOriginPaths, validateDataGridFilterValues } from "./validation-passes.js";
18
+ import { validateDataGridSortFields, validateFilterableHasIndex, validateOriginPaths, validateDataGridFilterValues, validateFieldObjectStorage, validateTemplatePayloadRefs } from "./validation-passes.js";
19
+ import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
19
20
  import { resolveDeferredSupers } from "../super-resolve.js";
20
21
  import { validateSubtypeRules } from "../subtype-rules.js";
21
22
  import { validateAttrSchema } from "../attr-schema-validate.js";
@@ -278,12 +279,25 @@ export class MetaDataLoader {
278
279
  // Seventh pass: @filter value validation — fields filterable + ops allowed per subtype.
279
280
  errors.push(...validateDataGridFilterValues(root));
280
281
 
282
+ // template.* validation — @payloadRef resolves to a known object;
283
+ // @requiredSlots are real fields on it (FR-004 Plan #3, T2).
284
+ errors.push(...validateTemplatePayloadRefs(root));
285
+
281
286
  // Eighth pass: attribute-schema validation (Phase A3) — checks each
282
287
  // node's @-attributes against its (type, subType) AttrSchema: required
283
288
  // attrs present, declared attrs well-typed, allowedValues honored.
284
289
  const attrSchemaResult = validateAttrSchema(root, this._registry);
285
290
  errors.push(...attrSchemaResult.errors);
286
291
  warnings.push(...attrSchemaResult.warnings);
292
+
293
+ // Ninth pass: @storage cross-attribute validation — @storage requires
294
+ // @objectRef, and @storage "flattened" forbids isArray=true.
295
+ errors.push(...validateFieldObjectStorage(root));
296
+
297
+ // Tenth pass: one-primary multi-source rule — if an object has ≥1 source,
298
+ // exactly one must carry role "primary" (ERR_SOURCE_NO_PRIMARY /
299
+ // ERR_SOURCE_MULTIPLE_PRIMARY).
300
+ errors.push(...validateSourceRoles(root));
287
301
  }
288
302
 
289
303
  // If nothing parsed successfully, synthesize an empty root so callers