@metaobjectsdev/metadata 0.24.5 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/attr-schema-validate.js +15 -3
  2. package/dist/attr-schema-validate.js.map +1 -1
  3. package/dist/core/attr/attr-definition.embedded.js +1 -1
  4. package/dist/core/attr/attr-definition.embedded.js.map +1 -1
  5. package/dist/core/field/field-definition.embedded.js +1 -1
  6. package/dist/core/field/field-definition.embedded.js.map +1 -1
  7. package/dist/core/object/meta-object.d.ts +2 -0
  8. package/dist/core/object/meta-object.d.ts.map +1 -1
  9. package/dist/core/object/meta-object.js +20 -0
  10. package/dist/core/object/meta-object.js.map +1 -1
  11. package/dist/core/object/object-definition.embedded.js +1 -1
  12. package/dist/core/object/object-definition.embedded.js.map +1 -1
  13. package/dist/core/relationship/relationship-definition.embedded.js +1 -1
  14. package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
  15. package/dist/core/validator/validator-definition.embedded.js +1 -1
  16. package/dist/core/validator/validator-definition.embedded.js.map +1 -1
  17. package/dist/errors.d.ts +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +11 -1
  20. package/dist/errors.js.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/naming.d.ts +107 -0
  26. package/dist/naming.d.ts.map +1 -1
  27. package/dist/naming.js +155 -14
  28. package/dist/naming.js.map +1 -1
  29. package/dist/parser-core.d.ts.map +1 -1
  30. package/dist/parser-core.js +130 -11
  31. package/dist/parser-core.js.map +1 -1
  32. package/dist/persistence/origin/origin-definition.embedded.js +1 -1
  33. package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
  34. package/dist/persistence/source/source-definition.embedded.js +1 -1
  35. package/dist/persistence/source/source-definition.embedded.js.map +1 -1
  36. package/dist/presentation/layout/layout-definition.embedded.js +1 -1
  37. package/dist/presentation/layout/layout-definition.embedded.js.map +1 -1
  38. package/dist/presentation/view/view-definition.embedded.js +1 -1
  39. package/dist/presentation/view/view-definition.embedded.js.map +1 -1
  40. package/dist/registry-coverage.d.ts +0 -6
  41. package/dist/registry-coverage.d.ts.map +1 -1
  42. package/dist/registry-coverage.js +25 -1
  43. package/dist/registry-coverage.js.map +1 -1
  44. package/dist/registry-manifest.d.ts +13 -6
  45. package/dist/registry-manifest.d.ts.map +1 -1
  46. package/dist/registry-manifest.js +6 -5
  47. package/dist/registry-manifest.js.map +1 -1
  48. package/dist/retired-vocabulary.d.ts +23 -0
  49. package/dist/retired-vocabulary.d.ts.map +1 -1
  50. package/dist/retired-vocabulary.js +99 -1
  51. package/dist/retired-vocabulary.js.map +1 -1
  52. package/dist/shared/meta-data.d.ts.map +1 -1
  53. package/dist/shared/meta-data.js +23 -1
  54. package/dist/shared/meta-data.js.map +1 -1
  55. package/dist/template/template-definition.embedded.js +1 -1
  56. package/dist/template/template-definition.embedded.js.map +1 -1
  57. package/dist/vocabulary-catalog.d.ts +49 -0
  58. package/dist/vocabulary-catalog.d.ts.map +1 -0
  59. package/dist/vocabulary-catalog.js +80 -0
  60. package/dist/vocabulary-catalog.js.map +1 -0
  61. package/package.json +1 -1
  62. package/src/attr-schema-validate.ts +15 -3
  63. package/src/core/attr/attr-definition.embedded.ts +1 -1
  64. package/src/core/field/field-definition.embedded.ts +1 -1
  65. package/src/core/object/meta-object.ts +22 -0
  66. package/src/core/object/object-definition.embedded.ts +1 -1
  67. package/src/core/relationship/relationship-definition.embedded.ts +1 -1
  68. package/src/core/validator/validator-definition.embedded.ts +1 -1
  69. package/src/errors.ts +11 -1
  70. package/src/index.ts +8 -1
  71. package/src/naming.ts +164 -18
  72. package/src/parser-core.ts +142 -12
  73. package/src/persistence/origin/origin-definition.embedded.ts +1 -1
  74. package/src/persistence/source/source-definition.embedded.ts +1 -1
  75. package/src/presentation/layout/layout-definition.embedded.ts +1 -1
  76. package/src/presentation/view/view-definition.embedded.ts +1 -1
  77. package/src/registry-coverage.ts +25 -3
  78. package/src/registry-manifest.ts +9 -8
  79. package/src/retired-vocabulary.ts +107 -1
  80. package/src/shared/meta-data.ts +23 -3
  81. package/src/template/template-definition.embedded.ts +1 -1
  82. package/src/vocabulary-catalog.ts +133 -0
@@ -1,6 +1,7 @@
1
1
  // server/typescript/packages/metadata/src/retired-vocabulary.ts
2
2
  //
3
- // Vocabulary this project USED to register, and what to do instead.
3
+ // Vocabulary this project USED to register or used to READ and document without ever
4
+ // registering — and what to do instead.
4
5
  //
5
6
  // WHY THIS EXISTS (#337). Under ADR-0023 the registry is strict and sealed, so retired
6
7
  // vocabulary fails the load — correctly. But the message it failed with said the attribute
@@ -45,6 +46,15 @@ export interface RetirementNote {
45
46
  readonly replacedBy?: string;
46
47
  /** Repo-relative migration guide. */
47
48
  readonly migration?: string;
49
+ /**
50
+ * True when `meta upgrade --apply` can make this edit itself.
51
+ *
52
+ * Carried on the NOTE, not just the entry, because the diagnostic is the only place an
53
+ * adopter meets a retirement — and the difference between "run one command" and "this is
54
+ * a judgement call" is the single most useful thing to tell them at that moment. Absent
55
+ * ⇒ the human decides (see `VocabularyRewrite`).
56
+ */
57
+ readonly automated?: boolean;
48
58
  }
49
59
 
50
60
  /**
@@ -97,6 +107,7 @@ export interface RetiredEntry extends RetirementNote {
97
107
 
98
108
  const REQUIREMENT_MIGRATION = "docs/features/migrations/verified-by-retirement.md";
99
109
  const RETIRED_STATUS_MIGRATION = "docs/features/migrations/retired-status-restore.md";
110
+ const EMIT_ATTR_MIGRATION = "docs/features/migrations/emit-attrs-to-generator-config.md";
100
111
 
101
112
  export const RETIRED_VOCABULARY: readonly RetiredEntry[] = [
102
113
  // ── 0.24.0: `@violation` is renamed `@counterexample` ──
@@ -244,8 +255,80 @@ export const RETIRED_VOCABULARY: readonly RetiredEntry[] = [
244
255
  // caught this exists to prevent.
245
256
  rewrite: { kind: "dropAttr" },
246
257
  },
258
+ // ── FR-040: the @emit* family leaves the model (0.25.0) ──
259
+ //
260
+ // Five attributes — @emitRoutes, @emitTanstack, @emitForm, @emitGrid, @emitAngular — were
261
+ // READ by the TypeScript generators as per-entity kill switches, and documented as THE way
262
+ // to suppress an artifact, but they were never REGISTERED by any provider. So they worked
263
+ // under `meta gen`, which loads open, and failed `meta verify`, which loads strict: a
264
+ // mechanism we documented broke the drift gate we documented beside it.
265
+ //
266
+ // They are retired rather than registered. codegen-ts's own constants file already called
267
+ // them "NOT metamodel vocabulary — they tune codegen, not the model" and then read them off
268
+ // metadata anyway; that contradiction is the defect, and ADR-0023 §2 names the class.
269
+ // Registering them instead would move `metamodelVersion` and oblige four other ports to
270
+ // carry a TypeScript-only generator flag none of them will ever read.
271
+ //
272
+ // WHAT REPLACED THEM IS CONFIGURATION, NOT VOCABULARY, so `replacedBy` names a mechanism
273
+ // rather than an attribute — the only entries here that do. The rule is one sentence:
274
+ // decide per generator what you consume, wire only the generators whose output you import,
275
+ // and narrow one with its own `filter`.
276
+ //
277
+ // `dropAttr` is safe for all five: after this release nothing reads them, so removing one
278
+ // cannot change what is emitted. What it DOES change is what `meta gen` emitted BEFORE —
279
+ // an adopter who suppressed an artifact this way now gets that artifact — which is why the
280
+ // run also warns by name, rather than letting the file appear with no explanation.
281
+ {
282
+ type: "object", subType: "*", attr: "emitRoutes",
283
+ since: "0.25.0",
284
+ why: "it was never registered vocabulary, so the opt-out we documented passed `meta gen` " +
285
+ "and failed `meta verify`",
286
+ replacedBy: "the routes generator's own `filter` — routesFile({ filter: (e) => … })",
287
+ migration: EMIT_ATTR_MIGRATION,
288
+ rewrite: { kind: "dropAttr" },
289
+ },
290
+ {
291
+ type: "object", subType: "*", attr: "emitTanstack",
292
+ since: "0.25.0",
293
+ why: "it was never registered vocabulary, so the opt-out we documented passed `meta gen` " +
294
+ "and failed `meta verify`",
295
+ replacedBy: "the `filter` option on tanstackQuery() / tanstackGrid() / tanstackGridHook()",
296
+ migration: EMIT_ATTR_MIGRATION,
297
+ rewrite: { kind: "dropAttr" },
298
+ },
299
+ {
300
+ type: "object", subType: "*", attr: "emitForm",
301
+ since: "0.25.0",
302
+ why: "it was never registered vocabulary, and its own doc comment described it backwards " +
303
+ "— as opt-IN via `true`, while the code implemented opt-OUT via `false`",
304
+ replacedBy: "formFile({ filter: (e) => … })",
305
+ migration: EMIT_ATTR_MIGRATION,
306
+ rewrite: { kind: "dropAttr" },
307
+ },
308
+ {
309
+ type: "object", subType: "*", attr: "emitAngular",
310
+ since: "0.25.0",
311
+ why: "it was never registered vocabulary, and unlike its four siblings it was not even a " +
312
+ "named constant — just a bare string literal in four generators",
313
+ replacedBy: "the `filter` option on the Angular generators",
314
+ migration: EMIT_ATTR_MIGRATION,
315
+ rewrite: { kind: "dropAttr" },
316
+ },
317
+ {
318
+ // The one that is opt-IN rather than opt-out, so `filter` cannot replace it: a filter is
319
+ // ANDed with the built-in gates and can only ever narrow. It became a generator OPTION,
320
+ // which must be passed to BOTH grid generators — they emit a matched pair of files.
321
+ type: "object", subType: "*", attr: "emitGrid",
322
+ since: "0.25.0",
323
+ why: "it was never registered vocabulary, and being an opt-IN it could not be expressed " +
324
+ "by a generator filter, which only ever narrows",
325
+ replacedBy: "the `tphSubtypeGrids` option on BOTH tanstackGrid() and tanstackGridHook()",
326
+ migration: EMIT_ATTR_MIGRATION,
327
+ rewrite: { kind: "dropAttr" },
328
+ },
247
329
  ];
248
330
 
331
+
249
332
  /**
250
333
  * True when `entry` governs `typeKey` (`"<type>.<subType>"`).
251
334
  *
@@ -267,6 +350,7 @@ export function note(entry: RetiredEntry): RetirementNote {
267
350
  why: entry.why,
268
351
  ...(entry.replacedBy !== undefined ? { replacedBy: entry.replacedBy } : {}),
269
352
  ...(entry.migration !== undefined ? { migration: entry.migration } : {}),
353
+ ...(entry.rewrite !== undefined ? { automated: true } : {}),
270
354
  };
271
355
  }
272
356
 
@@ -321,3 +405,25 @@ export function retirementHint(n: RetirementNote): string {
321
405
  if (n.migration !== undefined) parts.push(`Migration: ${n.migration}`);
322
406
  return `${parts.join(". ")}.`;
323
407
  }
408
+
409
+ /**
410
+ * The ADR-0009 `suggestions[]` for a retirement: what to DO, as distinct from
411
+ * `retirementHint`, which says what happened.
412
+ *
413
+ * WHY THIS EXISTS. A caller that knows only "the load failed with ERR_UNKNOWN_ATTR" gives
414
+ * the generic three exits — register the attr, stash it in an `attr.properties` bag, or
415
+ * re-run with `--lax`. For a TYPO that is exactly right. For a RETIREMENT the middle one is
416
+ * actively harmful: the properties bag is exempt from the strict-attr check, so it loads,
417
+ * and the value then sits there reaching nothing. The adopter gets a green `meta verify`
418
+ * over metadata that no longer means what they wrote — a loud, correct failure converted
419
+ * into a quiet, wrong pass. So a retirement supplies its own exits and the caller prints
420
+ * those instead of guessing.
421
+ */
422
+ export function retirementSuggestions(n: RetirementNote): string[] {
423
+ const out: string[] = [];
424
+ if (n.automated === true) out.push("Run `meta upgrade --apply` to make this edit for you.");
425
+ if (n.replacedBy !== undefined) out.push(`Use ${n.replacedBy} instead.`);
426
+ if (n.migration !== undefined) out.push(`Migration guide: ${n.migration}`);
427
+ // Never the `attr.properties` bag: it would load, and mean nothing. See above.
428
+ return out;
429
+ }
@@ -497,14 +497,34 @@ export abstract class MetaData {
497
497
  // Track which of our own children matched (overrode) a super child position.
498
498
  const appendQueue: MetaData[] = [];
499
499
 
500
+ // Indices already taken by an own child in THIS pass. Without it, an own child
501
+ // written into `result[idx]` stays visible to the next own sibling's scan, so a
502
+ // second own child sharing the same (type, name) matches its own sibling and
503
+ // overwrites it. `extends` decides what a child overrides; a sibling is not a super.
504
+ // The append queue already closed the other branch — a non-shadowing own child is
505
+ // deferred so it cannot be matched later — and this closes the branch it left open.
506
+ //
507
+ // The shape is a write-through entity (two unnamed `source.rdb` children, primary +
508
+ // replica) whose base ALSO declares a source: the entity's own primary was silently
509
+ // dropped, and `primaryRdbSource` reads `children()`.
510
+ // A flag array, not a Set: `result` does not grow until after this loop, so the length
511
+ // is known, and this is the memoized effective-children path walked for every node.
512
+ // One small allocation, index access instead of hashing, and no per-child closure.
513
+ const claimed = new Uint8Array(result.length);
500
514
  for (const ownChild of this._children) {
501
515
  // Find the index in result that has the same (type, name).
502
- const idx = result.findIndex(
503
- (sc) => sc.type === ownChild.type && sc.name === ownChild.name,
504
- );
516
+ let idx = -1;
517
+ for (let i = 0; i < result.length; i++) {
518
+ const sc = result[i] as MetaData;
519
+ if (claimed[i] === 0 && sc.type === ownChild.type && sc.name === ownChild.name) {
520
+ idx = i;
521
+ break;
522
+ }
523
+ }
505
524
  if (idx !== -1) {
506
525
  // Replace the super child with our own (in-place override).
507
526
  result[idx] = ownChild;
527
+ claimed[idx] = 1;
508
528
  } else {
509
529
  // No matching super child — will be appended at the end.
510
530
  appendQueue.push(ownChild);
@@ -12,7 +12,7 @@ export const TEMPLATE_DEFINITION: ProviderDefinition = {
12
12
  {
13
13
  "type": "template",
14
14
  "subType": "base",
15
- "description": "Abstract base template — the shared root subtype for the fourth pillar (FR-004, ADR-0011). A template is a typed payload bound to either a rendered text artifact (prompt/output) or a tool-call envelope. The base carries no attrs of its own; concrete subtypes add their reference + governance attrs.",
15
+ "description": "Abstract base template — the shared root subtype for the fourth pillar (FR-004, ADR-0011). A template is a typed payload bound to either a rendered text artifact (prompt/output) or a tool-call envelope. The base carries no attrs of its own; concrete subtypes add their reference + governance attrs. Not authored directly: a `template.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
  "rules": "A single MetaTemplate node class backs every template subtype (no subType→class dispatch); the subtype selects which reserved-attr set applies. @format (a closed-enum ATTRIBUTE, never a subtype) drives the render engine's escaper/whitespace behavior, so a new format costs one escaper + one enum value rather than a new subtype + cross-language port."
17
17
  },
18
18
  {
@@ -0,0 +1,133 @@
1
+ // #357 — "what can I author HERE?", which is NOT the question the registry manifest answers.
2
+ //
3
+ // `buildRegistryManifest` answers "what must all five ports byte-match?" and deliberately
4
+ // carves rows OUT of its answer: the `metadata.base` inheritance anchor, and the 13
5
+ // TS-web-presentation `view.*` controls, which stay REGISTERED in TypeScript (the loader
6
+ // must accept an authored `view.dropdown`) but are deregistered in C# and Python and
7
+ // excluded from the shared canonical everywhere. That carve-out is correct and documented
8
+ // (fixtures/registry-conformance/README.md, B-2).
9
+ //
10
+ // `meta types` — the vocabulary search the generated `AGENTS.md` and the
11
+ // `metaobjects-authoring` skill both make STEP 1 of the authoring procedure — was built on
12
+ // that same function, so the answer it gave an author was the cross-port contract with
13
+ // this port's own vocabulary removed: 2 of the 15 registered `view.*` subtypes, with the
14
+ // other 13 reported exactly as a genuine typo is. Following the documented procedure, an
15
+ // author correctly concluded `view.text` does not exist.
16
+ //
17
+ // One function cannot answer both questions, so there are two. This one enumerates every
18
+ // (type, subType) the registry accepts and MARKS the rows the cross-port manifest omits,
19
+ // which is the honest way to surface what the carve-out means: not "missing" but
20
+ // "TypeScript-only".
21
+
22
+ import type { TypeDefinition, TypeRegistry } from "./registry.js";
23
+ import {
24
+ type ManifestAttr,
25
+ type ManifestChild,
26
+ classifyPerTypeAttr,
27
+ INCLUDED,
28
+ compareStrings,
29
+ sortedAttrs,
30
+ sortedChildren,
31
+ METAMODEL_VERSION,
32
+ } from "./registry-manifest.js";
33
+ import { classifyTypeSubType } from "./registry-manifest-exclusions.js";
34
+ import { SUBTYPE_BASE } from "./shared/base-types.js";
35
+
36
+ /** One registered (type, subType), as an AUTHOR sees it. */
37
+ export interface VocabularyType {
38
+ type: string;
39
+ subType: string;
40
+ description: string;
41
+ rules?: string;
42
+ example?: string;
43
+ whenToUse?: string;
44
+ attrs: ManifestAttr[];
45
+ children: ManifestChild[];
46
+ parents?: string[];
47
+ /**
48
+ * False when the cross-port manifest carves this row out — vocabulary THIS port
49
+ * accepts that the shared five-port contract does not carry. An author can use it
50
+ * here; a sibling port may not have it.
51
+ */
52
+ crossPort: boolean;
53
+ /** Why it is not cross-port (the carve-out's declared category). Absent when it is. */
54
+ portPrivateReason?: string;
55
+ /**
56
+ * True for a type's shared ROOT subtype (`<type>.base`). Attrs and child rules
57
+ * registered there apply to every subtype of the type (see constraint-merge.ts), so a
58
+ * `base` row is a summary of the whole family rather than a control an author picks.
59
+ * Deliberately NOT called "abstract": nothing in the registry records abstractness, and
60
+ * some `base` rows (`attr.base`) are the authorable polymorphic form.
61
+ */
62
+ sharedRoot: boolean;
63
+ }
64
+
65
+ /** Every (type, subType) the registry accepts, plus what every node accepts. */
66
+ export interface VocabularyCatalog {
67
+ metamodelVersion: string;
68
+ types: VocabularyType[];
69
+ /** Attrs accepted on EVERY node, whatever its type (`@title`, `@description`, …). */
70
+ commonAttrs: ManifestAttr[];
71
+ defaultSubTypes: Record<string, string>;
72
+ }
73
+
74
+ function toVocabularyType(def: TypeDefinition): VocabularyType {
75
+ const { type, subType } = def.typeId;
76
+ const reason = classifyTypeSubType(type, subType);
77
+ const out: VocabularyType = {
78
+ type,
79
+ subType,
80
+ description: def.description,
81
+ ...(def.rules !== undefined ? { rules: def.rules } : {}),
82
+ ...(def.example !== undefined ? { example: def.example } : {}),
83
+ ...(def.whenToUse !== undefined ? { whenToUse: def.whenToUse } : {}),
84
+ // Same attr filter the manifest applies — the carved-out per-type attr names are
85
+ // bare structural keywords (`isArray`, `extends`) that `@`-prefix to ERR_RESERVED_ATTR,
86
+ // OO-port native bindings, and the `description` commonAttr re-registered per type.
87
+ // Listing any of them here would teach metadata the loader rejects. The manifest's
88
+ // FR-024 requiredness override is deliberately NOT applied: that freezes the
89
+ // CROSS-PORT value during a reference-first rollout, and this answer is about what
90
+ // THIS registry requires of an author right now.
91
+ attrs: sortedAttrs(def.attributes.filter((a) => classifyPerTypeAttr(a.name) === INCLUDED)),
92
+ children: sortedChildren(def.childRules),
93
+ crossPort: reason === undefined,
94
+ ...(reason !== undefined ? { portPrivateReason: reason } : {}),
95
+ sharedRoot: subType === SUBTYPE_BASE,
96
+ };
97
+ if (def.parents !== undefined && def.parents.length > 0) {
98
+ out.parents = [...def.parents].sort(compareStrings);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Build the authoring-facing vocabulary catalog from an assembled registry.
105
+ *
106
+ * The registry must be COMPOSED (`composeRegistry(coreProviders)`), not merely
107
+ * `registerCoreTypes`'d: the db, ui-web and documentation providers each register attrs
108
+ * onto types the core provider declares, so a partially-composed registry reports a type
109
+ * that exists with most of its attributes missing — `field.string` with 6 attrs instead of
110
+ * 16, and no commonAttrs at all.
111
+ */
112
+ export function buildVocabularyCatalog(registry: TypeRegistry): VocabularyCatalog {
113
+ const types = registry
114
+ .allTypes()
115
+ .map((typeId) => toVocabularyType(registry.find(typeId.type, typeId.subType) as TypeDefinition))
116
+ .sort((a, b) =>
117
+ compareStrings(`${a.type}.${a.subType}`, `${b.type}.${b.subType}`),
118
+ );
119
+
120
+ const typeNames = Array.from(new Set(types.map((t) => t.type))).sort(compareStrings);
121
+ const defaultSubTypes: Record<string, string> = {};
122
+ for (const typeName of typeNames) {
123
+ const defaultSub = registry.defaultSubTypeOf(typeName);
124
+ if (defaultSub !== undefined) defaultSubTypes[typeName] = defaultSub;
125
+ }
126
+
127
+ return {
128
+ metamodelVersion: METAMODEL_VERSION,
129
+ types,
130
+ commonAttrs: sortedAttrs(registry.getCommonAttrs()),
131
+ defaultSubTypes,
132
+ };
133
+ }