@metaobjectsdev/metadata 0.24.4 → 0.25.0

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 (87) hide show
  1. package/dist/attr-schema-validate.js +15 -3
  2. package/dist/attr-schema-validate.js.map +1 -1
  3. package/dist/core/attr/attr-definition.embedded.js +1 -1
  4. package/dist/core/attr/attr-definition.embedded.js.map +1 -1
  5. package/dist/core/field/field-definition.embedded.js +1 -1
  6. package/dist/core/field/field-definition.embedded.js.map +1 -1
  7. package/dist/core/object/meta-object.d.ts +2 -0
  8. package/dist/core/object/meta-object.d.ts.map +1 -1
  9. package/dist/core/object/meta-object.js +20 -0
  10. package/dist/core/object/meta-object.js.map +1 -1
  11. package/dist/core/object/object-definition.embedded.js +1 -1
  12. package/dist/core/object/object-definition.embedded.js.map +1 -1
  13. package/dist/core/relationship/relationship-definition.embedded.js +1 -1
  14. package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
  15. package/dist/core/validator/validator-definition.embedded.js +1 -1
  16. package/dist/core/validator/validator-definition.embedded.js.map +1 -1
  17. package/dist/errors.d.ts +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +11 -1
  20. package/dist/errors.js.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/naming.d.ts +93 -0
  26. package/dist/naming.d.ts.map +1 -1
  27. package/dist/naming.js +126 -14
  28. package/dist/naming.js.map +1 -1
  29. package/dist/parser-core.d.ts.map +1 -1
  30. package/dist/parser-core.js +130 -11
  31. package/dist/parser-core.js.map +1 -1
  32. package/dist/persistence/origin/origin-definition.embedded.js +1 -1
  33. package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
  34. package/dist/persistence/source/source-definition.embedded.js +1 -1
  35. package/dist/persistence/source/source-definition.embedded.js.map +1 -1
  36. package/dist/presentation/layout/layout-definition.embedded.js +1 -1
  37. package/dist/presentation/layout/layout-definition.embedded.js.map +1 -1
  38. package/dist/presentation/view/view-definition.embedded.js +1 -1
  39. package/dist/presentation/view/view-definition.embedded.js.map +1 -1
  40. package/dist/registry-coverage.d.ts +0 -6
  41. package/dist/registry-coverage.d.ts.map +1 -1
  42. package/dist/registry-coverage.js +25 -1
  43. package/dist/registry-coverage.js.map +1 -1
  44. package/dist/registry-manifest-exclusions.d.ts +2 -1
  45. package/dist/registry-manifest-exclusions.d.ts.map +1 -1
  46. package/dist/registry-manifest-exclusions.js +4 -2
  47. package/dist/registry-manifest-exclusions.js.map +1 -1
  48. package/dist/registry-manifest.d.ts +13 -6
  49. package/dist/registry-manifest.d.ts.map +1 -1
  50. package/dist/registry-manifest.js +6 -5
  51. package/dist/registry-manifest.js.map +1 -1
  52. package/dist/retired-vocabulary.d.ts +23 -0
  53. package/dist/retired-vocabulary.d.ts.map +1 -1
  54. package/dist/retired-vocabulary.js +99 -1
  55. package/dist/retired-vocabulary.js.map +1 -1
  56. package/dist/shared/meta-data.d.ts.map +1 -1
  57. package/dist/shared/meta-data.js +23 -1
  58. package/dist/shared/meta-data.js.map +1 -1
  59. package/dist/template/template-definition.embedded.js +1 -1
  60. package/dist/template/template-definition.embedded.js.map +1 -1
  61. package/dist/vocabulary-catalog.d.ts +49 -0
  62. package/dist/vocabulary-catalog.d.ts.map +1 -0
  63. package/dist/vocabulary-catalog.js +80 -0
  64. package/dist/vocabulary-catalog.js.map +1 -0
  65. package/package.json +1 -1
  66. package/src/attr-schema-validate.ts +15 -3
  67. package/src/core/attr/attr-definition.embedded.ts +1 -1
  68. package/src/core/field/field-definition.embedded.ts +1 -1
  69. package/src/core/object/meta-object.ts +22 -0
  70. package/src/core/object/object-definition.embedded.ts +1 -1
  71. package/src/core/relationship/relationship-definition.embedded.ts +1 -1
  72. package/src/core/validator/validator-definition.embedded.ts +1 -1
  73. package/src/errors.ts +11 -1
  74. package/src/index.ts +8 -1
  75. package/src/naming.ts +134 -18
  76. package/src/parser-core.ts +142 -12
  77. package/src/persistence/origin/origin-definition.embedded.ts +1 -1
  78. package/src/persistence/source/source-definition.embedded.ts +1 -1
  79. package/src/presentation/layout/layout-definition.embedded.ts +1 -1
  80. package/src/presentation/view/view-definition.embedded.ts +1 -1
  81. package/src/registry-coverage.ts +25 -3
  82. package/src/registry-manifest-exclusions.ts +4 -2
  83. package/src/registry-manifest.ts +9 -8
  84. package/src/retired-vocabulary.ts +107 -1
  85. package/src/shared/meta-data.ts +23 -3
  86. package/src/template/template-definition.embedded.ts +1 -1
  87. package/src/vocabulary-catalog.ts +133 -0
package/dist/naming.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MetaData } from "./shared/meta-data.js";
2
+ import type { MetaSource } from "./persistence/source/meta-source.js";
2
3
  /**
3
4
  * Strip the package prefix from a metadata-qualified name
4
5
  * (e.g. "pkg::Name" → "Name"). Returns the input unchanged if no
@@ -22,8 +23,100 @@ export type ColumnNamingStrategy = "snake_case" | "literal" | "kebab-case";
22
23
  export declare const DEFAULT_COLUMN_NAMING_STRATEGY: ColumnNamingStrategy;
23
24
  export declare function applyColumnNamingStrategy(name: string, strategy: ColumnNamingStrategy): string;
24
25
  export declare function pluralize(s: string): string;
26
+ /**
27
+ * THE `@role: primary` source lookup for this package — and the one place the
28
+ * primary-source DIVERGENCE refusal lives, so that every caller inherits it.
29
+ *
30
+ * Effective children (own + inherited via the super chain) so a TPH SUBTYPE —
31
+ * which declares no source of its own and inherits the discriminator base's
32
+ * single table (FR-017) — resolves to that base source rather than to nothing.
33
+ * For an entity declaring its own source, own shadows inherited, so the result
34
+ * is unchanged. Returns `undefined` when `entity` declares no primary source at
35
+ * all: #248, participation in persistence derives from a declared source, never
36
+ * from the object subtype, so an `object.value` and a sourceless
37
+ * `object.projection` both land here rather than being special-cased.
38
+ *
39
+ * `isMetaSource`, not `instanceof`: under two physical copies of this package a
40
+ * class check is false for a real primary source, and the failure is SILENT —
41
+ * the object reads as "not backed by any store".
42
+ *
43
+ * ## The refusal
44
+ *
45
+ * An object whose `@role: primary` sources resolve to MORE THAN ONE physical
46
+ * name has no single answer to give, so this throws rather than picking one.
47
+ * The shape loads with ZERO errors: `validateSourceRoles` enforces "exactly one
48
+ * primary" over `ownChildren()` only, and `_effectiveChildren` shadows an own
49
+ * child over a super child only on a (type, name) match — so two `source.rdb`
50
+ * nodes with DIFFERENT explicit names at two levels of an `extends` chain never
51
+ * collide, and both land on the child's effective `children()`.
52
+ *
53
+ * It lives HERE rather than in any one consumer because every consumer binds
54
+ * ONE name unconditionally. It used to live only in codegen-ts's
55
+ * `resolveObjectNames`, which runs only when the `names` generator is in the
56
+ * run — so with `namesFile()` unwired `meta migrate` emitted DDL against the
57
+ * PARENT's table (via `resolveTableName` → `buildExpectedSchema`) and
58
+ * `ObjectManager` read and wrote it, silently, on every run. A refusal that
59
+ * depends on which consumer asked is not a refusal. Mirrors Python's
60
+ * `source_resolution.primary_rdb_source`, whose codegen, api-docs and runtime
61
+ * callers all inherit it for free.
62
+ *
63
+ * DIRECTION-BLIND: it compares every primary against every other, so it does
64
+ * not matter which of them is writable nor which was declared first. Comparing
65
+ * against the first primary WRITABLE source can only see a divergence when one
66
+ * of the two is read-only — and, since `children()` places inherited entries
67
+ * first, only when the read-only one is the inherited one.
68
+ *
69
+ * Two primaries AGREEING on a name is not a divergence and stays legal: the
70
+ * invariant is that an object has ONE physical name, not that it declares one
71
+ * source. A read-only primary beside a non-primary REPLICA does not reach it
72
+ * either — a replica is not `role === "primary"`.
73
+ */
74
+ export declare function primaryRdbSource(entity: MetaData): MetaSource | undefined;
25
75
  export declare function resolveTableName(entity: MetaData): string;
26
76
  export declare function resolveColumnName(field: MetaData, strategy?: ColumnNamingStrategy): string;
77
+ /**
78
+ * An index's DATABASE name — for an `identity.secondary` (a unique alternate key) or an
79
+ * `index.lookup` (a non-unique retrieval index).
80
+ *
81
+ * These nodes carry no `@column`-style physical spelling: the database name IS the
82
+ * metamodel `name`. That is precisely why the answer must live in a function rather than
83
+ * at each call site. It was spelled independently in three places — the Drizzle emitter,
84
+ * migrate's expected-schema (twice) and the Kotlin Exposed emitter — and agreed only by
85
+ * coincidence; `fdb4118f1` is what that coincidence lapsing looks like, with codegen
86
+ * declaring `idx_<table>_<col>` while the index in the database was `identity.name`.
87
+ *
88
+ * Two rules the single door now owns, neither of which any call site had:
89
+ *
90
+ * - **Package qualifier stripped — and the reason first given for this was WRONG.** It was
91
+ * justified here as compensating for the JVM loader package-qualifying a nested index
92
+ * name (`acme::demo::by_name`), which `KotlinExposedTableGenerator` mirrored with
93
+ * `shortName ?: name`. Measured against the real JVM loader, that does not happen: a
94
+ * nested `identity.secondary` or `index.lookup` is named flat, including when inherited
95
+ * across packages via `extends`. Only a ROOT-level node takes the file's package, and an
96
+ * unnamed `view` child gets a synthesised FQN — the likeliest source of the belief. The
97
+ * JVM's local strip had been a no-op for its whole life while reading as the site that
98
+ * owned the rule. Pinned now on that side by `IndexNamingTest`.
99
+ *
100
+ * The strip stays, on the honest reason rather than the invented one: it is a no-op on
101
+ * every name either loader produces, which is what a rule that holds without a per-port
102
+ * branch looks like, and it costs one `lastIndexOf`. A normalisation nobody has to
103
+ * remember is worth more than a claim nobody checked.
104
+ * - **An empty name is REFUSED**, and the gap it closes is exactly one node type wide.
105
+ * An `identity.secondary` with an empty name is already refused by the LOADER in strict
106
+ * and lax mode alike (identity nodes carry an FR-024 name check so a dotted `extends`
107
+ * ref can address them). An `index.lookup` is not addressable that way and carries no
108
+ * such check, so `{"index.lookup": {"name": ""}}` loads with zero errors in both modes
109
+ * and reaches the emitters, which produce `index("")`: SQL no engine accepts, from a
110
+ * model that passed every gate. Refusing at the shared door closes it for codegen and
111
+ * migrate at once, without touching the byte-gated registry `rules` prose a loader-side
112
+ * fix would need. Measured, not assumed — `resolve-index-name.test.ts` asserts both
113
+ * arms, because "the loader already handles it" is the belief that would delete this.
114
+ */
115
+ export declare function resolveIndexName(node: {
116
+ readonly name: string;
117
+ readonly type: string;
118
+ readonly subType: string;
119
+ }): string;
27
120
  /**
28
121
  * Returns the DB schema declared on an entity's primary source child, or undefined
29
122
  * when no @schema attr is set or no source child exists. @schema is paradigm-agnostic
@@ -1 +1 @@
1
- {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAiBtD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI7D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,SAAS,GAAG,YAAY,CAAC;AAE3E,sDAAsD;AACtD,eAAO,MAAM,8BAA8B,EAAE,oBAAmC,CAAC;AAEjF,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAM9F;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3C;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAiBzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,EACf,QAAQ,GAAE,oBAAqD,GAC9D,MAAM,CAKR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAUvE;AAED,2GAA2G;AAC3G,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,QAAQ,EAChB,QAAQ,GAAE,oBAAqD,GAC9D,aAAa,CAYf"}
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAQtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAWtE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI7D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,SAAS,GAAG,YAAY,CAAC;AAE3E,sDAAsD;AACtD,eAAO,MAAM,8BAA8B,EAAE,oBAAmC,CAAC;AAEjF,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAM9F;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAgBzE;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CASzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,EACf,QAAQ,GAAE,oBAAqD,GAC9D,MAAM,CAKR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,gBAAgB,CAK9B,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/E,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAWvE;AAED,2GAA2G;AAC3G,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,QAAQ,EAChB,QAAQ,GAAE,oBAAqD,GAC9D,aAAa,CAYf"}
package/dist/naming.js CHANGED
@@ -2,13 +2,15 @@ import { TYPE_FIELD } from "./shared/base-types.js";
2
2
  import { PACKAGE_SEPARATOR } from "./shared/structural.js";
3
3
  import { FIELD_ATTR_COLUMN } from "./persistence/db/db-constants.js";
4
4
  import { SOURCE_ATTR_SCHEMA, SOURCE_ROLE_PRIMARY, } from "./persistence/source/source-constants.js";
5
- // isMetaSource, not `instanceof`: unlike the loader-internal validators, these two
6
- // are EXPORTED helpers that run on a caller-supplied node — migrate-ts calls both
7
- // on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata tree the
8
- // class check would be false for a real primary source, and resolveTableName would
9
- // silently fall through to the entity-name fallback: a DIFFERENT table name, which
10
- // migrate then emits as a rename against a live database.
5
+ // isMetaSource, not `instanceof`: unlike the loader-internal validators, the source
6
+ // lookups below are EXPORTED helpers that run on a caller-supplied node — migrate-ts
7
+ // calls them on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata
8
+ // tree the class check would be false for a real primary source, and resolveTableName
9
+ // would silently fall through to the entity-name fallback: a DIFFERENT table name,
10
+ // which migrate then emits as a rename against a live database. (The same split would
11
+ // also make the divergence refusal see no primaries at all, and say nothing.)
11
12
  import { isMetaSource } from "./shared/node-guards.js";
13
+ import { MetaModelError } from "./errors.js";
12
14
  /**
13
15
  * Strip the package prefix from a metadata-qualified name
14
16
  * (e.g. "pkg::Name" → "Name"). Returns the input unchanged if no
@@ -47,19 +49,75 @@ export function pluralize(s) {
47
49
  return s.slice(0, -1) + "ies";
48
50
  return s + "s";
49
51
  }
52
+ /**
53
+ * THE `@role: primary` source lookup for this package — and the one place the
54
+ * primary-source DIVERGENCE refusal lives, so that every caller inherits it.
55
+ *
56
+ * Effective children (own + inherited via the super chain) so a TPH SUBTYPE —
57
+ * which declares no source of its own and inherits the discriminator base's
58
+ * single table (FR-017) — resolves to that base source rather than to nothing.
59
+ * For an entity declaring its own source, own shadows inherited, so the result
60
+ * is unchanged. Returns `undefined` when `entity` declares no primary source at
61
+ * all: #248, participation in persistence derives from a declared source, never
62
+ * from the object subtype, so an `object.value` and a sourceless
63
+ * `object.projection` both land here rather than being special-cased.
64
+ *
65
+ * `isMetaSource`, not `instanceof`: under two physical copies of this package a
66
+ * class check is false for a real primary source, and the failure is SILENT —
67
+ * the object reads as "not backed by any store".
68
+ *
69
+ * ## The refusal
70
+ *
71
+ * An object whose `@role: primary` sources resolve to MORE THAN ONE physical
72
+ * name has no single answer to give, so this throws rather than picking one.
73
+ * The shape loads with ZERO errors: `validateSourceRoles` enforces "exactly one
74
+ * primary" over `ownChildren()` only, and `_effectiveChildren` shadows an own
75
+ * child over a super child only on a (type, name) match — so two `source.rdb`
76
+ * nodes with DIFFERENT explicit names at two levels of an `extends` chain never
77
+ * collide, and both land on the child's effective `children()`.
78
+ *
79
+ * It lives HERE rather than in any one consumer because every consumer binds
80
+ * ONE name unconditionally. It used to live only in codegen-ts's
81
+ * `resolveObjectNames`, which runs only when the `names` generator is in the
82
+ * run — so with `namesFile()` unwired `meta migrate` emitted DDL against the
83
+ * PARENT's table (via `resolveTableName` → `buildExpectedSchema`) and
84
+ * `ObjectManager` read and wrote it, silently, on every run. A refusal that
85
+ * depends on which consumer asked is not a refusal. Mirrors Python's
86
+ * `source_resolution.primary_rdb_source`, whose codegen, api-docs and runtime
87
+ * callers all inherit it for free.
88
+ *
89
+ * DIRECTION-BLIND: it compares every primary against every other, so it does
90
+ * not matter which of them is writable nor which was declared first. Comparing
91
+ * against the first primary WRITABLE source can only see a divergence when one
92
+ * of the two is read-only — and, since `children()` places inherited entries
93
+ * first, only when the read-only one is the inherited one.
94
+ *
95
+ * Two primaries AGREEING on a name is not a divergence and stays legal: the
96
+ * invariant is that an object has ONE physical name, not that it declares one
97
+ * source. A read-only primary beside a non-primary REPLICA does not reach it
98
+ * either — a replica is not `role === "primary"`.
99
+ */
100
+ export function primaryRdbSource(entity) {
101
+ const primaries = entity.children().filter((c) => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY);
102
+ if (primaries.length === 0)
103
+ return undefined;
104
+ const distinct = [...new Set(primaries.map((s) => s.physicalName))].sort();
105
+ if (distinct.length > 1) {
106
+ // Sorted, so the message is identical in every port regardless of children() order.
107
+ const joined = distinct.map((n) => `"${n}"`).join(", ");
108
+ throw new MetaModelError(`${entity.name}: role=primary sources disagree on the object's physical name — ` +
109
+ `${joined}. Every consumer binds ONE name. Give them matching physical names, ` +
110
+ `or drop the extra role=primary declaration.`);
111
+ }
112
+ return primaries[0];
113
+ }
50
114
  export function resolveTableName(entity) {
51
115
  // FR-016: primary source's `physicalName` implements the four-step rule
52
116
  // (kind-matching alias → legacy @table → source.name → entity-name fallback),
53
117
  // so this helper now just delegates. Writability (table vs view/storedProc/
54
118
  // tableFunction) only affects write-routing — for SELECT-side name resolution,
55
119
  // a read-only primary source is the right answer.
56
- //
57
- // Effective children (own + inherited via the super chain) so a TPH SUBTYPE
58
- // — which declares no source of its own and inherits the discriminator base's
59
- // single table (FR-017) — resolves to that base table rather than the
60
- // entity-name fallback. For an entity declaring its own source, own shadows
61
- // inherited, so the result is unchanged.
62
- const source = entity.children().find((c) => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY);
120
+ const source = primaryRdbSource(entity);
63
121
  if (source !== undefined)
64
122
  return source.physicalName;
65
123
  return pluralize(toSnakeCase(entity.name));
@@ -71,6 +129,57 @@ export function resolveColumnName(field, strategy = DEFAULT_COLUMN_NAMING_STRATE
71
129
  return col;
72
130
  return applyColumnNamingStrategy(field.name, strategy);
73
131
  }
132
+ /**
133
+ * An index's DATABASE name — for an `identity.secondary` (a unique alternate key) or an
134
+ * `index.lookup` (a non-unique retrieval index).
135
+ *
136
+ * These nodes carry no `@column`-style physical spelling: the database name IS the
137
+ * metamodel `name`. That is precisely why the answer must live in a function rather than
138
+ * at each call site. It was spelled independently in three places — the Drizzle emitter,
139
+ * migrate's expected-schema (twice) and the Kotlin Exposed emitter — and agreed only by
140
+ * coincidence; `fdb4118f1` is what that coincidence lapsing looks like, with codegen
141
+ * declaring `idx_<table>_<col>` while the index in the database was `identity.name`.
142
+ *
143
+ * Two rules the single door now owns, neither of which any call site had:
144
+ *
145
+ * - **Package qualifier stripped — and the reason first given for this was WRONG.** It was
146
+ * justified here as compensating for the JVM loader package-qualifying a nested index
147
+ * name (`acme::demo::by_name`), which `KotlinExposedTableGenerator` mirrored with
148
+ * `shortName ?: name`. Measured against the real JVM loader, that does not happen: a
149
+ * nested `identity.secondary` or `index.lookup` is named flat, including when inherited
150
+ * across packages via `extends`. Only a ROOT-level node takes the file's package, and an
151
+ * unnamed `view` child gets a synthesised FQN — the likeliest source of the belief. The
152
+ * JVM's local strip had been a no-op for its whole life while reading as the site that
153
+ * owned the rule. Pinned now on that side by `IndexNamingTest`.
154
+ *
155
+ * The strip stays, on the honest reason rather than the invented one: it is a no-op on
156
+ * every name either loader produces, which is what a rule that holds without a per-port
157
+ * branch looks like, and it costs one `lastIndexOf`. A normalisation nobody has to
158
+ * remember is worth more than a claim nobody checked.
159
+ * - **An empty name is REFUSED**, and the gap it closes is exactly one node type wide.
160
+ * An `identity.secondary` with an empty name is already refused by the LOADER in strict
161
+ * and lax mode alike (identity nodes carry an FR-024 name check so a dotted `extends`
162
+ * ref can address them). An `index.lookup` is not addressable that way and carries no
163
+ * such check, so `{"index.lookup": {"name": ""}}` loads with zero errors in both modes
164
+ * and reaches the emitters, which produce `index("")`: SQL no engine accepts, from a
165
+ * model that passed every gate. Refusing at the shared door closes it for codegen and
166
+ * migrate at once, without touching the byte-gated registry `rules` prose a loader-side
167
+ * fix would need. Measured, not assumed — `resolve-index-name.test.ts` asserts both
168
+ * arms, because "the loader already handles it" is the belief that would delete this.
169
+ */
170
+ export function resolveIndexName(
171
+ // The narrow structural shape rather than `MetaData`: this reads three properties, and
172
+ // every caller that has a real node satisfies it, while the Drizzle emitter's local
173
+ // duck-typed index node does not need a cast to pass one. A cast here would be the
174
+ // usual way a `never` slips past the compiler into a runtime property read.
175
+ node) {
176
+ const short = stripPackage(node.name);
177
+ if (short === "") {
178
+ throw new MetaModelError(`${node.type}.${node.subType} declares an empty name; an index's database name IS ` +
179
+ `its metamodel name, so there is nothing to emit. Give it a name.`);
180
+ }
181
+ return short;
182
+ }
74
183
  /**
75
184
  * Returns the DB schema declared on an entity's primary source child, or undefined
76
185
  * when no @schema attr is set or no source child exists. @schema is paradigm-agnostic
@@ -80,7 +189,10 @@ export function resolveColumnName(field, strategy = DEFAULT_COLUMN_NAMING_STRATE
80
189
  */
81
190
  export function resolveTableSchema(entity) {
82
191
  // ADR-0039: resolving — a concrete entity may inherit its source.rdb via extends.
83
- const source = entity.children().find((c) => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY);
192
+ // primaryRdbSource, not a second hand-rolled scan: a lookup written twice is a
193
+ // lookup that can disagree with itself, and only one of the two copies would
194
+ // carry the divergence refusal.
195
+ const source = primaryRdbSource(entity);
84
196
  if (!source)
85
197
  return undefined;
86
198
  // ADR-0039: resolving — an inherited source's @schema lives on the super node.
@@ -1 +1 @@
1
- {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"parser-core.d.ts","sourceRoot":"","sources":["../src/parser-core.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAU,YAAY,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,OAAO,EAAE,UAAU,EAAkB,MAAM,aAAa,CAAC;AAGzD,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAyCrG,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB;;;;;;OAMG;IACH,gBAAgB,EAAE,aAAa,EAAE,CAAC;CACnC;AAYD,wBAAgB,SAAS,IAAI,WAAW,CAuBvC;AAwMD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,WAAW,CAmJ1E"}
1
+ {"version":3,"file":"parser-core.d.ts","sourceRoot":"","sources":["../src/parser-core.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAU,YAAY,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGjD,OAAO,EAAE,UAAU,EAAkB,MAAM,aAAa,CAAC;AAGzD,OAAO,EAAkB,KAAK,WAAW,EAAE,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAyCrG,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB;;;;;;OAMG;IACH,gBAAgB,EAAE,aAAa,EAAE,CAAC;CACnC;AAYD,wBAAgB,SAAS,IAAI,WAAW,CAuBvC;AA6QD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,GAAG,WAAW,CAiL1E"}
@@ -28,7 +28,7 @@ import { MetaAttr } from "./core/attr/meta-attr.js";
28
28
  import { canonicalSerialize, inferAttrSubType } from "./serializer-json.js";
29
29
  import { ParseError } from "./errors.js";
30
30
  // #337 — see retired-vocabulary.ts. Diagnostic only; no load outcome changes.
31
- import { retiredSubType, retirementHint } from "./retired-vocabulary.js";
31
+ import { retiredSubType, retirementHint, retirementSuggestions } from "./retired-vocabulary.js";
32
32
  import { resolvedSource } from "./source.js";
33
33
  import { semanticDiff } from "./semantic-diff.js";
34
34
  import { resolveSuperRef, isChildTargetingRef, extendsTargetCompatible, EXTENDS_TARGET_MISMATCH_RULE, } from "./super-resolve.js";
@@ -112,19 +112,82 @@ function guardRelativeRefInCanonical(refLabel, rawValue, strict, warnings, path)
112
112
  warnings.push(msg);
113
113
  }
114
114
  }
115
+ // ---------------------------------------------------------------------------
116
+ // The one message for an authored `<type>.base`, so both doors say the same thing.
117
+ //
118
+ // Every registered `base` subtype is an ABSTRACT REGISTRY ANCHOR: the shared root that
119
+ // concrete subtypes inherit their attrs and child rules from. It has no runtime semantics
120
+ // and no concrete representation — `spec/metamodel/object.json` says so in as many words
121
+ // ("Has no runtime semantics of its own; not authored directly"), and every `base` entry's
122
+ // description opens with "Abstract".
123
+ //
124
+ // The JVM enforced this by accident (its impl classes are `public abstract`, so
125
+ // instantiation fails); TypeScript, C# and Python accepted it. The same document therefore
126
+ // loaded on three ports and failed to load on two — the cross-port conformance gap the
127
+ // corpora exist to catch, and it survived because every `*.base` subtype sits in the
128
+ // registry corpus's own `untestedSubTypes` list.
129
+ /**
130
+ * Is `<type>.base` an ABSTRACT ANCHOR for this type — i.e. does the type register at least one
131
+ * OTHER subtype for it to anchor?
132
+ *
133
+ * Registry-driven, not name-driven, and the distinction is load-bearing. All ten core anchors
134
+ * have concrete siblings (`object.base` beside entity/value/projection, `source.base` beside
135
+ * rdb, …), so the rule catches every one. A third-party provider may register `base` as a
136
+ * type's ONLY member — the SDK's forge `convention`/`glossary`/`failure` do — and there it is
137
+ * not anchoring anything: refusing it would leave the type unauthorable, which is a
138
+ * capability removal the contract never asked for. The manifest describes the CORE registry;
139
+ * this predicate is how that scope is expressed without hardcoding the core's type list.
140
+ */
141
+ function isAbstractAnchorFor(type, registry) {
142
+ return registry.allSubTypesOf(type).some((sub) => sub !== SUBTYPE_BASE);
143
+ }
144
+ function abstractSubtypeMessage(type) {
145
+ return (`"${type}.${SUBTYPE_BASE}" may not be authored — every "${SUBTYPE_BASE}" subtype is an ` +
146
+ `abstract registry anchor that concrete subtypes inherit from, with no runtime ` +
147
+ `semantics of its own. Declare a concrete ${type} subtype instead.`);
148
+ }
149
+ // The same rule reached by the OTHER spelling: a BARE wrapper key (`{"field": …}`, no fused
150
+ // subType) whose registry default resolves to the abstract anchor. The author did not type
151
+ // `.base`, so this is a MISSING subtype rather than an authored-anchor error — and
152
+ // ERR_MISSING_SUBTYPE is the shared code already chartered for it ("a node omits subType and
153
+ // the type has no default subType").
154
+ //
155
+ // Python has always emitted it here; TypeScript, C# and the JVM did not, so a bare key was a
156
+ // SECOND way one document got two verdicts: TS and C# resolved it to the anchor and loaded,
157
+ // the JVM resolved it identically and then failed to instantiate with a message naming a
158
+ // missing constructor. Closing the authored spelling alone would have left the rule half
159
+ // true, reachable by dropping four characters.
160
+ //
161
+ // Scoped to "the default IS the anchor", never to bare keys as such: a type that declares a
162
+ // CONCRETE default keeps resolving through it, which is the door a future default-subtype
163
+ // decision walks through (ADR-0054's closing section).
164
+ function missingSubtypeMessage(type) {
165
+ return (`type "${type}" has no default subType; write the full "${type}.<subType>"`);
166
+ }
167
+ /**
168
+ * The subType a BARE wrapper key (`{"object": …}`) resolves to: the type's DECLARED default,
169
+ * or `undefined` when it declares none.
170
+ *
171
+ * `registry.defaultSubTypeOf` is the same accessor the YAML desugar consults, and the shared
172
+ * corpus already pins the contract (`fixtures/yaml-conformance/yaml-bare-default-subtypes`:
173
+ * bare `object:` becomes `object.entity`). This used to guess instead — `allSubTypesOf()[0]`,
174
+ * i.e. registration order, falling back to `base` — so the two layers answered the same
175
+ * question two different ways. Registration order put `base` first, so a bare key in JSON
176
+ * resolved to the abstract anchor: it loaded here and in C#, while the JVM resolved it
177
+ * identically and then failed to INSTANTIATE, because its impl classes are abstract.
178
+ *
179
+ * A name resolved twice by two different functions is a name that can disagree with itself —
180
+ * the defect class this file's own header exists to prevent, reached through the one door
181
+ * nobody had pointed at the registry.
182
+ */
115
183
  function defaultSubTypeFor(type, registry) {
116
- const subs = registry.allSubTypesOf(type);
117
- const candidate = subs.length > 0 ? subs[0] : SUBTYPE_BASE;
118
- if (!registry.has(type, candidate) && registry.has(type, SUBTYPE_BASE)) {
119
- return SUBTYPE_BASE;
120
- }
121
- return candidate;
184
+ return registry.defaultSubTypeOf(type);
122
185
  }
123
186
  function splitTypeKey(key, registry) {
124
187
  const dotIdx = key.indexOf(TYPE_SUBTYPE_SEPARATOR);
125
188
  if (dotIdx < 0) {
126
189
  // Bare type, no fused subType — resolve via the registry default.
127
- return { type: key, subType: defaultSubTypeFor(key, registry), explicit: false };
190
+ return { type: key, subType: defaultSubTypeFor(key, registry) ?? "", explicit: false };
128
191
  }
129
192
  const type = key.slice(0, dotIdx);
130
193
  const subType = key.slice(dotIdx + TYPE_SUBTYPE_SEPARATOR.length);
@@ -283,7 +346,29 @@ export function buildTree(parsed, opts) {
283
346
  throw new ParseError(`Top-level wrapper "${rootKey}" must contain an object`, { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src });
284
347
  }
285
348
  const rootDataObj = rootData;
286
- const { type: rootType, subType: rootSubType } = splitTypeKey(rootKey, opts.registry);
349
+ const { type: rootType, subType: rootSubType, explicit: rootExplicit } = splitTypeKey(rootKey, opts.registry);
350
+ // A `<type>.base` node may not be AUTHORED — see abstractSubtypeMessage. This is the
351
+ // ROOT door; the child door is in the child loop below. One rule, both doors: a check
352
+ // on one of two entry points is a rule that is only half true.
353
+ // Registration first: an UNREGISTERED type has no default either, so without this a typo'd
354
+ // root key is diagnosed as a registered type that merely lacks a default. The registration
355
+ // check below owns that case and phrases it correctly.
356
+ if (opts.registry.allSubTypesOf(rootType).length > 0
357
+ && ((rootSubType === SUBTYPE_BASE && isAbstractAnchorFor(rootType, opts.registry))
358
+ || (!rootExplicit && rootSubType === ""))) {
359
+ _currentPath.pushKey(rootKey);
360
+ const src = errSource();
361
+ _currentPath.pop();
362
+ throw rootExplicit
363
+ ? new ParseError(abstractSubtypeMessage(rootType), {
364
+ code: "ERR_ABSTRACT_SUBTYPE_AUTHORED",
365
+ source: src,
366
+ })
367
+ : new ParseError(missingSubtypeMessage(rootType), {
368
+ code: "ERR_MISSING_SUBTYPE",
369
+ source: src,
370
+ });
371
+ }
287
372
  // Check root type is registered (always throw — can't skip the root)
288
373
  if (!opts.registry.has(rootType, rootSubType)) {
289
374
  const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
@@ -297,7 +382,13 @@ export function buildTree(parsed, opts) {
297
382
  // other" is how a rule ends up half-true and passing every probe.
298
383
  const retiredRoot = retiredSubType(rootType, rootSubType);
299
384
  throw new ParseError(`Unknown root type "${rootType}.${rootSubType}" — ` +
300
- (retiredRoot !== undefined ? retirementHint(retiredRoot) : "not registered"), { code: rootTypeCode, source: src });
385
+ (retiredRoot !== undefined ? retirementHint(retiredRoot) : "not registered"), {
386
+ code: rootTypeCode,
387
+ source: src,
388
+ ...(retiredRoot !== undefined
389
+ ? { suggestions: retirementSuggestions(retiredRoot) }
390
+ : {}),
391
+ });
301
392
  }
302
393
  // FR5a — push the wrapper-key segment onto the JSONPath stack so all
303
394
  // descendants emit jsonPath strings rooted at "$.<rootKey>". The merge-mode
@@ -938,11 +1029,33 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
938
1029
  }
939
1030
  const childDataObj = childData;
940
1031
  const { type: childType, subType: childSubTypeRaw, explicit } = splitTypeKey(childKey, registry);
1032
+ let childSubType = childSubTypeRaw;
1033
+ // A `<type>.base` may not be AUTHORED, and a bare key for a type with no declared default
1034
+ // is a MISSING subType. BEFORE the registered-type check below, matching the root door and
1035
+ // the JVM: run it after, and a bare key for a type that registers no `base` at all
1036
+ // (identity, index, requirement) falls into that check's else-arm first and is reported as
1037
+ // an UNKNOWN TYPE — a message asserting the type does not exist, about a type that does,
1038
+ // with its name mangled to `"identity."`. Order is the whole rule here.
1039
+ if ((childSubType === SUBTYPE_BASE && isAbstractAnchorFor(childType, registry))
1040
+ || (!explicit && childSubType === "")) {
1041
+ errors.push(explicit
1042
+ ? new ParseError(abstractSubtypeMessage(childType), {
1043
+ code: "ERR_ABSTRACT_SUBTYPE_AUTHORED",
1044
+ source: errSource(),
1045
+ })
1046
+ : new ParseError(missingSubtypeMessage(childType), {
1047
+ code: "ERR_MISSING_SUBTYPE",
1048
+ source: errSource(),
1049
+ }));
1050
+ _currentPath?.pop(); // pop child wrapper key
1051
+ _currentPath?.pop(); // pop array index
1052
+ _currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
1053
+ continue; // skip this child
1054
+ }
941
1055
  // --- Check if this child type is registered ---
942
1056
  // An EXPLICIT unknown subType (fused into the key) is an error — never
943
1057
  // silently downgraded to base. An OMITTED subType that resolves to an
944
1058
  // unregistered default falls back to base.
945
- let childSubType = childSubTypeRaw;
946
1059
  if (!registry.has(childType, childSubType)) {
947
1060
  if (!explicit && registry.has(childType, SUBTYPE_BASE)) {
948
1061
  childSubType = SUBTYPE_BASE;
@@ -962,6 +1075,12 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
962
1075
  continue; // skip this child
963
1076
  }
964
1077
  }
1078
+ // A `<type>.base` child may not be AUTHORED — the CHILD door (the root door is above).
1079
+ // Gated on `explicit`, deliberately: `base` is also this parser's fallback for an
1080
+ // OMITTED subtype whose registry default is unregistered (the branch directly above),
1081
+ // and refusing that would break every node relying on the default. Placed before the
1082
+ // attr branch so `attr.base` is covered by the same rule — an authored untyped attr is
1083
+ // the same mistake as an authored untyped field.
965
1084
  // --- Special handling for "attr" child nodes ---
966
1085
  if (childType === TYPE_ATTR) {
967
1086
  parseAttrChild(parent, childType, childSubType, childDataObj, registry, warnings, strict, source, childNodePath);