@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/src/errors.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { ErrorSource, LoaderError, NodeContext } from "./source.js";
|
|
|
6
6
|
// NOTE: The following codes are forward-declared (no emitting site in the current
|
|
7
7
|
// TS parser/loader — the condition is not yet detected):
|
|
8
8
|
// - ERR_DUPLICATE_NAME: parser silently reuses existing same-name nodes (find-or-create).
|
|
9
|
-
// - ERR_MISSING_SUBTYPE: missing subType is resolved to the registry default, never an error.
|
|
10
9
|
// - ERR_INVALID_SUBTYPE_CHILD: no child-rule validation pass exists yet.
|
|
11
10
|
// Cross-language conformance consumers should not expect these codes from the TS adapter.
|
|
12
11
|
//
|
|
@@ -20,6 +19,17 @@ export const ERROR_CODES = [
|
|
|
20
19
|
"ERR_TOP_LEVEL_NOT_OBJECT",
|
|
21
20
|
"ERR_UNKNOWN_TYPE",
|
|
22
21
|
"ERR_UNKNOWN_SUBTYPE",
|
|
22
|
+
// A document AUTHORS a `<type>.base` node. Every registered `base` subtype is an
|
|
23
|
+
// abstract registry anchor — the shared root concrete subtypes inherit from, with no
|
|
24
|
+
// runtime semantics of its own (spec/metamodel/object.json says so in as many words:
|
|
25
|
+
// "not authored directly"). The JVM enforced this by accident, because its impl
|
|
26
|
+
// classes are `public abstract`; TypeScript, C# and Python accepted it, so the same
|
|
27
|
+
// document loaded on three ports and failed on two.
|
|
28
|
+
//
|
|
29
|
+
// Scoped to an EXPLICITLY authored subtype. `base` is also the parser's own fallback
|
|
30
|
+
// for an OMITTED subtype whose registry default is unregistered, and that path is
|
|
31
|
+
// untouched — refusing it would break every node that relies on the default.
|
|
32
|
+
"ERR_ABSTRACT_SUBTYPE_AUTHORED",
|
|
23
33
|
"ERR_MISSING_SUBTYPE",
|
|
24
34
|
"ERR_DUPLICATE_NAME",
|
|
25
35
|
"ERR_UNRESOLVED_SUPER",
|
package/src/index.ts
CHANGED
|
@@ -178,6 +178,11 @@ export { validateConstraints } from "./constraint-validate.js";
|
|
|
178
178
|
|
|
179
179
|
// Registry conformance manifest (SP-G) — the canonical logical-vocabulary serializer.
|
|
180
180
|
export { buildRegistryManifest, emitRegistryManifest, classifyPerTypeAttr, METAMODEL_VERSION } from "./registry-manifest.js";
|
|
181
|
+
// #357 — the AUTHORING-facing twin of buildRegistryManifest: every registered
|
|
182
|
+
// (type, subType) this port accepts, with the cross-port carve-outs MARKED rather
|
|
183
|
+
// than dropped. `meta types` reads this; the five-port byte gate reads the manifest.
|
|
184
|
+
export { buildVocabularyCatalog } from "./vocabulary-catalog.js";
|
|
185
|
+
export type { VocabularyCatalog, VocabularyType } from "./vocabulary-catalog.js";
|
|
181
186
|
export type { AttrClassification } from "./registry-manifest.js";
|
|
182
187
|
export { ExclusionReason } from "./registry-manifest-exclusions.js";
|
|
183
188
|
|
|
@@ -282,7 +287,8 @@ export type { AttrSchemaValidationResult } from "./attr-schema-validate.js";
|
|
|
282
287
|
export {
|
|
283
288
|
toSnakeCase, toKebabCase, pluralize,
|
|
284
289
|
applyColumnNamingStrategy, DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
285
|
-
resolveTableName, resolveColumnName, resolveTableSchema,
|
|
290
|
+
resolveTableName, resolveColumnName, resolveTableSchema, resolveIndexName,
|
|
291
|
+
primaryRdbSource, sourceAddressKey,
|
|
286
292
|
buildNameMap,
|
|
287
293
|
stripPackage,
|
|
288
294
|
} from "./naming.js";
|
|
@@ -297,6 +303,7 @@ export {
|
|
|
297
303
|
retiredAttrValue,
|
|
298
304
|
retiredSubType,
|
|
299
305
|
retirementHint,
|
|
306
|
+
retirementSuggestions,
|
|
300
307
|
} from "./retired-vocabulary.js";
|
|
301
308
|
export type { RetirementNote, RetiredEntry, VocabularyRewrite } from "./retired-vocabulary.js";
|
|
302
309
|
// Its sibling: pairs of LIVE attributes that may not sit on one node. Same two consumers,
|
package/src/naming.ts
CHANGED
|
@@ -7,13 +7,15 @@ import {
|
|
|
7
7
|
SOURCE_ROLE_PRIMARY,
|
|
8
8
|
} from "./persistence/source/source-constants.js";
|
|
9
9
|
import type { MetaSource } from "./persistence/source/meta-source.js";
|
|
10
|
-
// isMetaSource, not `instanceof`: unlike the loader-internal validators,
|
|
11
|
-
// are EXPORTED helpers that run on a caller-supplied node — migrate-ts
|
|
12
|
-
// on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata
|
|
13
|
-
// class check would be false for a real primary source, and resolveTableName
|
|
14
|
-
// silently fall through to the entity-name fallback: a DIFFERENT table name,
|
|
15
|
-
// migrate then emits as a rename against a live database.
|
|
10
|
+
// isMetaSource, not `instanceof`: unlike the loader-internal validators, the source
|
|
11
|
+
// lookups below are EXPORTED helpers that run on a caller-supplied node — migrate-ts
|
|
12
|
+
// calls them on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata
|
|
13
|
+
// tree the class check would be false for a real primary source, and resolveTableName
|
|
14
|
+
// would silently fall through to the entity-name fallback: a DIFFERENT table name,
|
|
15
|
+
// which migrate then emits as a rename against a live database. (The same split would
|
|
16
|
+
// also make the divergence refusal see no primaries at all, and say nothing.)
|
|
16
17
|
import { isMetaSource } from "./shared/node-guards.js";
|
|
18
|
+
import { MetaModelError } from "./errors.js";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Strip the package prefix from a metadata-qualified name
|
|
@@ -66,21 +68,109 @@ export function pluralize(s: string): string {
|
|
|
66
68
|
return s + "s";
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
/**
|
|
72
|
+
* THE `@role: primary` source lookup for this package — and the one place the
|
|
73
|
+
* primary-source DIVERGENCE refusal lives, so that every caller inherits it.
|
|
74
|
+
*
|
|
75
|
+
* Effective children (own + inherited via the super chain) so a TPH SUBTYPE —
|
|
76
|
+
* which declares no source of its own and inherits the discriminator base's
|
|
77
|
+
* single table (FR-017) — resolves to that base source rather than to nothing.
|
|
78
|
+
* For an entity declaring its own source, own shadows inherited, so the result
|
|
79
|
+
* is unchanged. Returns `undefined` when `entity` declares no primary source at
|
|
80
|
+
* all: #248, participation in persistence derives from a declared source, never
|
|
81
|
+
* from the object subtype, so an `object.value` and a sourceless
|
|
82
|
+
* `object.projection` both land here rather than being special-cased.
|
|
83
|
+
*
|
|
84
|
+
* `isMetaSource`, not `instanceof`: under two physical copies of this package a
|
|
85
|
+
* class check is false for a real primary source, and the failure is SILENT —
|
|
86
|
+
* the object reads as "not backed by any store".
|
|
87
|
+
*
|
|
88
|
+
* ## The refusal
|
|
89
|
+
*
|
|
90
|
+
* An object whose `@role: primary` sources resolve to MORE THAN ONE physical
|
|
91
|
+
* ADDRESS has no single answer to give, so this throws rather than picking one.
|
|
92
|
+
* The address is `(kind, schema, physical name)` — see `sourceAddressKey`. It was
|
|
93
|
+
* once the bare physical name, which was a WEAKER key than any consumer's: migrate
|
|
94
|
+
* keys every table it compares by schema-or-default + name, Postgres DDL is
|
|
95
|
+
* `CREATE TABLE "s"."t"`, and since 0.25.0 generated code binds the schema half too
|
|
96
|
+
* (`pgSchema(...).table(...)`, `[Table(..., Schema = ...)]`). Two primaries agreeing
|
|
97
|
+
* on `@table` and disagreeing on `@schema` therefore passed here and were refused by
|
|
98
|
+
* the names artifact, so `meta gen` failed on a model every other door admitted.
|
|
99
|
+
*
|
|
100
|
+
* What decided it was not the asymmetry but WHICH source the weaker key returned:
|
|
101
|
+
* `primaries[0]` is the INHERITED source in TS/C#/Python (`children()` puts the
|
|
102
|
+
* super's entries first) and the OWN source on the JVM, so such a model silently
|
|
103
|
+
* bound one schema in three ports and the other in two — one document, two verdicts
|
|
104
|
+
* depending on which toolchain read it.
|
|
105
|
+
* The shape loads with ZERO errors: `validateSourceRoles` enforces "exactly one
|
|
106
|
+
* primary" over `ownChildren()` only, and `_effectiveChildren` shadows an own
|
|
107
|
+
* child over a super child only on a (type, name) match — so two `source.rdb`
|
|
108
|
+
* nodes with DIFFERENT explicit names at two levels of an `extends` chain never
|
|
109
|
+
* collide, and both land on the child's effective `children()`.
|
|
110
|
+
*
|
|
111
|
+
* It lives HERE rather than in any one consumer because every consumer binds
|
|
112
|
+
* ONE name unconditionally. It used to live only in codegen-ts's
|
|
113
|
+
* `resolveObjectNames`, which runs only when the `names` generator is in the
|
|
114
|
+
* run — so with `namesFile()` unwired `meta migrate` emitted DDL against the
|
|
115
|
+
* PARENT's table (via `resolveTableName` → `buildExpectedSchema`) and
|
|
116
|
+
* `ObjectManager` read and wrote it, silently, on every run. A refusal that
|
|
117
|
+
* depends on which consumer asked is not a refusal. Mirrors Python's
|
|
118
|
+
* `source_resolution.primary_rdb_source`, whose codegen, api-docs and runtime
|
|
119
|
+
* callers all inherit it for free.
|
|
120
|
+
*
|
|
121
|
+
* DIRECTION-BLIND: it compares every primary against every other, so it does
|
|
122
|
+
* not matter which of them is writable nor which was declared first. Comparing
|
|
123
|
+
* against the first primary WRITABLE source can only see a divergence when one
|
|
124
|
+
* of the two is read-only — and, since `children()` places inherited entries
|
|
125
|
+
* first, only when the read-only one is the inherited one.
|
|
126
|
+
*
|
|
127
|
+
* Two primaries AGREEING on an address is not a divergence and stays legal: the
|
|
128
|
+
* invariant is that an object has ONE address, not that it declares one source.
|
|
129
|
+
* A read-only primary beside a non-primary REPLICA does not reach it either — a
|
|
130
|
+
* replica is not `role === "primary"`.
|
|
131
|
+
*/
|
|
132
|
+
export function sourceAddressKey(source: MetaSource): string {
|
|
133
|
+
// The RESOLVED address, and the same three parts the names artifact compares — so the
|
|
134
|
+
// shared authority and the generated artifact cannot answer "is this one object?"
|
|
135
|
+
// differently. Rendered rather than structural because every port has to produce a
|
|
136
|
+
// byte-identical message from it.
|
|
137
|
+
//
|
|
138
|
+
// RAW, deliberately: an absent `@schema` is NOT folded into a dialect default. On
|
|
139
|
+
// Postgres absent and "public" address the same relation, but on SQLite/D1 they do
|
|
140
|
+
// not — `migrate-ts`'s expected-schema builder throws on ANY declared schema, "public"
|
|
141
|
+
// included, while an absent one is fine. `resolveTableSchema` already documents that
|
|
142
|
+
// deciding what "undefined" means belongs to the caller's dialect, not to this layer,
|
|
143
|
+
// so normalizing here would import one dialect's rule into a dialect-free tier.
|
|
144
|
+
const schema = source.attr(SOURCE_ATTR_SCHEMA);
|
|
145
|
+
const qualifier = typeof schema === "string" && schema !== "" ? `"${schema}".` : "";
|
|
146
|
+
return `${qualifier}"${source.physicalName}" (${source.effectiveKind})`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function primaryRdbSource(entity: MetaData): MetaSource | undefined {
|
|
150
|
+
const primaries = entity.children().filter(
|
|
151
|
+
(c): c is MetaSource => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY,
|
|
152
|
+
);
|
|
153
|
+
if (primaries.length === 0) return undefined;
|
|
154
|
+
const distinct = [...new Set(primaries.map(sourceAddressKey))].sort();
|
|
155
|
+
if (distinct.length > 1) {
|
|
156
|
+
// Sorted, so the message is identical in every port regardless of children() order.
|
|
157
|
+
const joined = distinct.join(" vs ");
|
|
158
|
+
throw new MetaModelError(
|
|
159
|
+
`${entity.name}: role=primary sources disagree on the object's physical address — ` +
|
|
160
|
+
`${joined}. Every consumer binds ONE address. Give them a matching @kind, @schema ` +
|
|
161
|
+
`and physical name, or drop the extra role=primary declaration.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return primaries[0];
|
|
165
|
+
}
|
|
166
|
+
|
|
69
167
|
export function resolveTableName(entity: MetaData): string {
|
|
70
168
|
// FR-016: primary source's `physicalName` implements the four-step rule
|
|
71
169
|
// (kind-matching alias → legacy @table → source.name → entity-name fallback),
|
|
72
170
|
// so this helper now just delegates. Writability (table vs view/storedProc/
|
|
73
171
|
// tableFunction) only affects write-routing — for SELECT-side name resolution,
|
|
74
172
|
// a read-only primary source is the right answer.
|
|
75
|
-
|
|
76
|
-
// Effective children (own + inherited via the super chain) so a TPH SUBTYPE
|
|
77
|
-
// — which declares no source of its own and inherits the discriminator base's
|
|
78
|
-
// single table (FR-017) — resolves to that base table rather than the
|
|
79
|
-
// entity-name fallback. For an entity declaring its own source, own shadows
|
|
80
|
-
// inherited, so the result is unchanged.
|
|
81
|
-
const source = entity.children().find(
|
|
82
|
-
(c): c is MetaSource => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY,
|
|
83
|
-
);
|
|
173
|
+
const source = primaryRdbSource(entity);
|
|
84
174
|
if (source !== undefined) return source.physicalName;
|
|
85
175
|
return pluralize(toSnakeCase(entity.name));
|
|
86
176
|
}
|
|
@@ -95,6 +185,61 @@ export function resolveColumnName(
|
|
|
95
185
|
return applyColumnNamingStrategy(field.name, strategy);
|
|
96
186
|
}
|
|
97
187
|
|
|
188
|
+
/**
|
|
189
|
+
* An index's DATABASE name — for an `identity.secondary` (a unique alternate key) or an
|
|
190
|
+
* `index.lookup` (a non-unique retrieval index).
|
|
191
|
+
*
|
|
192
|
+
* These nodes carry no `@column`-style physical spelling: the database name IS the
|
|
193
|
+
* metamodel `name`. That is precisely why the answer must live in a function rather than
|
|
194
|
+
* at each call site. It was spelled independently in three places — the Drizzle emitter,
|
|
195
|
+
* migrate's expected-schema (twice) and the Kotlin Exposed emitter — and agreed only by
|
|
196
|
+
* coincidence; `fdb4118f1` is what that coincidence lapsing looks like, with codegen
|
|
197
|
+
* declaring `idx_<table>_<col>` while the index in the database was `identity.name`.
|
|
198
|
+
*
|
|
199
|
+
* Two rules the single door now owns, neither of which any call site had:
|
|
200
|
+
*
|
|
201
|
+
* - **Package qualifier stripped — and the reason first given for this was WRONG.** It was
|
|
202
|
+
* justified here as compensating for the JVM loader package-qualifying a nested index
|
|
203
|
+
* name (`acme::demo::by_name`), which `KotlinExposedTableGenerator` mirrored with
|
|
204
|
+
* `shortName ?: name`. Measured against the real JVM loader, that does not happen: a
|
|
205
|
+
* nested `identity.secondary` or `index.lookup` is named flat, including when inherited
|
|
206
|
+
* across packages via `extends`. Only a ROOT-level node takes the file's package, and an
|
|
207
|
+
* unnamed `view` child gets a synthesised FQN — the likeliest source of the belief. The
|
|
208
|
+
* JVM's local strip had been a no-op for its whole life while reading as the site that
|
|
209
|
+
* owned the rule. Pinned now on that side by `IndexNamingTest`.
|
|
210
|
+
*
|
|
211
|
+
* The strip stays, on the honest reason rather than the invented one: it is a no-op on
|
|
212
|
+
* every name either loader produces, which is what a rule that holds without a per-port
|
|
213
|
+
* branch looks like, and it costs one `lastIndexOf`. A normalisation nobody has to
|
|
214
|
+
* remember is worth more than a claim nobody checked.
|
|
215
|
+
* - **An empty name is REFUSED**, and the gap it closes is exactly one node type wide.
|
|
216
|
+
* An `identity.secondary` with an empty name is already refused by the LOADER in strict
|
|
217
|
+
* and lax mode alike (identity nodes carry an FR-024 name check so a dotted `extends`
|
|
218
|
+
* ref can address them). An `index.lookup` is not addressable that way and carries no
|
|
219
|
+
* such check, so `{"index.lookup": {"name": ""}}` loads with zero errors in both modes
|
|
220
|
+
* and reaches the emitters, which produce `index("")`: SQL no engine accepts, from a
|
|
221
|
+
* model that passed every gate. Refusing at the shared door closes it for codegen and
|
|
222
|
+
* migrate at once, without touching the byte-gated registry `rules` prose a loader-side
|
|
223
|
+
* fix would need. Measured, not assumed — `resolve-index-name.test.ts` asserts both
|
|
224
|
+
* arms, because "the loader already handles it" is the belief that would delete this.
|
|
225
|
+
*/
|
|
226
|
+
export function resolveIndexName(
|
|
227
|
+
// The narrow structural shape rather than `MetaData`: this reads three properties, and
|
|
228
|
+
// every caller that has a real node satisfies it, while the Drizzle emitter's local
|
|
229
|
+
// duck-typed index node does not need a cast to pass one. A cast here would be the
|
|
230
|
+
// usual way a `never` slips past the compiler into a runtime property read.
|
|
231
|
+
node: { readonly name: string; readonly type: string; readonly subType: string },
|
|
232
|
+
): string {
|
|
233
|
+
const short = stripPackage(node.name);
|
|
234
|
+
if (short === "") {
|
|
235
|
+
throw new MetaModelError(
|
|
236
|
+
`${node.type}.${node.subType} declares an empty name; an index's database name IS ` +
|
|
237
|
+
`its metamodel name, so there is nothing to emit. Give it a name.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return short;
|
|
241
|
+
}
|
|
242
|
+
|
|
98
243
|
/**
|
|
99
244
|
* Returns the DB schema declared on an entity's primary source child, or undefined
|
|
100
245
|
* when no @schema attr is set or no source child exists. @schema is paradigm-agnostic
|
|
@@ -104,9 +249,10 @@ export function resolveColumnName(
|
|
|
104
249
|
*/
|
|
105
250
|
export function resolveTableSchema(entity: MetaData): string | undefined {
|
|
106
251
|
// ADR-0039: resolving — a concrete entity may inherit its source.rdb via extends.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
252
|
+
// primaryRdbSource, not a second hand-rolled scan: a lookup written twice is a
|
|
253
|
+
// lookup that can disagree with itself, and only one of the two copies would
|
|
254
|
+
// carry the divergence refusal.
|
|
255
|
+
const source = primaryRdbSource(entity);
|
|
110
256
|
if (!source) return undefined;
|
|
111
257
|
// ADR-0039: resolving — an inherited source's @schema lives on the super node.
|
|
112
258
|
const schema = source.attr(SOURCE_ATTR_SCHEMA);
|
package/src/parser-core.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { MetaAttr } from "./core/attr/meta-attr.js";
|
|
|
30
30
|
import { canonicalSerialize, inferAttrSubType } from "./serializer-json.js";
|
|
31
31
|
import { ParseError, type ErrorCode } from "./errors.js";
|
|
32
32
|
// #337 — see retired-vocabulary.ts. Diagnostic only; no load outcome changes.
|
|
33
|
-
import { retiredSubType, retirementHint } from "./retired-vocabulary.js";
|
|
33
|
+
import { retiredSubType, retirementHint, retirementSuggestions } from "./retired-vocabulary.js";
|
|
34
34
|
import { resolvedSource, type ErrorSource, type LoaderWarning, type Contributor } from "./source.js";
|
|
35
35
|
import { semanticDiff } from "./semantic-diff.js";
|
|
36
36
|
import {
|
|
@@ -206,6 +206,64 @@ function guardRelativeRefInCanonical(
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// ---------------------------------------------------------------------------
|
|
209
|
+
// The one message for an authored `<type>.base`, so both doors say the same thing.
|
|
210
|
+
//
|
|
211
|
+
// Every registered `base` subtype is an ABSTRACT REGISTRY ANCHOR: the shared root that
|
|
212
|
+
// concrete subtypes inherit their attrs and child rules from. It has no runtime semantics
|
|
213
|
+
// and no concrete representation — `spec/metamodel/object.json` says so in as many words
|
|
214
|
+
// ("Has no runtime semantics of its own; not authored directly"), and every `base` entry's
|
|
215
|
+
// description opens with "Abstract".
|
|
216
|
+
//
|
|
217
|
+
// The JVM enforced this by accident (its impl classes are `public abstract`, so
|
|
218
|
+
// instantiation fails); TypeScript, C# and Python accepted it. The same document therefore
|
|
219
|
+
// loaded on three ports and failed to load on two — the cross-port conformance gap the
|
|
220
|
+
// corpora exist to catch, and it survived because every `*.base` subtype sits in the
|
|
221
|
+
// registry corpus's own `untestedSubTypes` list.
|
|
222
|
+
/**
|
|
223
|
+
* Is `<type>.base` an ABSTRACT ANCHOR for this type — i.e. does the type register at least one
|
|
224
|
+
* OTHER subtype for it to anchor?
|
|
225
|
+
*
|
|
226
|
+
* Registry-driven, not name-driven, and the distinction is load-bearing. All ten core anchors
|
|
227
|
+
* have concrete siblings (`object.base` beside entity/value/projection, `source.base` beside
|
|
228
|
+
* rdb, …), so the rule catches every one. A third-party provider may register `base` as a
|
|
229
|
+
* type's ONLY member — the SDK's forge `convention`/`glossary`/`failure` do — and there it is
|
|
230
|
+
* not anchoring anything: refusing it would leave the type unauthorable, which is a
|
|
231
|
+
* capability removal the contract never asked for. The manifest describes the CORE registry;
|
|
232
|
+
* this predicate is how that scope is expressed without hardcoding the core's type list.
|
|
233
|
+
*/
|
|
234
|
+
function isAbstractAnchorFor(type: string, registry: TypeRegistry): boolean {
|
|
235
|
+
return registry.allSubTypesOf(type).some((sub) => sub !== SUBTYPE_BASE);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function abstractSubtypeMessage(type: string): string {
|
|
239
|
+
return (
|
|
240
|
+
`"${type}.${SUBTYPE_BASE}" may not be authored — every "${SUBTYPE_BASE}" subtype is an ` +
|
|
241
|
+
`abstract registry anchor that concrete subtypes inherit from, with no runtime ` +
|
|
242
|
+
`semantics of its own. Declare a concrete ${type} subtype instead.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The same rule reached by the OTHER spelling: a BARE wrapper key (`{"field": …}`, no fused
|
|
247
|
+
// subType) whose registry default resolves to the abstract anchor. The author did not type
|
|
248
|
+
// `.base`, so this is a MISSING subtype rather than an authored-anchor error — and
|
|
249
|
+
// ERR_MISSING_SUBTYPE is the shared code already chartered for it ("a node omits subType and
|
|
250
|
+
// the type has no default subType").
|
|
251
|
+
//
|
|
252
|
+
// Python has always emitted it here; TypeScript, C# and the JVM did not, so a bare key was a
|
|
253
|
+
// SECOND way one document got two verdicts: TS and C# resolved it to the anchor and loaded,
|
|
254
|
+
// the JVM resolved it identically and then failed to instantiate with a message naming a
|
|
255
|
+
// missing constructor. Closing the authored spelling alone would have left the rule half
|
|
256
|
+
// true, reachable by dropping four characters.
|
|
257
|
+
//
|
|
258
|
+
// Scoped to "the default IS the anchor", never to bare keys as such: a type that declares a
|
|
259
|
+
// CONCRETE default keeps resolving through it, which is the door a future default-subtype
|
|
260
|
+
// decision walks through (ADR-0054's closing section).
|
|
261
|
+
function missingSubtypeMessage(type: string): string {
|
|
262
|
+
return (
|
|
263
|
+
`type "${type}" has no default subType; write the full "${type}.<subType>"`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
209
267
|
// Internal helper — split a fused wrapper key into (type, subType).
|
|
210
268
|
//
|
|
211
269
|
// Canonical JSON always writes the full `type.subType`. An omitted subType
|
|
@@ -220,20 +278,31 @@ interface SplitKey {
|
|
|
220
278
|
explicit: boolean;
|
|
221
279
|
}
|
|
222
280
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
281
|
+
/**
|
|
282
|
+
* The subType a BARE wrapper key (`{"object": …}`) resolves to: the type's DECLARED default,
|
|
283
|
+
* or `undefined` when it declares none.
|
|
284
|
+
*
|
|
285
|
+
* `registry.defaultSubTypeOf` is the same accessor the YAML desugar consults, and the shared
|
|
286
|
+
* corpus already pins the contract (`fixtures/yaml-conformance/yaml-bare-default-subtypes`:
|
|
287
|
+
* bare `object:` becomes `object.entity`). This used to guess instead — `allSubTypesOf()[0]`,
|
|
288
|
+
* i.e. registration order, falling back to `base` — so the two layers answered the same
|
|
289
|
+
* question two different ways. Registration order put `base` first, so a bare key in JSON
|
|
290
|
+
* resolved to the abstract anchor: it loaded here and in C#, while the JVM resolved it
|
|
291
|
+
* identically and then failed to INSTANTIATE, because its impl classes are abstract.
|
|
292
|
+
*
|
|
293
|
+
* A name resolved twice by two different functions is a name that can disagree with itself —
|
|
294
|
+
* the defect class this file's own header exists to prevent, reached through the one door
|
|
295
|
+
* nobody had pointed at the registry.
|
|
296
|
+
*/
|
|
297
|
+
function defaultSubTypeFor(type: string, registry: TypeRegistry): string | undefined {
|
|
298
|
+
return registry.defaultSubTypeOf(type);
|
|
230
299
|
}
|
|
231
300
|
|
|
232
301
|
function splitTypeKey(key: string, registry: TypeRegistry): SplitKey {
|
|
233
302
|
const dotIdx = key.indexOf(TYPE_SUBTYPE_SEPARATOR);
|
|
234
303
|
if (dotIdx < 0) {
|
|
235
304
|
// Bare type, no fused subType — resolve via the registry default.
|
|
236
|
-
return { type: key, subType: defaultSubTypeFor(key, registry), explicit: false };
|
|
305
|
+
return { type: key, subType: defaultSubTypeFor(key, registry) ?? "", explicit: false };
|
|
237
306
|
}
|
|
238
307
|
const type = key.slice(0, dotIdx);
|
|
239
308
|
const subType = key.slice(dotIdx + TYPE_SUBTYPE_SEPARATOR.length);
|
|
@@ -419,7 +488,31 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
419
488
|
}
|
|
420
489
|
|
|
421
490
|
const rootDataObj = rootData as Record<string, unknown>;
|
|
422
|
-
const { type: rootType, subType: rootSubType } =
|
|
491
|
+
const { type: rootType, subType: rootSubType, explicit: rootExplicit } =
|
|
492
|
+
splitTypeKey(rootKey, opts.registry);
|
|
493
|
+
|
|
494
|
+
// A `<type>.base` node may not be AUTHORED — see abstractSubtypeMessage. This is the
|
|
495
|
+
// ROOT door; the child door is in the child loop below. One rule, both doors: a check
|
|
496
|
+
// on one of two entry points is a rule that is only half true.
|
|
497
|
+
// Registration first: an UNREGISTERED type has no default either, so without this a typo'd
|
|
498
|
+
// root key is diagnosed as a registered type that merely lacks a default. The registration
|
|
499
|
+
// check below owns that case and phrases it correctly.
|
|
500
|
+
if (opts.registry.allSubTypesOf(rootType).length > 0
|
|
501
|
+
&& ((rootSubType === SUBTYPE_BASE && isAbstractAnchorFor(rootType, opts.registry))
|
|
502
|
+
|| (!rootExplicit && rootSubType === ""))) {
|
|
503
|
+
_currentPath!.pushKey(rootKey);
|
|
504
|
+
const src = errSource();
|
|
505
|
+
_currentPath!.pop();
|
|
506
|
+
throw rootExplicit
|
|
507
|
+
? new ParseError(abstractSubtypeMessage(rootType), {
|
|
508
|
+
code: "ERR_ABSTRACT_SUBTYPE_AUTHORED",
|
|
509
|
+
source: src,
|
|
510
|
+
})
|
|
511
|
+
: new ParseError(missingSubtypeMessage(rootType), {
|
|
512
|
+
code: "ERR_MISSING_SUBTYPE",
|
|
513
|
+
source: src,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
423
516
|
|
|
424
517
|
// Check root type is registered (always throw — can't skip the root)
|
|
425
518
|
if (!opts.registry.has(rootType, rootSubType)) {
|
|
@@ -436,7 +529,13 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
436
529
|
throw new ParseError(
|
|
437
530
|
`Unknown root type "${rootType}.${rootSubType}" — ` +
|
|
438
531
|
(retiredRoot !== undefined ? retirementHint(retiredRoot) : "not registered"),
|
|
439
|
-
{
|
|
532
|
+
{
|
|
533
|
+
code: rootTypeCode,
|
|
534
|
+
source: src,
|
|
535
|
+
...(retiredRoot !== undefined
|
|
536
|
+
? { suggestions: retirementSuggestions(retiredRoot) }
|
|
537
|
+
: {}),
|
|
538
|
+
},
|
|
440
539
|
);
|
|
441
540
|
}
|
|
442
541
|
|
|
@@ -1259,11 +1358,36 @@ function processChildren(
|
|
|
1259
1358
|
const childDataObj = childData as Record<string, unknown>;
|
|
1260
1359
|
const { type: childType, subType: childSubTypeRaw, explicit } = splitTypeKey(childKey, registry);
|
|
1261
1360
|
|
|
1361
|
+
let childSubType = childSubTypeRaw;
|
|
1362
|
+
// A `<type>.base` may not be AUTHORED, and a bare key for a type with no declared default
|
|
1363
|
+
// is a MISSING subType. BEFORE the registered-type check below, matching the root door and
|
|
1364
|
+
// the JVM: run it after, and a bare key for a type that registers no `base` at all
|
|
1365
|
+
// (identity, index, requirement) falls into that check's else-arm first and is reported as
|
|
1366
|
+
// an UNKNOWN TYPE — a message asserting the type does not exist, about a type that does,
|
|
1367
|
+
// with its name mangled to `"identity."`. Order is the whole rule here.
|
|
1368
|
+
if ((childSubType === SUBTYPE_BASE && isAbstractAnchorFor(childType, registry))
|
|
1369
|
+
|| (!explicit && childSubType === "")) {
|
|
1370
|
+
errors.push(
|
|
1371
|
+
explicit
|
|
1372
|
+
? new ParseError(abstractSubtypeMessage(childType), {
|
|
1373
|
+
code: "ERR_ABSTRACT_SUBTYPE_AUTHORED",
|
|
1374
|
+
source: errSource(),
|
|
1375
|
+
})
|
|
1376
|
+
: new ParseError(missingSubtypeMessage(childType), {
|
|
1377
|
+
code: "ERR_MISSING_SUBTYPE",
|
|
1378
|
+
source: errSource(),
|
|
1379
|
+
}),
|
|
1380
|
+
);
|
|
1381
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
1382
|
+
_currentPath?.pop(); // pop array index
|
|
1383
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
1384
|
+
continue; // skip this child
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1262
1387
|
// --- Check if this child type is registered ---
|
|
1263
1388
|
// An EXPLICIT unknown subType (fused into the key) is an error — never
|
|
1264
1389
|
// silently downgraded to base. An OMITTED subType that resolves to an
|
|
1265
1390
|
// unregistered default falls back to base.
|
|
1266
|
-
let childSubType = childSubTypeRaw;
|
|
1267
1391
|
if (!registry.has(childType, childSubType)) {
|
|
1268
1392
|
if (!explicit && registry.has(childType, SUBTYPE_BASE)) {
|
|
1269
1393
|
childSubType = SUBTYPE_BASE;
|
|
@@ -1288,6 +1412,12 @@ function processChildren(
|
|
|
1288
1412
|
}
|
|
1289
1413
|
}
|
|
1290
1414
|
|
|
1415
|
+
// A `<type>.base` child may not be AUTHORED — the CHILD door (the root door is above).
|
|
1416
|
+
// Gated on `explicit`, deliberately: `base` is also this parser's fallback for an
|
|
1417
|
+
// OMITTED subtype whose registry default is unregistered (the branch directly above),
|
|
1418
|
+
// and refusing that would break every node relying on the default. Placed before the
|
|
1419
|
+
// attr branch so `attr.base` is covered by the same rule — an authored untyped attr is
|
|
1420
|
+
// the same mistake as an authored untyped field.
|
|
1291
1421
|
// --- Special handling for "attr" child nodes ---
|
|
1292
1422
|
if (childType === TYPE_ATTR) {
|
|
1293
1423
|
parseAttrChild(parent, childType, childSubType, childDataObj, registry, warnings, strict, source, childNodePath);
|
|
@@ -12,7 +12,7 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
12
12
|
{
|
|
13
13
|
"type": "origin",
|
|
14
14
|
"subType": "base",
|
|
15
|
-
"description": "Abstract base origin — the shared root subtype for field-level provenance. A field carrying any origin.* is derived ⇒ read-only wherever it lives. The base carries no attrs of its own; concrete subtypes add their provenance attrs."
|
|
15
|
+
"description": "Abstract base origin — the shared root subtype for field-level provenance. A field carrying any origin.* is derived ⇒ read-only wherever it lives. The base carries no attrs of its own; concrete subtypes add their provenance attrs. Not authored directly: a `origin.base` node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED) — this subtype is a registry anchor concrete subtypes inherit from, never a node in a document."
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"type": "origin",
|
|
@@ -12,7 +12,7 @@ export const SOURCE_DEFINITION: ProviderDefinition = {
|
|
|
12
12
|
{
|
|
13
13
|
"type": "source",
|
|
14
14
|
"subType": "base",
|
|
15
|
-
"description": "Abstract base source — the shared root subtype for declaring where an object's data lives (Project E). The base carries no attrs of its own; the concrete paradigm subtype (rdb) carries the physical-storage attrs, which are contributed by the db domain provider."
|
|
15
|
+
"description": "Abstract base source — the shared root subtype for declaring where an object's data lives (Project E). The base carries no attrs of its own; the concrete paradigm subtype (rdb) carries the physical-storage attrs, which are contributed by the db domain provider. Not authored directly: a `source.base` node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED) — this subtype is a registry anchor concrete subtypes inherit from, never a node in a document."
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"type": "source",
|
|
@@ -12,7 +12,7 @@ export const LAYOUT_DEFINITION: ProviderDefinition = {
|
|
|
12
12
|
{
|
|
13
13
|
"type": "layout",
|
|
14
14
|
"subType": "base",
|
|
15
|
-
"description": "Abstract base layout — the shared root subtype for object-level UI surfaces. A layout attaches a presentation concern (grids, forms, tabs, cards) to an object. The base carries no attrs of its own; concrete subtypes add their presentation attrs."
|
|
15
|
+
"description": "Abstract base layout — the shared root subtype for object-level UI surfaces. A layout attaches a presentation concern (grids, forms, tabs, cards) to an object. The base carries no attrs of its own; concrete subtypes add their presentation attrs. Not authored directly: a `layout.base` node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED) — this subtype is a registry anchor concrete subtypes inherit from, never a node in a document."
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"type": "layout",
|
|
@@ -12,7 +12,7 @@ export const VIEW_DEFINITION: ProviderDefinition = {
|
|
|
12
12
|
{
|
|
13
13
|
"type": "view",
|
|
14
14
|
"subType": "base",
|
|
15
|
-
"description": "Abstract view base — the shared root subtype for field-level UI/render hints. A view declares how a field's value is rendered or edited; the base carries no attrs of its own."
|
|
15
|
+
"description": "Abstract view base — the shared root subtype for field-level UI/render hints. A view declares how a field's value is rendered or edited; the base carries no attrs of its own. Not authored directly: a `view.base` node fails to load (ERR_ABSTRACT_SUBTYPE_AUTHORED) — this subtype is a registry anchor concrete subtypes inherit from, never a node in a document."
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"type": "view",
|
package/src/registry-coverage.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
RESERVED_KEYS,
|
|
24
24
|
TYPE_SUBTYPE_SEPARATOR,
|
|
25
25
|
} from "./shared/structural.js";
|
|
26
|
+
import { SUBTYPE_BASE } from "./shared/base-types.js";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Manifest shape (a structural subset of fixtures/registry-conformance schema)
|
|
@@ -249,15 +250,35 @@ function compareStrings(a: string, b: string): number {
|
|
|
249
250
|
* @param manifest the parsed registry manifest (the registered vocabulary)
|
|
250
251
|
* @param roots fixture-corpus directory roots to scan for usage
|
|
251
252
|
*/
|
|
253
|
+
/**
|
|
254
|
+
* True for a `<type>.base` registry entry — an ABSTRACT ANCHOR, excluded from the coverage
|
|
255
|
+
* universe entirely.
|
|
256
|
+
*
|
|
257
|
+
* A `base` subtype is the shared root that concrete subtypes inherit their attrs and child
|
|
258
|
+
* rules from. It has no runtime semantics and no concrete representation, and authoring one
|
|
259
|
+
* is now a load error (ERR_ABSTRACT_SUBTYPE_AUTHORED), so no fixture can ever exercise it.
|
|
260
|
+
* Listing all ten as "untested vocabulary" described them as a backlog somebody could work
|
|
261
|
+
* off; they are the contract instead.
|
|
262
|
+
*
|
|
263
|
+
* Excluding them also keeps the attr accounting honest. The attrs a `base` entry declares
|
|
264
|
+
* are the ones its CONCRETE subtypes inherit — `@required` is declared on `field.base` and
|
|
265
|
+
* set on a `field.string` — so measuring them against fixtures that name `field.base` would
|
|
266
|
+
* report every shared field attr as untested the moment one negative fixture mentioned the
|
|
267
|
+
* subtype, which is what the corpus fixture for the new rule did.
|
|
268
|
+
*/
|
|
269
|
+
function isAbstractAnchor(type: { readonly subType: string }): boolean {
|
|
270
|
+
return type.subType === SUBTYPE_BASE;
|
|
271
|
+
}
|
|
272
|
+
|
|
252
273
|
export function computeCoverage(
|
|
253
274
|
manifest: RegistryManifest,
|
|
254
275
|
roots: readonly string[],
|
|
255
276
|
): CoverageReport {
|
|
256
277
|
const usage = scanFixtureUsage(roots);
|
|
257
278
|
|
|
258
|
-
const registeredKeys = manifest.types
|
|
259
|
-
(t) =>
|
|
260
|
-
|
|
279
|
+
const registeredKeys = manifest.types
|
|
280
|
+
.filter((t) => !isAbstractAnchor(t))
|
|
281
|
+
.map((t) => `${t.type}${TYPE_SUBTYPE_SEPARATOR}${t.subType}`);
|
|
261
282
|
|
|
262
283
|
const untestedSubTypes = registeredKeys
|
|
263
284
|
.filter((key) => !usage.subTypes.has(key))
|
|
@@ -268,6 +289,7 @@ export function computeCoverage(
|
|
|
268
289
|
// Per EXERCISED subtype, which declared attrs no fixture sets.
|
|
269
290
|
const untestedAttrsByExercisedSubType: UntestedAttrs[] = [];
|
|
270
291
|
for (const t of manifest.types) {
|
|
292
|
+
if (isAbstractAnchor(t)) continue; // abstract anchor — see isAbstractAnchor
|
|
271
293
|
const key = `${t.type}${TYPE_SUBTYPE_SEPARATOR}${t.subType}`;
|
|
272
294
|
if (!usage.subTypes.has(key)) continue; // skip untested subtypes (all-untested by definition)
|
|
273
295
|
if (t.attrs.length === 0) continue;
|
package/src/registry-manifest.ts
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from "./registry-manifest-exclusions.js";
|
|
38
38
|
|
|
39
39
|
/** One attribute in the manifest — the logical, cross-port-identical facet. */
|
|
40
|
-
interface ManifestAttr {
|
|
40
|
+
export interface ManifestAttr {
|
|
41
41
|
name: string;
|
|
42
42
|
/** The attr's SCALAR value-type subtype, or null for a polymorphic/untyped attr (e.g. @default). */
|
|
43
43
|
valueType: string | null;
|
|
@@ -63,7 +63,7 @@ interface ManifestAttr {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** One structural child rule of a type (FR-033 constraint graph). */
|
|
66
|
-
interface ManifestChild {
|
|
66
|
+
export interface ManifestChild {
|
|
67
67
|
/** The admitted child `type` (`"*"` = any). */
|
|
68
68
|
childType: string;
|
|
69
69
|
/** The admitted child subType — a single subtype, `"*"` (any), or a list of admitted subtypes. */
|
|
@@ -79,7 +79,7 @@ interface ManifestChild {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** One registered (type, subType) in the manifest, with its docs + attrs + constraint graph. */
|
|
82
|
-
interface ManifestType {
|
|
82
|
+
export interface ManifestType {
|
|
83
83
|
type: string;
|
|
84
84
|
subType: string;
|
|
85
85
|
/** FR-033 — human/AI-facing description of the type/subType (required, non-empty). */
|
|
@@ -112,7 +112,7 @@ interface ManifestType {
|
|
|
112
112
|
* constant read `"0.10"`). Bump with that script — never by hand — so the manifest and
|
|
113
113
|
* all four port constants move together.
|
|
114
114
|
*/
|
|
115
|
-
export const METAMODEL_VERSION = "0
|
|
115
|
+
export const METAMODEL_VERSION = "1.0";
|
|
116
116
|
|
|
117
117
|
/** The full canonical manifest. All collections are sorted for byte-stability. */
|
|
118
118
|
interface RegistryManifest {
|
|
@@ -124,7 +124,7 @@ interface RegistryManifest {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** ASCII-string compare so the sort is locale-independent and byte-stable across ports. */
|
|
127
|
-
function compareStrings(a: string, b: string): number {
|
|
127
|
+
export function compareStrings(a: string, b: string): number {
|
|
128
128
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -195,7 +195,7 @@ function toManifestChild(rule: ChildRule): ManifestChild {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/** Sort the constraint graph by (childType, childSubTypeKey, childName) — ASCII. */
|
|
198
|
-
function sortedChildren(rules: readonly ChildRule[]): ManifestChild[] {
|
|
198
|
+
export function sortedChildren(rules: readonly ChildRule[]): ManifestChild[] {
|
|
199
199
|
return rules
|
|
200
200
|
.map(toManifestChild)
|
|
201
201
|
.sort(
|
|
@@ -207,7 +207,7 @@ function sortedChildren(rules: readonly ChildRule[]): ManifestChild[] {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/** Sort attrs by name (ascending, ASCII). */
|
|
210
|
-
function sortedAttrs(attrs: readonly AttrSchema[]): ManifestAttr[] {
|
|
210
|
+
export function sortedAttrs(attrs: readonly AttrSchema[]): ManifestAttr[] {
|
|
211
211
|
return attrs
|
|
212
212
|
.map(toManifestAttr)
|
|
213
213
|
.sort((a, b) => compareStrings(a.name, b.name));
|
|
@@ -260,7 +260,8 @@ function sortedPerTypeAttrs(
|
|
|
260
260
|
* judge a carve-out "dead" — that is a cross-port property, asserted by the
|
|
261
261
|
* shared byte-canonical, not here.
|
|
262
262
|
*/
|
|
263
|
-
|
|
263
|
+
/** The classifier's "this is logical cross-port vocabulary" verdict. */
|
|
264
|
+
export const INCLUDED = "included" as const;
|
|
264
265
|
export type AttrClassification = ExclusionReason | typeof INCLUDED;
|
|
265
266
|
|
|
266
267
|
export function classifyPerTypeAttr(name: string): AttrClassification {
|