@metaobjectsdev/metadata 0.24.5 → 1.0.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 (82) 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 +107 -0
  26. package/dist/naming.d.ts.map +1 -1
  27. package/dist/naming.js +155 -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.d.ts +13 -6
  45. package/dist/registry-manifest.d.ts.map +1 -1
  46. package/dist/registry-manifest.js +6 -5
  47. package/dist/registry-manifest.js.map +1 -1
  48. package/dist/retired-vocabulary.d.ts +23 -0
  49. package/dist/retired-vocabulary.d.ts.map +1 -1
  50. package/dist/retired-vocabulary.js +99 -1
  51. package/dist/retired-vocabulary.js.map +1 -1
  52. package/dist/shared/meta-data.d.ts.map +1 -1
  53. package/dist/shared/meta-data.js +23 -1
  54. package/dist/shared/meta-data.js.map +1 -1
  55. package/dist/template/template-definition.embedded.js +1 -1
  56. package/dist/template/template-definition.embedded.js.map +1 -1
  57. package/dist/vocabulary-catalog.d.ts +49 -0
  58. package/dist/vocabulary-catalog.d.ts.map +1 -0
  59. package/dist/vocabulary-catalog.js +80 -0
  60. package/dist/vocabulary-catalog.js.map +1 -0
  61. package/package.json +1 -1
  62. package/src/attr-schema-validate.ts +15 -3
  63. package/src/core/attr/attr-definition.embedded.ts +1 -1
  64. package/src/core/field/field-definition.embedded.ts +1 -1
  65. package/src/core/object/meta-object.ts +22 -0
  66. package/src/core/object/object-definition.embedded.ts +1 -1
  67. package/src/core/relationship/relationship-definition.embedded.ts +1 -1
  68. package/src/core/validator/validator-definition.embedded.ts +1 -1
  69. package/src/errors.ts +11 -1
  70. package/src/index.ts +8 -1
  71. package/src/naming.ts +164 -18
  72. package/src/parser-core.ts +142 -12
  73. package/src/persistence/origin/origin-definition.embedded.ts +1 -1
  74. package/src/persistence/source/source-definition.embedded.ts +1 -1
  75. package/src/presentation/layout/layout-definition.embedded.ts +1 -1
  76. package/src/presentation/view/view-definition.embedded.ts +1 -1
  77. package/src/registry-coverage.ts +25 -3
  78. package/src/registry-manifest.ts +9 -8
  79. package/src/retired-vocabulary.ts +107 -1
  80. package/src/shared/meta-data.ts +23 -3
  81. package/src/template/template-definition.embedded.ts +1 -1
  82. 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,114 @@ 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
+ * ADDRESS has no single answer to give, so this throws rather than picking one.
47
+ * The address is `(kind, schema, physical name)` — see `sourceAddressKey`. It was
48
+ * once the bare physical name, which was a WEAKER key than any consumer's: migrate
49
+ * keys every table it compares by schema-or-default + name, Postgres DDL is
50
+ * `CREATE TABLE "s"."t"`, and since 0.25.0 generated code binds the schema half too
51
+ * (`pgSchema(...).table(...)`, `[Table(..., Schema = ...)]`). Two primaries agreeing
52
+ * on `@table` and disagreeing on `@schema` therefore passed here and were refused by
53
+ * the names artifact, so `meta gen` failed on a model every other door admitted.
54
+ *
55
+ * What decided it was not the asymmetry but WHICH source the weaker key returned:
56
+ * `primaries[0]` is the INHERITED source in TS/C#/Python (`children()` puts the
57
+ * super's entries first) and the OWN source on the JVM, so such a model silently
58
+ * bound one schema in three ports and the other in two — one document, two verdicts
59
+ * depending on which toolchain read it.
60
+ * The shape loads with ZERO errors: `validateSourceRoles` enforces "exactly one
61
+ * primary" over `ownChildren()` only, and `_effectiveChildren` shadows an own
62
+ * child over a super child only on a (type, name) match — so two `source.rdb`
63
+ * nodes with DIFFERENT explicit names at two levels of an `extends` chain never
64
+ * collide, and both land on the child's effective `children()`.
65
+ *
66
+ * It lives HERE rather than in any one consumer because every consumer binds
67
+ * ONE name unconditionally. It used to live only in codegen-ts's
68
+ * `resolveObjectNames`, which runs only when the `names` generator is in the
69
+ * run — so with `namesFile()` unwired `meta migrate` emitted DDL against the
70
+ * PARENT's table (via `resolveTableName` → `buildExpectedSchema`) and
71
+ * `ObjectManager` read and wrote it, silently, on every run. A refusal that
72
+ * depends on which consumer asked is not a refusal. Mirrors Python's
73
+ * `source_resolution.primary_rdb_source`, whose codegen, api-docs and runtime
74
+ * callers all inherit it for free.
75
+ *
76
+ * DIRECTION-BLIND: it compares every primary against every other, so it does
77
+ * not matter which of them is writable nor which was declared first. Comparing
78
+ * against the first primary WRITABLE source can only see a divergence when one
79
+ * of the two is read-only — and, since `children()` places inherited entries
80
+ * first, only when the read-only one is the inherited one.
81
+ *
82
+ * Two primaries AGREEING on an address is not a divergence and stays legal: the
83
+ * invariant is that an object has ONE address, not that it declares one source.
84
+ * A read-only primary beside a non-primary REPLICA does not reach it either — a
85
+ * replica is not `role === "primary"`.
86
+ */
87
+ export declare function sourceAddressKey(source: MetaSource): string;
88
+ export declare function primaryRdbSource(entity: MetaData): MetaSource | undefined;
25
89
  export declare function resolveTableName(entity: MetaData): string;
26
90
  export declare function resolveColumnName(field: MetaData, strategy?: ColumnNamingStrategy): string;
91
+ /**
92
+ * An index's DATABASE name — for an `identity.secondary` (a unique alternate key) or an
93
+ * `index.lookup` (a non-unique retrieval index).
94
+ *
95
+ * These nodes carry no `@column`-style physical spelling: the database name IS the
96
+ * metamodel `name`. That is precisely why the answer must live in a function rather than
97
+ * at each call site. It was spelled independently in three places — the Drizzle emitter,
98
+ * migrate's expected-schema (twice) and the Kotlin Exposed emitter — and agreed only by
99
+ * coincidence; `fdb4118f1` is what that coincidence lapsing looks like, with codegen
100
+ * declaring `idx_<table>_<col>` while the index in the database was `identity.name`.
101
+ *
102
+ * Two rules the single door now owns, neither of which any call site had:
103
+ *
104
+ * - **Package qualifier stripped — and the reason first given for this was WRONG.** It was
105
+ * justified here as compensating for the JVM loader package-qualifying a nested index
106
+ * name (`acme::demo::by_name`), which `KotlinExposedTableGenerator` mirrored with
107
+ * `shortName ?: name`. Measured against the real JVM loader, that does not happen: a
108
+ * nested `identity.secondary` or `index.lookup` is named flat, including when inherited
109
+ * across packages via `extends`. Only a ROOT-level node takes the file's package, and an
110
+ * unnamed `view` child gets a synthesised FQN — the likeliest source of the belief. The
111
+ * JVM's local strip had been a no-op for its whole life while reading as the site that
112
+ * owned the rule. Pinned now on that side by `IndexNamingTest`.
113
+ *
114
+ * The strip stays, on the honest reason rather than the invented one: it is a no-op on
115
+ * every name either loader produces, which is what a rule that holds without a per-port
116
+ * branch looks like, and it costs one `lastIndexOf`. A normalisation nobody has to
117
+ * remember is worth more than a claim nobody checked.
118
+ * - **An empty name is REFUSED**, and the gap it closes is exactly one node type wide.
119
+ * An `identity.secondary` with an empty name is already refused by the LOADER in strict
120
+ * and lax mode alike (identity nodes carry an FR-024 name check so a dotted `extends`
121
+ * ref can address them). An `index.lookup` is not addressable that way and carries no
122
+ * such check, so `{"index.lookup": {"name": ""}}` loads with zero errors in both modes
123
+ * and reaches the emitters, which produce `index("")`: SQL no engine accepts, from a
124
+ * model that passed every gate. Refusing at the shared door closes it for codegen and
125
+ * migrate at once, without touching the byte-gated registry `rules` prose a loader-side
126
+ * fix would need. Measured, not assumed — `resolve-index-name.test.ts` asserts both
127
+ * arms, because "the loader already handles it" is the belief that would delete this.
128
+ */
129
+ export declare function resolveIndexName(node: {
130
+ readonly name: string;
131
+ readonly type: string;
132
+ readonly subType: string;
133
+ }): string;
27
134
  /**
28
135
  * Returns the DB schema declared on an entity's primary source child, or undefined
29
136
  * 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAe3D;AAED,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,104 @@ 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
+ * ADDRESS has no single answer to give, so this throws rather than picking one.
73
+ * The address is `(kind, schema, physical name)` — see `sourceAddressKey`. It was
74
+ * once the bare physical name, which was a WEAKER key than any consumer's: migrate
75
+ * keys every table it compares by schema-or-default + name, Postgres DDL is
76
+ * `CREATE TABLE "s"."t"`, and since 0.25.0 generated code binds the schema half too
77
+ * (`pgSchema(...).table(...)`, `[Table(..., Schema = ...)]`). Two primaries agreeing
78
+ * on `@table` and disagreeing on `@schema` therefore passed here and were refused by
79
+ * the names artifact, so `meta gen` failed on a model every other door admitted.
80
+ *
81
+ * What decided it was not the asymmetry but WHICH source the weaker key returned:
82
+ * `primaries[0]` is the INHERITED source in TS/C#/Python (`children()` puts the
83
+ * super's entries first) and the OWN source on the JVM, so such a model silently
84
+ * bound one schema in three ports and the other in two — one document, two verdicts
85
+ * depending on which toolchain read it.
86
+ * The shape loads with ZERO errors: `validateSourceRoles` enforces "exactly one
87
+ * primary" over `ownChildren()` only, and `_effectiveChildren` shadows an own
88
+ * child over a super child only on a (type, name) match — so two `source.rdb`
89
+ * nodes with DIFFERENT explicit names at two levels of an `extends` chain never
90
+ * collide, and both land on the child's effective `children()`.
91
+ *
92
+ * It lives HERE rather than in any one consumer because every consumer binds
93
+ * ONE name unconditionally. It used to live only in codegen-ts's
94
+ * `resolveObjectNames`, which runs only when the `names` generator is in the
95
+ * run — so with `namesFile()` unwired `meta migrate` emitted DDL against the
96
+ * PARENT's table (via `resolveTableName` → `buildExpectedSchema`) and
97
+ * `ObjectManager` read and wrote it, silently, on every run. A refusal that
98
+ * depends on which consumer asked is not a refusal. Mirrors Python's
99
+ * `source_resolution.primary_rdb_source`, whose codegen, api-docs and runtime
100
+ * callers all inherit it for free.
101
+ *
102
+ * DIRECTION-BLIND: it compares every primary against every other, so it does
103
+ * not matter which of them is writable nor which was declared first. Comparing
104
+ * against the first primary WRITABLE source can only see a divergence when one
105
+ * of the two is read-only — and, since `children()` places inherited entries
106
+ * first, only when the read-only one is the inherited one.
107
+ *
108
+ * Two primaries AGREEING on an address is not a divergence and stays legal: the
109
+ * invariant is that an object has ONE address, not that it declares one source.
110
+ * A read-only primary beside a non-primary REPLICA does not reach it either — a
111
+ * replica is not `role === "primary"`.
112
+ */
113
+ export function sourceAddressKey(source) {
114
+ // The RESOLVED address, and the same three parts the names artifact compares — so the
115
+ // shared authority and the generated artifact cannot answer "is this one object?"
116
+ // differently. Rendered rather than structural because every port has to produce a
117
+ // byte-identical message from it.
118
+ //
119
+ // RAW, deliberately: an absent `@schema` is NOT folded into a dialect default. On
120
+ // Postgres absent and "public" address the same relation, but on SQLite/D1 they do
121
+ // not — `migrate-ts`'s expected-schema builder throws on ANY declared schema, "public"
122
+ // included, while an absent one is fine. `resolveTableSchema` already documents that
123
+ // deciding what "undefined" means belongs to the caller's dialect, not to this layer,
124
+ // so normalizing here would import one dialect's rule into a dialect-free tier.
125
+ const schema = source.attr(SOURCE_ATTR_SCHEMA);
126
+ const qualifier = typeof schema === "string" && schema !== "" ? `"${schema}".` : "";
127
+ return `${qualifier}"${source.physicalName}" (${source.effectiveKind})`;
128
+ }
129
+ export function primaryRdbSource(entity) {
130
+ const primaries = entity.children().filter((c) => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY);
131
+ if (primaries.length === 0)
132
+ return undefined;
133
+ const distinct = [...new Set(primaries.map(sourceAddressKey))].sort();
134
+ if (distinct.length > 1) {
135
+ // Sorted, so the message is identical in every port regardless of children() order.
136
+ const joined = distinct.join(" vs ");
137
+ throw new MetaModelError(`${entity.name}: role=primary sources disagree on the object's physical address — ` +
138
+ `${joined}. Every consumer binds ONE address. Give them a matching @kind, @schema ` +
139
+ `and physical name, or drop the extra role=primary declaration.`);
140
+ }
141
+ return primaries[0];
142
+ }
50
143
  export function resolveTableName(entity) {
51
144
  // FR-016: primary source's `physicalName` implements the four-step rule
52
145
  // (kind-matching alias → legacy @table → source.name → entity-name fallback),
53
146
  // so this helper now just delegates. Writability (table vs view/storedProc/
54
147
  // tableFunction) only affects write-routing — for SELECT-side name resolution,
55
148
  // 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);
149
+ const source = primaryRdbSource(entity);
63
150
  if (source !== undefined)
64
151
  return source.physicalName;
65
152
  return pluralize(toSnakeCase(entity.name));
@@ -71,6 +158,57 @@ export function resolveColumnName(field, strategy = DEFAULT_COLUMN_NAMING_STRATE
71
158
  return col;
72
159
  return applyColumnNamingStrategy(field.name, strategy);
73
160
  }
161
+ /**
162
+ * An index's DATABASE name — for an `identity.secondary` (a unique alternate key) or an
163
+ * `index.lookup` (a non-unique retrieval index).
164
+ *
165
+ * These nodes carry no `@column`-style physical spelling: the database name IS the
166
+ * metamodel `name`. That is precisely why the answer must live in a function rather than
167
+ * at each call site. It was spelled independently in three places — the Drizzle emitter,
168
+ * migrate's expected-schema (twice) and the Kotlin Exposed emitter — and agreed only by
169
+ * coincidence; `fdb4118f1` is what that coincidence lapsing looks like, with codegen
170
+ * declaring `idx_<table>_<col>` while the index in the database was `identity.name`.
171
+ *
172
+ * Two rules the single door now owns, neither of which any call site had:
173
+ *
174
+ * - **Package qualifier stripped — and the reason first given for this was WRONG.** It was
175
+ * justified here as compensating for the JVM loader package-qualifying a nested index
176
+ * name (`acme::demo::by_name`), which `KotlinExposedTableGenerator` mirrored with
177
+ * `shortName ?: name`. Measured against the real JVM loader, that does not happen: a
178
+ * nested `identity.secondary` or `index.lookup` is named flat, including when inherited
179
+ * across packages via `extends`. Only a ROOT-level node takes the file's package, and an
180
+ * unnamed `view` child gets a synthesised FQN — the likeliest source of the belief. The
181
+ * JVM's local strip had been a no-op for its whole life while reading as the site that
182
+ * owned the rule. Pinned now on that side by `IndexNamingTest`.
183
+ *
184
+ * The strip stays, on the honest reason rather than the invented one: it is a no-op on
185
+ * every name either loader produces, which is what a rule that holds without a per-port
186
+ * branch looks like, and it costs one `lastIndexOf`. A normalisation nobody has to
187
+ * remember is worth more than a claim nobody checked.
188
+ * - **An empty name is REFUSED**, and the gap it closes is exactly one node type wide.
189
+ * An `identity.secondary` with an empty name is already refused by the LOADER in strict
190
+ * and lax mode alike (identity nodes carry an FR-024 name check so a dotted `extends`
191
+ * ref can address them). An `index.lookup` is not addressable that way and carries no
192
+ * such check, so `{"index.lookup": {"name": ""}}` loads with zero errors in both modes
193
+ * and reaches the emitters, which produce `index("")`: SQL no engine accepts, from a
194
+ * model that passed every gate. Refusing at the shared door closes it for codegen and
195
+ * migrate at once, without touching the byte-gated registry `rules` prose a loader-side
196
+ * fix would need. Measured, not assumed — `resolve-index-name.test.ts` asserts both
197
+ * arms, because "the loader already handles it" is the belief that would delete this.
198
+ */
199
+ export function resolveIndexName(
200
+ // The narrow structural shape rather than `MetaData`: this reads three properties, and
201
+ // every caller that has a real node satisfies it, while the Drizzle emitter's local
202
+ // duck-typed index node does not need a cast to pass one. A cast here would be the
203
+ // usual way a `never` slips past the compiler into a runtime property read.
204
+ node) {
205
+ const short = stripPackage(node.name);
206
+ if (short === "") {
207
+ throw new MetaModelError(`${node.type}.${node.subType} declares an empty name; an index's database name IS ` +
208
+ `its metamodel name, so there is nothing to emit. Give it a name.`);
209
+ }
210
+ return short;
211
+ }
74
212
  /**
75
213
  * Returns the DB schema declared on an entity's primary source child, or undefined
76
214
  * when no @schema attr is set or no source child exists. @schema is paradigm-agnostic
@@ -80,7 +218,10 @@ export function resolveColumnName(field, strategy = DEFAULT_COLUMN_NAMING_STRATE
80
218
  */
81
219
  export function resolveTableSchema(entity) {
82
220
  // 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);
221
+ // primaryRdbSource, not a second hand-rolled scan: a lookup written twice is a
222
+ // lookup that can disagree with itself, and only one of the two copies would
223
+ // carry the divergence refusal.
224
+ const source = primaryRdbSource(entity);
84
225
  if (!source)
85
226
  return undefined;
86
227
  // 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);