@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.
- package/dist/attr-schema-validate.js +15 -3
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/attr/attr-definition.embedded.js +1 -1
- package/dist/core/attr/attr-definition.embedded.js.map +1 -1
- package/dist/core/field/field-definition.embedded.js +1 -1
- package/dist/core/field/field-definition.embedded.js.map +1 -1
- package/dist/core/object/meta-object.d.ts +2 -0
- package/dist/core/object/meta-object.d.ts.map +1 -1
- package/dist/core/object/meta-object.js +20 -0
- package/dist/core/object/meta-object.js.map +1 -1
- package/dist/core/object/object-definition.embedded.js +1 -1
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core/relationship/relationship-definition.embedded.js +1 -1
- package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
- package/dist/core/validator/validator-definition.embedded.js +1 -1
- package/dist/core/validator/validator-definition.embedded.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +11 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/naming.d.ts +107 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +155 -14
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +130 -11
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/persistence/source/source-definition.embedded.js +1 -1
- package/dist/persistence/source/source-definition.embedded.js.map +1 -1
- package/dist/presentation/layout/layout-definition.embedded.js +1 -1
- package/dist/presentation/layout/layout-definition.embedded.js.map +1 -1
- package/dist/presentation/view/view-definition.embedded.js +1 -1
- package/dist/presentation/view/view-definition.embedded.js.map +1 -1
- package/dist/registry-coverage.d.ts +0 -6
- package/dist/registry-coverage.d.ts.map +1 -1
- package/dist/registry-coverage.js +25 -1
- package/dist/registry-coverage.js.map +1 -1
- package/dist/registry-manifest.d.ts +13 -6
- package/dist/registry-manifest.d.ts.map +1 -1
- package/dist/registry-manifest.js +6 -5
- package/dist/registry-manifest.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +23 -0
- package/dist/retired-vocabulary.d.ts.map +1 -1
- package/dist/retired-vocabulary.js +99 -1
- package/dist/retired-vocabulary.js.map +1 -1
- package/dist/shared/meta-data.d.ts.map +1 -1
- package/dist/shared/meta-data.js +23 -1
- package/dist/shared/meta-data.js.map +1 -1
- package/dist/template/template-definition.embedded.js +1 -1
- package/dist/template/template-definition.embedded.js.map +1 -1
- package/dist/vocabulary-catalog.d.ts +49 -0
- package/dist/vocabulary-catalog.d.ts.map +1 -0
- package/dist/vocabulary-catalog.js +80 -0
- package/dist/vocabulary-catalog.js.map +1 -0
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +15 -3
- package/src/core/attr/attr-definition.embedded.ts +1 -1
- package/src/core/field/field-definition.embedded.ts +1 -1
- package/src/core/object/meta-object.ts +22 -0
- package/src/core/object/object-definition.embedded.ts +1 -1
- package/src/core/relationship/relationship-definition.embedded.ts +1 -1
- package/src/core/validator/validator-definition.embedded.ts +1 -1
- package/src/errors.ts +11 -1
- package/src/index.ts +8 -1
- package/src/naming.ts +164 -18
- package/src/parser-core.ts +142 -12
- package/src/persistence/origin/origin-definition.embedded.ts +1 -1
- package/src/persistence/source/source-definition.embedded.ts +1 -1
- package/src/presentation/layout/layout-definition.embedded.ts +1 -1
- package/src/presentation/view/view-definition.embedded.ts +1 -1
- package/src/registry-coverage.ts +25 -3
- package/src/registry-manifest.ts +9 -8
- package/src/retired-vocabulary.ts +107 -1
- package/src/shared/meta-data.ts +23 -3
- package/src/template/template-definition.embedded.ts +1 -1
- 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
|
package/dist/naming.d.ts.map
CHANGED
|
@@ -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;
|
|
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,
|
|
6
|
-
// are EXPORTED helpers that run on a caller-supplied node — migrate-ts
|
|
7
|
-
// on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata
|
|
8
|
-
// class check would be false for a real primary source, and resolveTableName
|
|
9
|
-
// silently fall through to the entity-name fallback: a DIFFERENT table name,
|
|
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
|
-
|
|
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.
|
package/dist/naming.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,0CAA0C,CAAC;AAElD,
|
|
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;
|
|
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"}
|
package/dist/parser-core.js
CHANGED
|
@@ -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
|
-
|
|
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"), {
|
|
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);
|