@metaobjectsdev/metadata 0.21.6 → 0.22.0-rc.2

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 (56) hide show
  1. package/dist/core/requirement/meta-requirement.d.ts +23 -0
  2. package/dist/core/requirement/meta-requirement.d.ts.map +1 -0
  3. package/dist/core/requirement/meta-requirement.js +52 -0
  4. package/dist/core/requirement/meta-requirement.js.map +1 -0
  5. package/dist/core/requirement/requirement-constants.d.ts +35 -0
  6. package/dist/core/requirement/requirement-constants.d.ts.map +1 -0
  7. package/dist/core/requirement/requirement-constants.js +68 -0
  8. package/dist/core/requirement/requirement-constants.js.map +1 -0
  9. package/dist/core/requirement/requirement-definition.embedded.d.ts +3 -0
  10. package/dist/core/requirement/requirement-definition.embedded.d.ts.map +1 -0
  11. package/dist/core/requirement/requirement-definition.embedded.js +141 -0
  12. package/dist/core/requirement/requirement-definition.embedded.js.map +1 -0
  13. package/dist/core-types.d.ts.map +1 -1
  14. package/dist/core-types.js +27 -1
  15. package/dist/core-types.js.map +1 -1
  16. package/dist/errors.d.ts +1 -1
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +1 -0
  19. package/dist/errors.js.map +1 -1
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +7 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/naming.d.ts.map +1 -1
  25. package/dist/naming.js +9 -3
  26. package/dist/naming.js.map +1 -1
  27. package/dist/registry.d.ts.map +1 -1
  28. package/dist/registry.js +28 -0
  29. package/dist/registry.js.map +1 -1
  30. package/dist/shared/base-types.d.ts +4 -1
  31. package/dist/shared/base-types.d.ts.map +1 -1
  32. package/dist/shared/base-types.js +4 -0
  33. package/dist/shared/base-types.js.map +1 -1
  34. package/dist/shared/node-guards.d.ts +24 -0
  35. package/dist/shared/node-guards.d.ts.map +1 -0
  36. package/dist/shared/node-guards.js +87 -0
  37. package/dist/shared/node-guards.js.map +1 -0
  38. package/dist/template/prompt-definition.embedded.d.ts.map +1 -1
  39. package/dist/template/prompt-definition.embedded.js +0 -276
  40. package/dist/template/prompt-definition.embedded.js.map +1 -1
  41. package/dist/template/template-definition.embedded.d.ts.map +1 -1
  42. package/dist/template/template-definition.embedded.js +267 -3
  43. package/dist/template/template-definition.embedded.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/core/requirement/meta-requirement.ts +69 -0
  46. package/src/core/requirement/requirement-constants.ts +82 -0
  47. package/src/core/requirement/requirement-definition.embedded.ts +148 -0
  48. package/src/core-types.ts +30 -0
  49. package/src/errors.ts +1 -0
  50. package/src/index.ts +15 -0
  51. package/src/naming.ts +10 -4
  52. package/src/registry.ts +34 -0
  53. package/src/shared/base-types.ts +4 -0
  54. package/src/shared/node-guards.ts +96 -0
  55. package/src/template/prompt-definition.embedded.ts +0 -276
  56. package/src/template/template-definition.embedded.ts +267 -3
@@ -0,0 +1,69 @@
1
+ // MetaRequirement — concrete node class for type=requirement nodes.
2
+ //
3
+ // Extends MetaData directly: no model wrapper, no metaOf() indirection.
4
+ // Accessors are RESOLVING (ADR-0039) so a requirement that `extends` an
5
+ // abstract parent inherits its properties.
6
+
7
+ import { MetaData } from "../../shared/meta-data.js";
8
+ import {
9
+ REQUIREMENT_SUBTYPE_FUNCTIONAL,
10
+ REQUIREMENT_SUBTYPE_ARCHITECTURAL,
11
+ REQUIREMENT_ATTR_LEVEL,
12
+ REQUIREMENT_ATTR_STATUS,
13
+ REQUIREMENT_ATTR_IMPLEMENTED_BY,
14
+ REQUIREMENT_ATTR_VERIFIED_BY,
15
+ REQUIREMENT_LINK_FLOOR_LEVEL,
16
+ REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES,
17
+ type RequirementStatus,
18
+ } from "./requirement-constants.js";
19
+
20
+ export class MetaRequirement extends MetaData {
21
+ /** What the product does for a user — checked by EXISTENCE. */
22
+ isFunctional(): boolean {
23
+ return this.subType === REQUIREMENT_SUBTYPE_FUNCTIONAL;
24
+ }
25
+
26
+ /** How the system is built — checked by UNIVERSALITY (the opposite polarity). */
27
+ isArchitectural(): boolean {
28
+ return this.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL;
29
+ }
30
+
31
+ /** 1 solution · 2 segment · 3 service · 4 object · 5 member. Architectural
32
+ * requirements carry none — they are object-independent by definition. */
33
+ level(): number | undefined {
34
+ const v = this.attr(REQUIREMENT_ATTR_LEVEL);
35
+ return typeof v === "number" ? v : undefined;
36
+ }
37
+
38
+ status(): RequirementStatus | undefined {
39
+ const v = this.attr(REQUIREMENT_ATTR_STATUS);
40
+ return typeof v === "string" ? (v as RequirementStatus) : undefined;
41
+ }
42
+
43
+ implementedBy(): string[] {
44
+ const v = this.attr(REQUIREMENT_ATTR_IMPLEMENTED_BY);
45
+ return Array.isArray(v) ? (v as string[]) : [];
46
+ }
47
+
48
+ verifiedBy(): string[] {
49
+ const v = this.attr(REQUIREMENT_ATTR_VERIFIED_BY);
50
+ return Array.isArray(v) ? (v as string[]) : [];
51
+ }
52
+
53
+ /** True when this requirement is permitted to reference the model at all.
54
+ * Architectural requirements always may (their claim set is the point);
55
+ * functional ones only at or below the link floor, so the organisational
56
+ * tiers stay organisational. */
57
+ mayReferenceModel(): boolean {
58
+ if (this.isArchitectural()) return true;
59
+ const lvl = this.level();
60
+ return lvl !== undefined && lvl >= REQUIREMENT_LINK_FLOOR_LEVEL;
61
+ }
62
+
63
+ /** True when a dangling `@implementedBy` is an ERROR rather than expected.
64
+ * An abandoned or superseded requirement's nodes are supposed to be gone. */
65
+ requiresLiveNodes(): boolean {
66
+ const s = this.status();
67
+ return s !== undefined && REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES.includes(s);
68
+ }
69
+ }
@@ -0,0 +1,82 @@
1
+ // Requirement concern constants — type name, subtypes, and attr keys.
2
+ //
3
+ // `requirement.*` is REGISTERED metamodel vocabulary (ruling amendment 3): the
4
+ // capability ledger is a metadata model, so it is declared in `metaobjects/`
5
+ // beside the entities it describes and validated by the loader like everything
6
+ // else — not hand-parsed from a side file.
7
+
8
+ export const REQUIREMENT = "requirement";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Subtypes — the axis is the CHECK POLARITY, which is a genuine behaviour
12
+ // difference and therefore a subtype under ADR-0037 §2:
13
+ // functional -> EXISTENCE: fails when nothing implements it
14
+ // architectural -> UNIVERSALITY: fails when something violates it
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export const REQUIREMENT_SUBTYPE_FUNCTIONAL = "functional";
18
+ export const REQUIREMENT_SUBTYPE_ARCHITECTURAL = "architectural";
19
+
20
+ export const REQUIREMENT_SUBTYPES = [
21
+ REQUIREMENT_SUBTYPE_FUNCTIONAL,
22
+ REQUIREMENT_SUBTYPE_ARCHITECTURAL,
23
+ ] as const;
24
+ export type RequirementSubType = (typeof REQUIREMENT_SUBTYPES)[number];
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Attrs
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** 1 solution · 2 segment (app/library) · 3 service · 4 object · 5 member. */
31
+ export const REQUIREMENT_ATTR_LEVEL = "level";
32
+ export const REQUIREMENT_ATTR_STATUS = "status";
33
+ export const REQUIREMENT_ATTR_STATEMENT = "statement";
34
+ export const REQUIREMENT_ATTR_VIOLATION = "violation";
35
+ export const REQUIREMENT_ATTR_IMPLEMENTED_BY = "implementedBy";
36
+ export const REQUIREMENT_ATTR_VERIFIED_BY = "verifiedBy";
37
+ export const REQUIREMENT_ATTR_SUPERSEDED_BY = "supersededBy";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Status — a closed enum, enforced by the registry via `allowedValues`. This is
41
+ // the one payload with controlled evidence behind it: model-only agents flagged
42
+ // a deliberately-retired capability 0 times out of 24, so a typo that silently
43
+ // disabled it would disable the whole mechanism.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export const REQUIREMENT_STATUS_LIVE = "live";
47
+ export const REQUIREMENT_STATUS_PARTIAL = "partial";
48
+ export const REQUIREMENT_STATUS_ABANDONED = "abandoned";
49
+ export const REQUIREMENT_STATUS_SUPERSEDED = "superseded";
50
+
51
+ export const REQUIREMENT_STATUSES = [
52
+ REQUIREMENT_STATUS_LIVE,
53
+ REQUIREMENT_STATUS_PARTIAL,
54
+ REQUIREMENT_STATUS_ABANDONED,
55
+ REQUIREMENT_STATUS_SUPERSEDED,
56
+ ] as const;
57
+ export type RequirementStatus = (typeof REQUIREMENT_STATUSES)[number];
58
+
59
+ /** Statuses whose implementing nodes are supposed to still exist. A dangling
60
+ * `@implementedBy` on one of these means the model moved and the requirement is
61
+ * stale; on the other two the nodes are supposed to be GONE, which is the whole
62
+ * point of the entry. The asymmetry inverts as a pair. */
63
+ export const REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES: readonly RequirementStatus[] = [
64
+ REQUIREMENT_STATUS_LIVE,
65
+ REQUIREMENT_STATUS_PARTIAL,
66
+ ];
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Levels — organisational above the link floor, model-referencing at or below.
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export const REQUIREMENT_LEVEL_SOLUTION = 1;
73
+ export const REQUIREMENT_LEVEL_SEGMENT = 2;
74
+ export const REQUIREMENT_LEVEL_SERVICE = 3;
75
+ export const REQUIREMENT_LEVEL_OBJECT = 4;
76
+ export const REQUIREMENT_LEVEL_MEMBER = 5;
77
+
78
+ /** The lowest level that may reference the model. L1-L3 are organisational and
79
+ * carrying `@implementedBy` there is an error. */
80
+ export const REQUIREMENT_LINK_FLOOR_LEVEL = REQUIREMENT_LEVEL_OBJECT;
81
+ export const REQUIREMENT_MIN_LEVEL = REQUIREMENT_LEVEL_SOLUTION;
82
+ export const REQUIREMENT_MAX_LEVEL = REQUIREMENT_LEVEL_MEMBER;
@@ -0,0 +1,148 @@
1
+ // AUTO-GENERATED by scripts/generate-embedded-metamodel.ts — DO NOT EDIT.
2
+ // Canonical source: repo-root spec/metamodel/requirement.json
3
+ // Regenerate: bun run scripts/generate-embedded-metamodel.ts
4
+ //
5
+ // Embeds the canonical FR-033 ProviderDefinition so the provider can register
6
+ // itself wherever the on-disk spec/ tree is unavailable (bundled builds).
7
+ import type { ProviderDefinition } from "../../provider-data.js";
8
+
9
+ export const REQUIREMENT_DEFINITION: ProviderDefinition = {
10
+ "provider": "metaobjects-core-types",
11
+ "types": [
12
+ {
13
+ "type": "requirement",
14
+ "subType": "functional",
15
+ "description": "What the product does for a user, stated as one violable claim. Its check is EXISTENCE: it fails when nothing implements it. Hierarchy is nesting — an L1 solution contains its L2 segments, which contain L3 services, and so on down to the levels that reference the model.",
16
+ "whenToUse": "Something exists because someone asked for it. Levels: 1 solution, 2 segment (an application or library), 3 service, 4 object, 5 member. Only L4 and L5 carry @implementedBy — L1-L3 are organisational and never reference the model.",
17
+ "children": [
18
+ {
19
+ "type": "attr",
20
+ "subType": "int",
21
+ "name": "level",
22
+ "min": 1,
23
+ "max": 1,
24
+ "description": "1 solution, 2 segment (application/library), 3 service, 4 object, 5 member. Nesting depth must agree with it."
25
+ },
26
+ {
27
+ "type": "attr",
28
+ "subType": "string",
29
+ "name": "status",
30
+ "min": 1,
31
+ "max": 1,
32
+ "allowedValues": [
33
+ "live",
34
+ "partial",
35
+ "abandoned",
36
+ "superseded"
37
+ ],
38
+ "description": "live implemented and in use; partial implemented with known gaps; abandoned built then deliberately retired; superseded replaced by a different mechanism. A dangling @implementedBy is an ERROR on live/partial (the model moved, the requirement is stale) and ALLOWED on abandoned/superseded (those nodes are meant to be gone — that is the entry doing its job)."
39
+ },
40
+ {
41
+ "type": "attr",
42
+ "subType": "string",
43
+ "name": "statement",
44
+ "min": 1,
45
+ "max": 1,
46
+ "description": "What the capability is, in one sentence."
47
+ },
48
+ {
49
+ "type": "attr",
50
+ "subType": "string",
51
+ "name": "violation",
52
+ "min": 1,
53
+ "max": 1,
54
+ "description": "What breaking it looks like, in one sentence. A requirement MUST be violable: 'every entity has a uuid primary key' is (point at one with a composite string key); 'things are persisted' is not, and is a description rather than a requirement."
55
+ },
56
+ {
57
+ "type": "attr",
58
+ "subType": "string",
59
+ "name": "implementedBy",
60
+ "isArray": true,
61
+ "min": 0,
62
+ "max": 1,
63
+ "description": "FQN references to the model nodes realising this requirement. Legal on level 4 (an object) and level 5 (a field, view or identity) only; an organisational level carrying it is ERR_REQUIREMENT_LINK_ABOVE_FLOOR. Many-to-many by construction — several requirements may name the same node."
64
+ },
65
+ {
66
+ "type": "attr",
67
+ "subType": "string",
68
+ "name": "verifiedBy",
69
+ "isArray": true,
70
+ "min": 0,
71
+ "max": 1,
72
+ "description": "Names of the tests proving the behaviour. verify checks each exists and is not skipped; it never runs them."
73
+ },
74
+ {
75
+ "type": "attr",
76
+ "subType": "string",
77
+ "name": "supersededBy",
78
+ "min": 0,
79
+ "max": 1,
80
+ "description": "The requirement that replaced this one. Expected on status=superseded."
81
+ },
82
+ {
83
+ "type": "requirement",
84
+ "subType": "*",
85
+ "name": "*",
86
+ "min": 0,
87
+ "max": null,
88
+ "description": "Nested child requirements. Hierarchy IS nesting — an L1 solution contains its L2 segments, which contain L3 services — so regrouping moves a node rather than editing a parent string, and a requirement is addressable by the same dotted child-name path as every other node."
89
+ }
90
+ ]
91
+ },
92
+ {
93
+ "type": "requirement",
94
+ "subType": "architectural",
95
+ "description": "How the system is built, applied uniformly across the model. Its check is UNIVERSALITY: it fails when something VIOLATES it, which is the opposite polarity to a functional requirement. Carries no level and no parent — levels come from object-in-focus decomposition and an architectural requirement is object-independent by definition.",
96
+ "whenToUse": "Something exists because every entity here looks like this — a uuid primary key, an @autoSet createdAt, a change-attribution column, tenant scoping. The discriminator is mechanical: did this exist because someone asked for something, or because it is the architecture?",
97
+ "children": [
98
+ {
99
+ "type": "attr",
100
+ "subType": "string",
101
+ "name": "status",
102
+ "min": 1,
103
+ "max": 1,
104
+ "allowedValues": [
105
+ "live",
106
+ "partial",
107
+ "abandoned",
108
+ "superseded"
109
+ ],
110
+ "description": "As on requirement.functional. A live or partial architectural requirement claimed by NOTHING is an error: a policy declared and applied to nothing."
111
+ },
112
+ {
113
+ "type": "attr",
114
+ "subType": "string",
115
+ "name": "statement",
116
+ "min": 1,
117
+ "max": 1,
118
+ "description": "The policy, in one sentence."
119
+ },
120
+ {
121
+ "type": "attr",
122
+ "subType": "string",
123
+ "name": "violation",
124
+ "min": 1,
125
+ "max": 1,
126
+ "description": "What breaking it looks like — the node that would contradict it. This is what makes universality checkable."
127
+ },
128
+ {
129
+ "type": "attr",
130
+ "subType": "string",
131
+ "name": "implementedBy",
132
+ "isArray": true,
133
+ "min": 0,
134
+ "max": 1,
135
+ "description": "FQN references to the nodes applying this policy. High fan-out is normal and expected: one uuid-primary-key requirement is claimed by every entity."
136
+ },
137
+ {
138
+ "type": "attr",
139
+ "subType": "string",
140
+ "name": "supersededBy",
141
+ "min": 0,
142
+ "max": 1,
143
+ "description": "The requirement that replaced this one. Expected on status=superseded."
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ };
package/src/core-types.ts CHANGED
@@ -56,6 +56,9 @@ import { TEMPLATE_SUBTYPES } from "./template/template-constants.js";
56
56
  import { MetaIndex } from "./core/index/meta-index.js";
57
57
  import { INDEX_DEFINITION } from "./core/index/index-definition.embedded.js";
58
58
  import { INDEX_SUBTYPES } from "./core/index/index-constants.js";
59
+ import { MetaRequirement } from "./core/requirement/meta-requirement.js";
60
+ import { REQUIREMENT_DEFINITION } from "./core/requirement/requirement-definition.embedded.js";
61
+ import { REQUIREMENT_SUBTYPES } from "./core/requirement/requirement-constants.js";
59
62
  import {
60
63
  TYPE_METADATA,
61
64
  TYPE_OBJECT,
@@ -70,6 +73,7 @@ import {
70
73
  TYPE_ORIGIN,
71
74
  TYPE_TEMPLATE,
72
75
  TYPE_INDEX,
76
+ TYPE_REQUIREMENT,
73
77
  SUBTYPE_ROOT,
74
78
  } from "./shared/base-types.js";
75
79
  import { CHILD_RULE_WILDCARD } from "./shared/structural.js";
@@ -186,6 +190,8 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
186
190
  wildcard(TYPE_FIELD),
187
191
  wildcard(TYPE_VALIDATOR),
188
192
  wildcard(TYPE_TEMPLATE),
193
+ // Capability requirements are declared beside the entities they describe.
194
+ wildcard(TYPE_REQUIREMENT),
189
195
  ], MetaRoot),
190
196
  );
191
197
 
@@ -487,6 +493,23 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
487
493
  registry.register(indexDef);
488
494
  }
489
495
 
496
+ // requirement — 2 subtypes. The axis is CHECK POLARITY, a genuine behaviour
497
+ // difference (ADR-0037 §2): `functional` is checked by EXISTENCE (nothing
498
+ // implements it -> fail), `architectural` by UNIVERSALITY (something violates
499
+ // it -> fail). Registered rather than hand-parsed because the capability
500
+ // ledger IS a metadata model: a closed status enum and a list of FQN
501
+ // references to model nodes are exactly `allowedValues` plus reference
502
+ // resolution, both of which this registry already owns (ruling amendment 3).
503
+ const REQUIREMENT_FACTORIES: FactoryMap = Object.fromEntries(
504
+ REQUIREMENT_SUBTYPES.map((subType) => [
505
+ `${TYPE_REQUIREMENT}.${subType}`,
506
+ (typeId: TypeId, name: string) => new MetaRequirement(typeId, name),
507
+ ]),
508
+ );
509
+ for (const reqDef of defineProviderFromData(REQUIREMENT_DEFINITION, REQUIREMENT_FACTORIES)) {
510
+ registry.register(reqDef);
511
+ }
512
+
490
513
  // Declare the core cross-references ON their TypeDefinitions, so the loader's
491
514
  // registry-derived validation resolves them generically (a dangling target fails the
492
515
  // load). Set on the concrete subtypes the parser produces. (Production moves these into
@@ -499,6 +522,13 @@ function registerCoreTypeDefs(registry: TypeRegistry): void {
499
522
  ];
500
523
  }
501
524
  }
525
+ // NOTE: @implementedBy is deliberately NOT declared as a loader `references`
526
+ // descriptor. That pass always ERRORS on an unresolved target, and a
527
+ // requirement with status `abandoned`/`superseded` exists precisely to name
528
+ // nodes that are GONE — declaring it here would make the entries that carry
529
+ // the mechanism's only controlled evidence fail to load. The loader owns what
530
+ // is unconditional (the status enum, shape, levels); `meta verify` owns the
531
+ // status-conditional resolution, where the severity actually depends on data.
502
532
  const idRefDef = registry.find(TYPE_IDENTITY, IDENTITY_SUBTYPE_REFERENCE);
503
533
  if (idRefDef) {
504
534
  idRefDef.references = [
package/src/errors.ts CHANGED
@@ -60,6 +60,7 @@ export const ERROR_CODES = [
60
60
  "ERR_PROVIDER_DUPLICATE_ID",
61
61
  "ERR_PROVIDER_MISSING_DEPENDENCY",
62
62
  "ERR_PROVIDER_ATTR_CONFLICT",
63
+ "ERR_EXTEND_REQUIRED_ATTR",
63
64
  "ERR_MALFORMED_JSON",
64
65
  "ERR_MISSING_REQUIRED_ATTR",
65
66
  "ERR_SUBTYPE_RULE_VIOLATION",
package/src/index.ts CHANGED
@@ -32,6 +32,8 @@ export * from "./core/documentation/doc-constants.js";
32
32
  export * from "./core/validator/validator-constants.js";
33
33
  export * from "./core/identity/identity-constants.js";
34
34
  export * from "./core/index/index-constants.js";
35
+ export * from "./core/requirement/requirement-constants.js";
36
+ export { MetaRequirement } from "./core/requirement/meta-requirement.js";
35
37
  export * from "./core/relationship/relationship-constants.js";
36
38
  export * from "./core/query/query-constants.js";
37
39
  export * from "./persistence/source/source-constants.js";
@@ -48,6 +50,19 @@ export type { AttrValue } from "./shared/meta-data.js";
48
50
  // Shared node classes
49
51
  export { MetaRoot } from "./shared/meta-root.js";
50
52
 
53
+ // Cross-realm node guards — identify a node by metamodel `type`, not `instanceof`.
54
+ // Cross-package callers (codegen-ts / migrate-ts / runtime-ts) MUST use these:
55
+ // `instanceof` silently fails when two physical copies of this package are
56
+ // loaded. See shared/node-guards.ts for the mechanism.
57
+ export {
58
+ isMetaRoot,
59
+ isMetaObject,
60
+ isMetaField,
61
+ isMetaSource,
62
+ isWritableSource,
63
+ isReadOnlySource,
64
+ } from "./shared/node-guards.js";
65
+
51
66
  // Core node classes
52
67
  export { MetaObject } from "./core/object/meta-object.js";
53
68
  export { MetaField } from "./core/field/meta-field.js";
package/src/naming.ts CHANGED
@@ -6,7 +6,14 @@ import {
6
6
  SOURCE_ATTR_SCHEMA,
7
7
  SOURCE_ROLE_PRIMARY,
8
8
  } from "./persistence/source/source-constants.js";
9
- import { MetaSource } from "./persistence/source/meta-source.js";
9
+ import type { MetaSource } from "./persistence/source/meta-source.js";
10
+ // isMetaSource, not `instanceof`: unlike the loader-internal validators, these two
11
+ // are EXPORTED helpers that run on a caller-supplied node — migrate-ts calls both
12
+ // on nodes the CLI's loader built. Under a split @metaobjectsdev/metadata tree the
13
+ // class check would be false for a real primary source, and resolveTableName would
14
+ // silently fall through to the entity-name fallback: a DIFFERENT table name, which
15
+ // migrate then emits as a rename against a live database.
16
+ import { isMetaSource } from "./shared/node-guards.js";
10
17
 
11
18
  /**
12
19
  * Strip the package prefix from a metadata-qualified name
@@ -72,7 +79,7 @@ export function resolveTableName(entity: MetaData): string {
72
79
  // entity-name fallback. For an entity declaring its own source, own shadows
73
80
  // inherited, so the result is unchanged.
74
81
  const source = entity.children().find(
75
- (c): c is MetaSource => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
82
+ (c): c is MetaSource => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY,
76
83
  );
77
84
  if (source !== undefined) return source.physicalName;
78
85
  return pluralize(toSnakeCase(entity.name));
@@ -98,8 +105,7 @@ export function resolveColumnName(
98
105
  export function resolveTableSchema(entity: MetaData): string | undefined {
99
106
  // ADR-0039: resolving — a concrete entity may inherit its source.rdb via extends.
100
107
  const source = entity.children().find(
101
- (c): c is MetaSource =>
102
- c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
108
+ (c): c is MetaSource => isMetaSource(c) && c.role === SOURCE_ROLE_PRIMARY,
103
109
  );
104
110
  if (!source) return undefined;
105
111
  // ADR-0039: resolving — an inherited source's @schema lives on the super node.
package/src/registry.ts CHANGED
@@ -252,6 +252,20 @@ export class TypeRegistry {
252
252
  `which is not valid for attrs. Use no valueType (omit the field) for a polymorphic/untyped attr.`,
253
253
  );
254
254
  }
255
+ // ADR-0050, the second door. A common attr is a projection onto EVERY type, so a
256
+ // required one is the same defect as a required `extend` — with the widest possible
257
+ // blast radius: drop the provider and every type in the registry silently loses a
258
+ // required-attr rule. Java makes this unrepresentable (its registerCommonAttribute
259
+ // takes no `required` parameter); TS's AttrSchema carries `required`, so it needs the
260
+ // explicit guard.
261
+ if (attr.required === true) {
262
+ throw new MetaModelError(
263
+ `TypeRegistry.registerCommonAttrs: attribute "${attr.name}" is REQUIRED. A common attr ` +
264
+ `is projected onto every registered type, so a required one would vanish from all of ` +
265
+ `them whenever its provider is composed out. Common attrs must be optional (ADR-0050).`,
266
+ { code: "ERR_EXTEND_REQUIRED_ATTR" },
267
+ );
268
+ }
255
269
  if (this._commonAttrs.some((existing) => existing.name === attr.name)) {
256
270
  continue; // dedupe same-name re-registration; conflict-with-per-type-attr is checked at validation time
257
271
  }
@@ -297,6 +311,26 @@ export class TypeRegistry {
297
311
  { code: "ERR_PROVIDER_ATTR_CONFLICT" },
298
312
  );
299
313
  }
314
+ // ADR-0050: a REQUIRED attr may never be PROJECTED onto someone else's type.
315
+ // `extend` is how a concern provider decorates a type another provider owns, and
316
+ // a concern can be composed out. A required attr arriving this way makes the
317
+ // target type's validity depend on an optional provider — and the failure is
318
+ // silent: drop the provider and the type stays registered while its required-attr
319
+ // rule simply stops firing, so invalid metadata begins loading clean.
320
+ //
321
+ // If the attr is genuinely required, it is OWN, not projected: declare it with the
322
+ // type, in the type's own provider. This is exactly how FR-033 broke template.*'s
323
+ // @payloadRef / @toolName, and this guard makes that class unrepresentable rather
324
+ // than merely fixed.
325
+ if (attr.required === true) {
326
+ throw new MetaModelError(
327
+ `TypeRegistry.extend: attribute "${attr.name}" being added to "${type}.${subType}" is REQUIRED. ` +
328
+ `A provider may only project OPTIONAL attributes onto a type it does not own — a required ` +
329
+ `attr registered this way disappears, silently, whenever that provider is composed out. ` +
330
+ `Declare it with the type instead (ADR-0050).`,
331
+ { code: "ERR_EXTEND_REQUIRED_ATTR" },
332
+ );
333
+ }
300
334
  def.attributes.push(attr);
301
335
  }
302
336
  for (const rule of ext.childRules ?? []) {
@@ -19,6 +19,9 @@ export const TYPE_ORIGIN = "origin";
19
19
  export const TYPE_TEMPLATE = "template";
20
20
  /** Non-unique lookup indexes — `index.lookup` is the only registered subtype. */
21
21
  export const TYPE_INDEX = "index";
22
+ /** Capability / requirement records — `requirement.functional` (existence check)
23
+ * and `requirement.architectural` (universality check). Ruling amendment 3. */
24
+ export const TYPE_REQUIREMENT = "requirement";
22
25
 
23
26
  export const BASE_TYPES = [
24
27
  TYPE_METADATA,
@@ -34,6 +37,7 @@ export const BASE_TYPES = [
34
37
  TYPE_ORIGIN,
35
38
  TYPE_TEMPLATE,
36
39
  TYPE_INDEX,
40
+ TYPE_REQUIREMENT,
37
41
  ] as const;
38
42
  export type BaseType = (typeof BASE_TYPES)[number];
39
43
 
@@ -0,0 +1,96 @@
1
+ // Cross-realm node guards — identify a node by its metamodel `type`, never by
2
+ // `instanceof`.
3
+ //
4
+ // WHY THESE EXIST
5
+ //
6
+ // `x instanceof MetaSource` is only sound when the class object and the instance
7
+ // come from the SAME physical copy of this package. Two copies in one process —
8
+ // a globally-installed or linked `meta` CLI alongside a project-local
9
+ // dependency — give them different class objects, so `instanceof` returns false
10
+ // for a node that is a source in every observable respect. This is the same
11
+ // class-identity defect that split ts-poet's `Code` objects in 0.21.6; there it
12
+ // was loud (duplicate imports → TS2300), but across the metadata boundary it is
13
+ // SILENT: a consumer package reads the node as "not a source" and simply emits
14
+ // nothing for it, with no error.
15
+ //
16
+ // WHO IS EXPOSED
17
+ //
18
+ // Sites INSIDE this package are immune by construction — a package's own module
19
+ // graph resolves its own files, so the class and the instance always match.
20
+ // The exposed callers are the OTHER packages that import a class from here
21
+ // (`codegen-ts`, `migrate-ts`, `runtime-ts`). `meta gen` / `meta migrate` alias
22
+ // `@metaobjectsdev/metadata` to the CLI's own copy (the CLI's
23
+ // `load-metaobjects-config.ts` CLI_PKG_PATHS), which closes the split for the
24
+ // CLI path — but that alias map does not run when a consumer embeds `runGen()`
25
+ // or the migrate engine programmatically, so cross-package code must not depend
26
+ // on it. A flat single-tree install also dedupes and hides the split, which is
27
+ // why in-process gates do not reproduce it.
28
+ //
29
+ // WHY TYPE-BASED IDENTIFICATION IS EQUIVALENT
30
+ //
31
+ // The registry binds one concrete class per metamodel type (TYPE_SOURCE →
32
+ // MetaSource, TYPE_OBJECT → MetaObject, TYPE_METADATA → MetaRoot), so on a
33
+ // single-copy tree `node.type === TYPE_SOURCE` and `node instanceof MetaSource`
34
+ // answer identically — these guards change nothing there. On a split tree the
35
+ // type still answers correctly, because `type` is data on the node rather than
36
+ // an identity shared with a class object.
37
+ //
38
+ // The returned predicate narrows to this copy's class type. That is a
39
+ // structural assertion, not a claim of instance identity: a foreign-realm node
40
+ // carries the same properties and prototype methods, so every member the caller
41
+ // then touches resolves. Callers needing behaviour from a node that may be
42
+ // foreign should still fail closed if a method is absent.
43
+
44
+ import { TYPE_FIELD, TYPE_METADATA, TYPE_OBJECT, TYPE_SOURCE } from "./base-types.js";
45
+ import type { MetaData } from "./meta-data.js";
46
+ import type { MetaRoot } from "./meta-root.js";
47
+ import type { MetaObject } from "../core/object/meta-object.js";
48
+ import type { MetaField } from "../core/field/meta-field.js";
49
+ import type { MetaSource } from "../persistence/source/meta-source.js";
50
+
51
+ /** The node's metamodel `type`, or undefined when the value is not a node at all. */
52
+ function nodeType(node: unknown): string | undefined {
53
+ if (typeof node !== "object" || node === null) return undefined;
54
+ const t = (node as { type?: unknown }).type;
55
+ return typeof t === "string" ? t : undefined;
56
+ }
57
+
58
+ /** True when the node is a metadata root (type=metadata), across package copies. */
59
+ export function isMetaRoot(node: unknown): node is MetaRoot {
60
+ return nodeType(node) === TYPE_METADATA;
61
+ }
62
+
63
+ /** True when the node is an object node (type=object), across package copies. */
64
+ export function isMetaObject(node: unknown): node is MetaObject {
65
+ return nodeType(node) === TYPE_OBJECT;
66
+ }
67
+
68
+ /** True when the node is a field node (type=field), across package copies. */
69
+ export function isMetaField(node: unknown): node is MetaField {
70
+ return nodeType(node) === TYPE_FIELD;
71
+ }
72
+
73
+ /** True when the node is a source node (type=source), across package copies. */
74
+ export function isMetaSource(node: unknown): node is MetaSource {
75
+ return nodeType(node) === TYPE_SOURCE;
76
+ }
77
+
78
+ /**
79
+ * True when the node is a source that reports itself writable.
80
+ *
81
+ * Reads the writability surface through the node rather than through the class,
82
+ * and fails closed when a foreign node cannot answer — the same outcome the old
83
+ * `instanceof` guard produced for a node it did not recognise.
84
+ */
85
+ export function isWritableSource(node: MetaData | undefined): node is MetaSource {
86
+ if (!isMetaSource(node)) return false;
87
+ const probe = node as unknown as { isWritable?: () => boolean };
88
+ return typeof probe.isWritable === "function" && probe.isWritable();
89
+ }
90
+
91
+ /** True when the node is a source that reports itself read-only. See isWritableSource. */
92
+ export function isReadOnlySource(node: MetaData | undefined): node is MetaSource {
93
+ if (!isMetaSource(node)) return false;
94
+ const probe = node as unknown as { isReadOnly?: () => boolean };
95
+ return typeof probe.isReadOnly === "function" && probe.isReadOnly();
96
+ }