@astrale-os/kernel-dsl 0.2.0-beta.7 → 0.2.0-beta.8

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 (35) hide show
  1. package/dist/v1/addressing/domain-dependency.d.ts +10 -0
  2. package/dist/v1/addressing/domain-dependency.js +6 -1
  3. package/dist/v1/addressing/index.d.ts +1 -1
  4. package/dist/v1/addressing/patterns.d.ts +4 -0
  5. package/dist/v1/addressing/patterns.js +4 -0
  6. package/dist/v1/builder/index.d.ts +1 -1
  7. package/dist/v1/builder/lowering/schema/capture.js +1 -1
  8. package/dist/v1/builder/lowering/schema/compile.js +2 -2
  9. package/dist/v1/builder/lowering/schema/context.d.ts +2 -2
  10. package/dist/v1/builder/lowering/schema/context.js +20 -15
  11. package/dist/v1/builder/schema/define.d.ts +1 -1
  12. package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
  13. package/dist/v1/builder/schema/input.d.ts +1 -1
  14. package/dist/v1/builder/schema/projection.d.ts +13 -2
  15. package/dist/v1/bundle/codec.js +23 -6
  16. package/dist/v1/compiled/admission/accept.js +64 -23
  17. package/dist/v1/compiled/diagnostic.d.ts +1 -1
  18. package/dist/v1/compiled/loader/load.js +9 -7
  19. package/dist/v1/compiled/model/schema.d.ts +2 -2
  20. package/dist/v1/domain/create.d.ts +3 -3
  21. package/dist/v1/domain/create.js +155 -83
  22. package/dist/v1/domain/model/domain.d.ts +7 -1
  23. package/dist/v1/domain/model/input.d.ts +9 -5
  24. package/dist/v1/schema/codec/header.d.ts +2 -2
  25. package/dist/v1/schema/codec/header.js +4 -1
  26. package/dist/v1/schema/diagnostic.d.ts +1 -1
  27. package/dist/v1/schema/model/schema.d.ts +2 -2
  28. package/dist/v1/schema/normalization/domain.js +1 -1
  29. package/dist/v1/schema/resolution/closure.d.ts +1 -1
  30. package/dist/v1/schema/resolution/closure.js +1 -1
  31. package/dist/v1/schema/resolution/context.js +77 -45
  32. package/dist/v1/schema/resolution/domain.js +9 -6
  33. package/dist/v1/schema/resolution/value.registry.js +2 -2
  34. package/dist/v1/schema/validation/domain.structure.js +6 -2
  35. package/package.json +1 -1
@@ -6,7 +6,7 @@ const admittedDomains = new WeakSet();
6
6
  export function isDomain(input) {
7
7
  return input !== null && typeof input === 'object' && admittedDomains.has(input);
8
8
  }
9
- /** Retain source provenance without losing the admitted Domain identity. */
9
+ /** Retain source provenance without losing the admitted root Domain identity. */
10
10
  export function createSourceDomain(domain, source) {
11
11
  if (!isDomain(domain))
12
12
  throw new TypeError('Source provenance requires an admitted Domain.');
@@ -14,12 +14,129 @@ export function createSourceDomain(domain, source) {
14
14
  admittedDomains.add(resolved);
15
15
  return resolved;
16
16
  }
17
+ /** Construct one resolved Definition universe and one memoized facade per closure Domain. */
17
18
  export function createDomain(input) {
18
- const classesByKey = createClasses(input.definitions, input.validation);
19
+ const entries = new Map();
20
+ for (const entry of input.entries) {
21
+ if (entries.has(entry.origin))
22
+ throw new TypeError(`Domain entry ${entry.origin} is duplicated.`);
23
+ entries.set(entry.origin, entry);
24
+ }
25
+ const root = entries.get(input.root.origin);
26
+ if (root === undefined || root.revision !== input.root.revision) {
27
+ throw new TypeError('Domain root is absent or has the wrong revision.');
28
+ }
29
+ const declarations = Object.freeze(input.entries.flatMap(({ definitions }) => definitions));
30
+ const classesByKey = createClasses(declarations, input.validation);
31
+ const definitionsByKey = createDefinitions(declarations, classesByKey, input);
32
+ const callables = createCallables(definitionsByKey, classesByKey);
33
+ const facades = new Map();
34
+ const projecting = new Set();
35
+ const project = (entry) => {
36
+ const cached = facades.get(entry.origin);
37
+ if (cached !== undefined)
38
+ return cached;
39
+ if (projecting.has(entry.origin))
40
+ throw new TypeError(`Dependency cycle reaches ${entry.origin}.`);
41
+ projecting.add(entry.origin);
42
+ const visibleEntries = reachableEntries(entry, entries);
43
+ const visibleOrigins = new Set(visibleEntries.map(({ origin }) => origin));
44
+ const local = [...definitionsByKey.values()]
45
+ .filter(({ ref }) => ref.origin === entry.origin)
46
+ .sort((left, right) => compare(left.key, right.key));
47
+ const classes = namespace(local, 'class');
48
+ const functions = namespace(local, 'function');
49
+ const policies = namespace(local, 'policy');
50
+ const views = namespace(local, 'view');
51
+ const coreNodes = namespace(local, 'core');
52
+ const domainEndpoint = Object.freeze({
53
+ kind: 'domain',
54
+ origin: entry.origin,
55
+ });
56
+ const coreEdges = Object.freeze(entry.coreEdges.map((edge) => {
57
+ const classValue = classesByKey.get(Key.of(edge.class));
58
+ if (classValue?.kind !== 'edge') {
59
+ throw new TypeError(`Core Edge has no Edge Class ${Key.of(edge.class)}.`);
60
+ }
61
+ return Object.freeze({
62
+ class: classValue,
63
+ source: edge.source.kind === 'domain'
64
+ ? domainEndpoint
65
+ : requiredCore(definitionsByKey, edge.source),
66
+ target: edge.target.kind === 'domain'
67
+ ? domainEndpoint
68
+ : requiredCore(definitionsByKey, edge.target),
69
+ properties: edge.properties,
70
+ });
71
+ }));
72
+ const dependencies = frozenRecord(Object.entries(entry.dependencies).map(([alias, pin]) => {
73
+ const dependency = entries.get(pin.origin);
74
+ if (dependency === undefined || dependency.revision !== pin.revision) {
75
+ throw new TypeError(`Dependency alias ${alias} has no exact ${pin.origin}@${pin.revision} entry.`);
76
+ }
77
+ return [alias, project(dependency)];
78
+ }));
79
+ const definitionLookup = ((value) => {
80
+ const resolved = definitionsByKey.get(typeof value === 'string' ? value : Key.of(value));
81
+ return resolved !== undefined && visibleOrigins.has(resolved.ref.origin)
82
+ ? resolved
83
+ : undefined;
84
+ });
85
+ const domain = {
86
+ origin: entry.origin,
87
+ revision: entry.revision,
88
+ closure: Object.freeze(visibleEntries
89
+ .map(({ origin, revision }) => Object.freeze({ origin, revision }))
90
+ .sort((left, right) => compare(left.origin, right.origin))),
91
+ dependencies,
92
+ classes,
93
+ functions,
94
+ policies,
95
+ views,
96
+ core: Object.freeze({ nodes: coreNodes, edges: coreEdges }),
97
+ definition: definitionLookup,
98
+ callable(key) {
99
+ const callable = callables.get(key);
100
+ if (callable === undefined)
101
+ return undefined;
102
+ const origin = 'ref' in callable ? callable.ref.origin : callable.owner.ref.origin;
103
+ return visibleOrigins.has(origin) ? callable : undefined;
104
+ },
105
+ definitions(kind) {
106
+ return kind === undefined ? local : local.filter(({ ref }) => ref.kind === kind);
107
+ },
108
+ viewsFor(target) {
109
+ return Object.freeze(Object.values(views).filter((view) => {
110
+ if (view.target.kind === 'domain')
111
+ return target === 'domain';
112
+ if (target === 'domain')
113
+ return false;
114
+ return view.target.definitions.some((ref) => {
115
+ const selected = classesByKey.get(Key.of(ref));
116
+ if (target.kind === 'node' && selected?.kind === 'node') {
117
+ return target.satisfies(selected);
118
+ }
119
+ if (target.kind === 'edge' && selected?.kind === 'edge') {
120
+ return target.satisfies(selected);
121
+ }
122
+ return false;
123
+ });
124
+ }));
125
+ },
126
+ };
127
+ const admitted = Object.freeze(domain);
128
+ facades.set(entry.origin, admitted);
129
+ admittedDomains.add(admitted);
130
+ projecting.delete(entry.origin);
131
+ return admitted;
132
+ };
133
+ return project(root);
134
+ }
135
+ function createDefinitions(declarations, classesByKey, input) {
19
136
  const definitionsByKey = new Map();
20
137
  for (const value of classesByKey.values())
21
138
  definitionsByKey.set(value.key, value);
22
- for (const definition of input.definitions) {
139
+ for (const definition of declarations) {
23
140
  if (definition.ref.kind === 'class')
24
141
  continue;
25
142
  if (isFunctionDefinition(definition)) {
@@ -44,35 +161,33 @@ export function createDomain(input) {
44
161
  }
45
162
  if (isPolicyDefinition(definition)) {
46
163
  const key = Key.of(definition.ref);
47
- const resolved = Object.freeze({
164
+ definitionsByKey.set(key, Object.freeze({
48
165
  ref: definition.ref,
49
166
  key,
50
167
  expression: definition.expression,
51
168
  description: definition.description,
52
169
  definition,
53
- });
54
- definitionsByKey.set(key, resolved);
170
+ }));
55
171
  continue;
56
172
  }
57
173
  if (isViewDefinition(definition)) {
58
174
  const key = Key.of(definition.ref);
59
- const resolved = Object.freeze({
175
+ definitionsByKey.set(key, Object.freeze({
60
176
  ref: definition.ref,
61
177
  key,
62
178
  target: definition.target,
63
179
  description: definition.description,
64
180
  definition,
65
- });
66
- definitionsByKey.set(key, resolved);
67
- continue;
181
+ }));
68
182
  }
69
183
  }
70
- for (const definition of input.definitions) {
184
+ for (const definition of declarations) {
71
185
  if (!isCoreDefinition(definition))
72
186
  continue;
73
187
  const classValue = classesByKey.get(Key.of(definition.class));
74
- if (classValue?.kind !== 'node')
188
+ if (classValue?.kind !== 'node') {
75
189
  throw new TypeError(`Core ${Key.of(definition.ref)} has no Node Class.`);
190
+ }
76
191
  if (classValue.abstract) {
77
192
  throw new TypeError(`Core ${Key.of(definition.ref)} cannot use abstract Class ${classValue.key}.`);
78
193
  }
@@ -85,84 +200,41 @@ export function createDomain(input) {
85
200
  });
86
201
  definitionsByKey.set(resolved.key, resolved);
87
202
  }
88
- const local = [...definitionsByKey.values()]
89
- .filter(({ ref }) => ref.origin === input.origin)
90
- .sort((left, right) => compare(left.key, right.key));
91
- const classes = namespace(local, 'class');
92
- const functions = namespace(local, 'function');
93
- const policies = namespace(local, 'policy');
94
- const views = namespace(local, 'view');
95
- const coreNodes = namespace(local, 'core');
96
- const domainEndpoint = Object.freeze({
97
- kind: 'domain',
98
- origin: input.origin,
99
- });
100
- const coreEdges = Object.freeze(input.coreEdges.map((edge) => {
101
- const classValue = classesByKey.get(Key.of(edge.class));
102
- if (classValue?.kind !== 'edge')
103
- throw new TypeError(`Core Edge has no Edge Class ${Key.of(edge.class)}.`);
104
- return Object.freeze({
105
- class: classValue,
106
- source: edge.source.kind === 'domain'
107
- ? domainEndpoint
108
- : requiredCore(definitionsByKey, edge.source),
109
- target: edge.target.kind === 'domain'
110
- ? domainEndpoint
111
- : requiredCore(definitionsByKey, edge.target),
112
- properties: edge.properties,
113
- });
114
- }));
203
+ return definitionsByKey;
204
+ }
205
+ function createCallables(definitions, classes) {
115
206
  const callables = new Map();
116
- for (const definition of definitionsByKey.values()) {
117
- if (definition.ref.kind === 'function')
207
+ for (const definition of definitions.values()) {
208
+ if (definition.ref.kind === 'function') {
118
209
  callables.set(definition.key, definition);
210
+ }
119
211
  }
120
- for (const definition of classesByKey.values()) {
212
+ for (const definition of classes.values()) {
121
213
  if (definition.kind !== 'node')
122
214
  continue;
123
- for (const method of [...definition.methods, ...definition.static.methods])
215
+ for (const method of [...definition.methods, ...definition.static.methods]) {
124
216
  callables.set(method.key, method);
217
+ }
125
218
  }
126
- const definitionLookup = ((value) => definitionsByKey.get(typeof value === 'string' ? value : Key.of(value)));
127
- const domain = {
128
- origin: input.origin,
129
- revision: input.revision,
130
- closure: Object.freeze(input.closure.map(({ origin, revision }) => Object.freeze({ origin, revision }))),
131
- dependencies: Object.freeze(input.dependencies.map(({ origin, revision }) => Object.freeze({ origin, revision }))),
132
- classes,
133
- functions,
134
- policies,
135
- views,
136
- core: Object.freeze({ nodes: coreNodes, edges: coreEdges }),
137
- definition: definitionLookup,
138
- callable(key) {
139
- return callables.get(key);
140
- },
141
- definitions(kind) {
142
- return kind === undefined ? local : local.filter(({ ref }) => ref.kind === kind);
143
- },
144
- viewsFor(target) {
145
- return Object.freeze(Object.values(views).filter((view) => {
146
- if (view.target.kind === 'domain')
147
- return target === 'domain';
148
- if (target === 'domain')
149
- return false;
150
- return view.target.definitions.some((ref) => {
151
- const selected = classesByKey.get(Key.of(ref));
152
- if (target.kind === 'node' && selected?.kind === 'node') {
153
- return target.satisfies(selected);
154
- }
155
- if (target.kind === 'edge' && selected?.kind === 'edge') {
156
- return target.satisfies(selected);
157
- }
158
- return false;
159
- });
160
- }));
161
- },
162
- };
163
- const admitted = Object.freeze(domain);
164
- admittedDomains.add(admitted);
165
- return admitted;
219
+ return callables;
220
+ }
221
+ function reachableEntries(root, entries) {
222
+ const reachable = new Map();
223
+ const pending = [root];
224
+ while (pending.length > 0) {
225
+ const current = pending.pop();
226
+ if (reachable.has(current.origin))
227
+ continue;
228
+ reachable.set(current.origin, current);
229
+ for (const dependency of Object.values(current.dependencies)) {
230
+ const nested = entries.get(dependency.origin);
231
+ if (nested === undefined || nested.revision !== dependency.revision) {
232
+ throw new TypeError(`Domain universe has no exact ${dependency.origin}@${dependency.revision}.`);
233
+ }
234
+ pending.push(nested);
235
+ }
236
+ }
237
+ return [...reachable.values()];
166
238
  }
167
239
  function namespace(definitions, kind) {
168
240
  return frozenRecord(definitions
@@ -14,12 +14,18 @@ export interface CoreNamespace<Nodes extends Readonly<Record<string, unknown>> =
14
14
  readonly nodes: Readonly<Nodes>;
15
15
  readonly edges: readonly ResolvedCoreEdge[];
16
16
  }
17
+ /** Erased recursive dependency projection used when no authored Schema witness is available. */
18
+ export interface DependencyDomains {
19
+ readonly [alias: string]: Domain<Readonly<Record<string, AnyResolvedClass>>, Readonly<Record<string, ResolvedFunction>>, Readonly<Record<string, ResolvedPolicy>>, Readonly<Record<string, ResolvedView>>, Readonly<Record<string, ResolvedCoreDefinition>>, Revision>;
20
+ }
21
+ export type DependencyDomainNamespace<Dependencies extends Readonly<Record<string, unknown>> = DependencyDomains> = Readonly<Dependencies>;
17
22
  export interface Domain<Classes extends Readonly<Record<string, unknown>> = Readonly<Record<string, AnyResolvedClass>>, Functions extends Readonly<Record<string, unknown>> = Readonly<Record<string, ResolvedFunction>>, Policies extends Readonly<Record<string, unknown>> = Readonly<Record<string, ResolvedPolicy>>, Views extends Readonly<Record<string, unknown>> = Readonly<Record<string, ResolvedView>>, CoreNodes extends Readonly<Record<string, unknown>> = Readonly<Record<string, ResolvedCoreDefinition>>, RevisionValue extends Revision = Revision> {
18
23
  readonly origin: Key.Origin;
19
24
  readonly revision: RevisionValue;
20
25
  /** Exact admitted root and transitive dependency revisions, ordered by origin. */
21
26
  readonly closure: readonly DomainDependency[];
22
- readonly dependencies: readonly DomainDependency[];
27
+ /** Exact immutable direct dependency Domains keyed by authored Schema aliases. */
28
+ readonly dependencies: DependencyDomainNamespace;
23
29
  readonly classes: ClassNamespace<Classes>;
24
30
  readonly functions: FunctionNamespace<Functions>;
25
31
  readonly policies: PolicyNamespace<Policies>;
@@ -1,17 +1,21 @@
1
1
  import type { Key as AddressKey, PropertyKey, Revision } from '../../addressing/index.js';
2
- import type { DomainDependency } from '../../addressing/index.js';
2
+ import type { DomainDependency, DomainDependencyMap } from '../../addressing/index.js';
3
3
  import type { CoreEdge } from '../../language/core/model/core.js';
4
4
  import type { Definition } from '../../language/definition.js';
5
5
  import type { Diagnostic } from '../../language/validation/diagnostic.js';
6
6
  import type { OrderingKind } from '../../language/value/ordering.js';
7
- /** Dependency-closed, representation-neutral input to the one Domain constructor. */
8
- export interface DomainInput {
7
+ /** One admitted Schema entry in a dependency-closed Domain universe. */
8
+ export interface DomainEntryInput {
9
9
  readonly origin: AddressKey.Origin;
10
10
  readonly revision: Revision;
11
- readonly closure: readonly DomainDependency[];
12
- readonly dependencies: readonly DomainDependency[];
11
+ readonly dependencies: DomainDependencyMap;
13
12
  readonly definitions: readonly Definition[];
14
13
  readonly coreEdges: readonly CoreEdge[];
14
+ }
15
+ /** Dependency-closed, representation-neutral input to the one Domain universe constructor. */
16
+ export interface DomainInput {
17
+ readonly root: DomainDependency;
18
+ readonly entries: readonly DomainEntryInput[];
15
19
  readonly validation: DomainValidation;
16
20
  }
17
21
  /** Already-bound validation semantics supplied by source compilation or compiled loading. */
@@ -1,5 +1,5 @@
1
1
  import type { Key } from '../../addressing/index.js';
2
- import type { DomainDependency } from '../../addressing/index.js';
2
+ import type { DomainDependencyMap } from '../../addressing/index.js';
3
3
  /**
4
4
  * Structurally trusted routing information used to discover an exact
5
5
  * dependency closure before whole-Domain semantic acceptance is possible.
@@ -11,7 +11,7 @@ export interface DomainSchemaHeader {
11
11
  readonly format: 'astrale.dsl';
12
12
  readonly version: 'v1';
13
13
  readonly origin: Key.Origin;
14
- readonly dependencies: readonly DomainDependency[];
14
+ readonly dependencies: DomainDependencyMap;
15
15
  }
16
16
  /** Decode detached Schema bytes or text and retain only validated closure-routing identity. */
17
17
  export declare function header(input: string | Uint8Array): DomainSchemaHeader;
@@ -17,7 +17,10 @@ export function header(input) {
17
17
  return Object.freeze({
18
18
  format: structured.format,
19
19
  version: structured.version,
20
- dependencies: Object.freeze([...structured.dependencies]),
20
+ dependencies: Object.freeze(Object.fromEntries(Object.entries(structured.dependencies).map(([alias, dependency]) => [
21
+ alias,
22
+ Object.freeze({ ...dependency }),
23
+ ]))),
21
24
  origin: structured.origin,
22
25
  });
23
26
  }
@@ -1,4 +1,4 @@
1
- export type SchemaDiagnosticCode = 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_DEPENDENCY_USED' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'DV_VIEW_TARGET' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
1
+ export type SchemaDiagnosticCode = 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_KIND' | 'DV_CORE_EDGE_CLASS' | 'DV_CORE_EDGE_CONSTRAINTS' | 'DV_CORE_EDGE_KEY' | 'DV_CORE_EDGE_PROPERTIES' | 'DV_CORE_NODE_CLASS' | 'DV_CORE_NODE_PROPERTIES' | 'DV_CORE_REF' | 'DV_DATA_COMPOSITION' | 'DV_DEPENDENCY_ACYCLIC' | 'DV_DEPENDENCY_DIRECT' | 'DV_DEPENDENCY_EXACT' | 'DV_DEPENDENCY_LIMITS' | 'DV_DEPENDENCY_REVISION_COHERENCE' | 'DV_DEPENDENCY_UNIQUE' | 'DV_EDGE_ACCEPTS' | 'DV_EDGE_CLASS_COMPOSITION' | 'DV_EDGE_CONSTRAINT' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | 'DV_VIEW_TARGET' | 'PL_BUDGET' | 'PL_COMPOSITION' | 'PL_CONNECTIVITY' | 'PL_REF' | 'PL_REPEAT' | 'PL_TARGET' | 'PL_TYPING' | 'PL_USE' | 'PL_VARIABLE_SCOPE' | 'STRUCTURE_ABSTRACT_POLICY' | 'STRUCTURE_CALLABLE_POLICY_AUTH' | 'STRUCTURE_DUPLICATE_ITEM' | 'STRUCTURE_EDGE_ORIENTATION' | 'STRUCTURE_FORMAT' | 'STRUCTURE_INVALID' | 'STRUCTURE_KEY' | 'STRUCTURE_MAXIMUM' | 'STRUCTURE_MINIMUM' | 'STRUCTURE_MULTIPLE' | 'STRUCTURE_REQUIRED' | 'STRUCTURE_TYPE' | 'STRUCTURE_UNION' | 'STRUCTURE_UNKNOWN_FIELD' | 'STRUCTURE_VALUE' | 'UNSUPPORTED_DOCUMENT_FORMAT' | 'UNSUPPORTED_DSL_VERSION';
2
2
  export interface SchemaDiagnostic<out Code extends SchemaDiagnosticCode = SchemaDiagnosticCode> {
3
3
  readonly code: Code;
4
4
  readonly pointer: string;
@@ -1,5 +1,5 @@
1
1
  import type { Key } from '../../addressing/index.js';
2
- import type { DomainDependency } from '../../addressing/index.js';
2
+ import type { DomainDependencyMap } from '../../addressing/index.js';
3
3
  import type { Domain, DomainOfSource, DomainSource } from '../../domain/model/domain.js';
4
4
  import type { FunctionDefinition } from '../../language/callable/model/callable.js';
5
5
  import type { ClassDefinition } from '../../language/class/model/class.js';
@@ -10,7 +10,7 @@ export interface DomainSchemaIR {
10
10
  readonly format: 'astrale.dsl';
11
11
  readonly version: 'v1';
12
12
  readonly origin: Key.Origin;
13
- readonly dependencies: readonly DomainDependency[];
13
+ readonly dependencies: DomainDependencyMap;
14
14
  readonly classes: Readonly<Record<string, ClassDefinition>>;
15
15
  readonly functions: Readonly<Record<string, FunctionDefinition>>;
16
16
  readonly policies: Readonly<Record<string, PolicyDefinition>>;
@@ -11,7 +11,7 @@ export function normalizeDomainSchema(schema) {
11
11
  format: 'astrale.dsl',
12
12
  version: 'v1',
13
13
  origin: schema.origin,
14
- dependencies: [...schema.dependencies].sort((left, right) => compareUnicode(left.origin, right.origin)),
14
+ dependencies: mapRecord(schema.dependencies, (dependency) => ({ ...dependency })),
15
15
  classes,
16
16
  functions: mapRecord(schema.functions, canonicalizeFunction),
17
17
  policies: mapRecord(schema.policies, canonicalizePolicy),
@@ -6,4 +6,4 @@ export declare function rememberDomainClosure(schema: DomainSchema, dependencies
6
6
  export declare function knownDomainClosure(schema: DomainSchema): readonly DomainSchemaEntry[] | undefined;
7
7
  export declare function requireDomainClosure(schema: DomainSchema): readonly DomainSchemaEntry[];
8
8
  export declare function lookupForDomainClosure(schema: DomainSchema): DomainSchemaLookup;
9
- export declare function lookupEntriesForDependencies(dependencies: readonly DomainSchema[]): readonly DomainSchemaEntry[];
9
+ export declare function lookupEntriesForDependencies(dependencies: Readonly<Record<string, DomainSchema>>): readonly DomainSchemaEntry[];
@@ -29,7 +29,7 @@ export function lookupForDomainClosure(schema) {
29
29
  }
30
30
  export function lookupEntriesForDependencies(dependencies) {
31
31
  const entries = new Map();
32
- for (const schema of dependencies) {
32
+ for (const schema of Object.values(dependencies)) {
33
33
  const revision = revisionOfDomainSchema(schema);
34
34
  entries.set(closureKey(schema.origin, revision), { schema, revision });
35
35
  for (const entry of requireDomainClosure(schema)) {
@@ -1,3 +1,4 @@
1
+ import { dependencyLimits } from '../../addressing/domain-dependency.js';
1
2
  import { definitionRefIndex } from '../../addressing/ordering.js';
2
3
  import { DOMAIN_REVISION_PATTERN } from '../../addressing/patterns.js';
3
4
  import { revisionOfDomainSchema } from '../canonical/revision.js';
@@ -9,8 +10,11 @@ export function buildDomainResolutionContext(candidate, lookup) {
9
10
  const dependencies = new Map();
10
11
  const definitions = new Map();
11
12
  const directOrigins = new Set();
12
- for (const [index, dependency] of candidate.dependencies.entries()) {
13
- const pointer = `/dependencies/${index}`;
13
+ if (Object.keys(candidate.dependencies).length > dependencyLimits.direct) {
14
+ diagnostics.push(diagnostic('DV_DEPENDENCY_LIMITS', '/dependencies', `A Domain may declare at most ${dependencyLimits.direct} direct dependency aliases.`));
15
+ }
16
+ for (const [alias, dependency] of Object.entries(candidate.dependencies)) {
17
+ const pointer = `/dependencies/${escapeToken(alias)}`;
14
18
  if (!DOMAIN_REVISION_PATTERN.test(dependency.revision))
15
19
  continue;
16
20
  if (dependency.origin === candidate.origin) {
@@ -68,48 +72,78 @@ export function buildDomainResolutionContext(candidate, lookup) {
68
72
  }
69
73
  function loadDependencyClosure(candidate, dependencies, lookup, diagnostics) {
70
74
  const revisionByOrigin = new Map();
71
- const visiting = new Set();
72
75
  const visited = new Set();
73
- const visit = (schema, key) => {
74
- if (visiting.has(key)) {
75
- diagnostics.push(diagnostic('DV_DEPENDENCY_ACYCLIC', '/dependencies', `Dependency cycle reaches ${schema.origin}.`));
76
- return;
76
+ const activeOrigins = new Set([candidate.origin]);
77
+ const frames = [
78
+ {
79
+ schema: candidate,
80
+ key: `${candidate.origin}\u0000candidate`,
81
+ depth: 0,
82
+ edges: Object.entries(candidate.dependencies),
83
+ index: 0,
84
+ },
85
+ ];
86
+ let closureLimitReported = false;
87
+ while (frames.length > 0) {
88
+ const frame = frames.at(-1);
89
+ const edge = frame.edges[frame.index++];
90
+ if (edge === undefined) {
91
+ frames.pop();
92
+ activeOrigins.delete(frame.schema.origin);
93
+ visited.add(frame.key);
94
+ continue;
77
95
  }
78
- if (visited.has(key))
79
- return;
80
- visiting.add(key);
81
- for (const dependency of schema.dependencies) {
82
- if (dependency.origin === candidate.origin) {
83
- diagnostics.push(diagnostic('DV_DEPENDENCY_ACYCLIC', '/dependencies', `Dependency cycle returns to ${candidate.origin}.`));
84
- continue;
85
- }
86
- const previous = revisionByOrigin.get(dependency.origin);
87
- if (previous !== undefined && previous !== dependency.revision) {
88
- diagnostics.push(diagnostic('DV_DEPENDENCY_REVISION_COHERENCE', '/dependencies', `Closure contains ${dependency.origin} at both ${previous} and ${dependency.revision}.`));
89
- }
90
- else {
91
- revisionByOrigin.set(dependency.origin, dependency.revision);
92
- }
93
- const nested = dependencies.get(dependency.origin) ?? lookup.get(dependency.origin, dependency.revision);
94
- if (nested === undefined) {
95
- diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', '/dependencies', `Dependency closure is missing ${dependency.origin}@${dependency.revision}.`));
96
- continue;
97
- }
98
- if (nested.origin !== dependency.origin) {
99
- diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', '/dependencies', `Schema lookup value origin ${nested.origin} disagrees with ${dependency.origin}.`));
100
- continue;
101
- }
102
- if (revisionOfDomainSchema(nested) !== dependency.revision) {
103
- diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', '/dependencies', `Schema lookup content for ${dependency.origin} disagrees with ${dependency.revision}.`));
104
- continue;
96
+ const [alias, dependency] = edge;
97
+ const pointer = frames.length === 1 ? `/dependencies/${escapeToken(alias)}` : '/dependencies';
98
+ if (activeOrigins.has(dependency.origin)) {
99
+ diagnostics.push(diagnostic('DV_DEPENDENCY_ACYCLIC', pointer, `Dependency cycle reaches ${dependency.origin}.`));
100
+ continue;
101
+ }
102
+ const previous = revisionByOrigin.get(dependency.origin);
103
+ if (previous !== undefined && previous !== dependency.revision) {
104
+ diagnostics.push(diagnostic('DV_DEPENDENCY_REVISION_COHERENCE', pointer, `Closure contains ${dependency.origin} at both ${previous} and ${dependency.revision}.`));
105
+ }
106
+ else {
107
+ revisionByOrigin.set(dependency.origin, dependency.revision);
108
+ }
109
+ const nested = dependencies.get(dependency.origin) ?? lookup.get(dependency.origin, dependency.revision);
110
+ if (nested === undefined) {
111
+ diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', pointer, `Dependency closure is missing ${dependency.origin}@${dependency.revision}.`));
112
+ continue;
113
+ }
114
+ if (nested.origin !== dependency.origin) {
115
+ diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', pointer, `Schema lookup value origin ${nested.origin} disagrees with ${dependency.origin}.`));
116
+ continue;
117
+ }
118
+ if (revisionOfDomainSchema(nested) !== dependency.revision) {
119
+ diagnostics.push(diagnostic('DV_DEPENDENCY_EXACT', pointer, `Schema lookup content for ${dependency.origin} disagrees with ${dependency.revision}.`));
120
+ continue;
121
+ }
122
+ dependencies.set(nested.origin, nested);
123
+ if (dependencies.size + 1 > dependencyLimits.closure) {
124
+ if (!closureLimitReported) {
125
+ diagnostics.push(diagnostic('DV_DEPENDENCY_LIMITS', '/dependencies', `A dependency closure may contain at most ${dependencyLimits.closure} Domains.`));
126
+ closureLimitReported = true;
105
127
  }
106
- dependencies.set(nested.origin, nested);
107
- visit(nested, `${dependency.origin}\u0000${dependency.revision}`);
128
+ continue;
129
+ }
130
+ const depth = frame.depth + 1;
131
+ if (depth > dependencyLimits.depth) {
132
+ diagnostics.push(diagnostic('DV_DEPENDENCY_LIMITS', pointer, `Dependency depth ${depth} exceeds the maximum ${dependencyLimits.depth}.`));
133
+ continue;
108
134
  }
109
- visiting.delete(key);
110
- visited.add(key);
111
- };
112
- visit(candidate, `${candidate.origin}\u0000candidate`);
135
+ const key = `${dependency.origin}\u0000${dependency.revision}`;
136
+ if (visited.has(key))
137
+ continue;
138
+ activeOrigins.add(nested.origin);
139
+ frames.push({
140
+ schema: nested,
141
+ key,
142
+ depth,
143
+ edges: Object.entries(nested.dependencies),
144
+ index: 0,
145
+ });
146
+ }
113
147
  }
114
148
  function validateDependencyUse(candidate, directOrigins, diagnostics) {
115
149
  const used = collectUsedForeignOrigins(candidate);
@@ -118,9 +152,7 @@ function validateDependencyUse(candidate, directOrigins, diagnostics) {
118
152
  diagnostics.push(diagnostic('DV_DEPENDENCY_DIRECT', '', `Foreign origin ${origin} is referenced without an exact direct dependency.`));
119
153
  }
120
154
  }
121
- for (const origin of directOrigins) {
122
- if (!used.has(origin)) {
123
- diagnostics.push(diagnostic('DV_DEPENDENCY_USED', '/dependencies', `Dependency ${origin} is not referenced by the Domain.`));
124
- }
125
- }
155
+ }
156
+ function escapeToken(value) {
157
+ return value.replaceAll('~', '~0').replaceAll('/', '~1');
126
158
  }
@@ -29,17 +29,20 @@ function resolveWithLookup(schema, lookup) {
29
29
  const closure = [schema, ...validation.context.dependencies.values()];
30
30
  const registries = new Map(closure.map((owner) => [owner.origin, createDomainValueSchemaRegistry(owner, lookup)]));
31
31
  const input = {
32
- origin: schema.origin,
33
- revision: revisionOfDomainSchema(schema),
34
- closure: Object.freeze(closure
32
+ root: Object.freeze({
33
+ origin: schema.origin,
34
+ revision: revisionOfDomainSchema(schema),
35
+ }),
36
+ entries: Object.freeze(closure
37
+ .filter((owner, index, values) => values.findIndex(({ origin }) => origin === owner.origin) === index)
35
38
  .map((owner) => Object.freeze({
36
39
  origin: owner.origin,
37
40
  revision: revisionOfDomainSchema(owner),
41
+ dependencies: owner.dependencies,
42
+ definitions: Object.freeze(definitionsOf(owner)),
43
+ coreEdges: owner.core.edges,
38
44
  }))
39
45
  .sort((left, right) => compareUnicode(left.origin, right.origin))),
40
- dependencies: schema.dependencies,
41
- definitions: Object.freeze(closure.flatMap(definitionsOf)),
42
- coreEdges: schema.core.edges,
43
46
  validation: sourceValidation(closure, registries),
44
47
  };
45
48
  const domain = createDomain(input);
@@ -9,7 +9,7 @@ export function createDomainValueSchemaRegistry(schema, lookup = lookupForDomain
9
9
  export function createValueSchemaRegistry(context) {
10
10
  const directDependencies = [];
11
11
  const transitiveDependencies = [];
12
- const directOrigins = new Set(context.candidate.dependencies.map(({ origin }) => origin));
12
+ const directOrigins = new Set(Object.values(context.candidate.dependencies).map(({ origin }) => origin));
13
13
  for (const schema of context.dependencies.values()) {
14
14
  const entry = {
15
15
  ...domainValueRegistry(schema),
@@ -33,7 +33,7 @@ function domainValueRegistry(schema) {
33
33
  .map(([name]) => ({ origin: schema.origin, kind: 'class', name }));
34
34
  return Object.freeze({
35
35
  origin: schema.origin,
36
- dependencies: schema.dependencies,
36
+ dependencies: Object.values(schema.dependencies),
37
37
  nodeDefinitions: Object.freeze(nodeDefinitions),
38
38
  });
39
39
  }