@astrale-os/kernel-dsl 1.0.0-beta.7 → 1.0.0-beta.9

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 (44) hide show
  1. package/dist/v1/builder/class/authoring.d.ts +29 -4
  2. package/dist/v1/builder/class/authoring.js +15 -5
  3. package/dist/v1/builder/index.d.ts +1 -1
  4. package/dist/v1/builder/lowering/class.js +12 -2
  5. package/dist/v1/builder/lowering/property-keys.d.ts +27 -0
  6. package/dist/v1/builder/lowering/property-keys.js +108 -0
  7. package/dist/v1/builder/lowering/schema/compile.js +8 -3
  8. package/dist/v1/builder/lowering/schema/context.d.ts +2 -0
  9. package/dist/v1/builder/lowering/schema/context.js +12 -0
  10. package/dist/v1/builder/lowering/schema/lifetime.d.ts +11 -0
  11. package/dist/v1/builder/lowering/schema/lifetime.js +55 -0
  12. package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
  13. package/dist/v1/compiled/admission/semantics/cardinality.d.ts +1 -1
  14. package/dist/v1/compiled/admission/semantics/cardinality.js +16 -1
  15. package/dist/v1/compiled/admission/semantics/class.js +23 -0
  16. package/dist/v1/compiled/admission/semantics/core.js +16 -0
  17. package/dist/v1/compiled/admission/wire/class.js +24 -4
  18. package/dist/v1/language/class/inheritance.d.ts +9 -0
  19. package/dist/v1/language/class/inheritance.js +10 -1
  20. package/dist/v1/language/class/lifetime.d.ts +50 -11
  21. package/dist/v1/language/class/lifetime.js +89 -20
  22. package/dist/v1/language/class/model/class.d.ts +16 -1
  23. package/dist/v1/language/class/model/edge.d.ts +22 -6
  24. package/dist/v1/language/class/unique.d.ts +123 -13
  25. package/dist/v1/language/class/unique.js +238 -27
  26. package/dist/v1/language/index.d.ts +5 -5
  27. package/dist/v1/language/index.js +3 -3
  28. package/dist/v1/schema/compatibility/api.d.ts +1 -0
  29. package/dist/v1/schema/compatibility/api.js +1 -0
  30. package/dist/v1/schema/compatibility/extension.d.ts +11 -0
  31. package/dist/v1/schema/compatibility/extension.js +22 -0
  32. package/dist/v1/schema/compatibility/model.d.ts +7 -0
  33. package/dist/v1/schema/compatibility/scope.d.ts +2 -0
  34. package/dist/v1/schema/compatibility/scope.js +4 -1
  35. package/dist/v1/schema/compatibility/structure/requirements.js +14 -11
  36. package/dist/v1/schema/compatibility/versioning/surface.js +13 -8
  37. package/dist/v1/schema/diagnostic.d.ts +1 -1
  38. package/dist/v1/schema/normalization/domain.js +15 -4
  39. package/dist/v1/schema/validation/class.js +70 -1
  40. package/dist/v1/schema/validation/core.js +50 -0
  41. package/dist/v1/schema/validation/domain.structure.js +18 -4
  42. package/package.json +1 -1
  43. package/dist/v1/builder/lowering/core-properties.d.ts +0 -10
  44. package/dist/v1/builder/lowering/core-properties.js +0 -76
@@ -2,16 +2,19 @@ import { Key } from '../../addressing/key.js';
2
2
  import { definitionRefIndex } from '../../addressing/ordering.js';
3
3
  import { isMethodExecutable } from '../../language/callable/executable.js';
4
4
  import { classSatisfies as satisfiesClass, isGraphRoot } from '../../language/class/inheritance.js';
5
+ import { lifetimeBreaches, lifetimeScope } from '../../language/class/lifetime.js';
6
+ import { keyableProperty } from '../../language/class/unique.js';
5
7
  import { admitStateTopology } from '../../language/state/topology.js';
6
8
  import { isBlobValueSchema } from '../../language/value/model/blob.js';
7
9
  import { classifyOrdering } from '../../language/value/ordering.js';
8
10
  import { diagnostic } from '../diagnostic.js';
9
- import { createValueSchemaRegistry } from '../resolution/value.registry.js';
11
+ import { createDomainValueSchemaRegistry, createValueSchemaRegistry, } from '../resolution/value.registry.js';
10
12
  const formatDefinitionRef = Key.of;
11
13
  /** Validate the semantic contracts owned by Class declarations. */
12
14
  export function validateDefinitions(environment, analysis) {
13
15
  const diagnostics = [...analysis.diagnostics];
14
16
  const schema = environment.candidate;
17
+ let scope;
15
18
  for (const [name, declaration] of Object.entries(schema.classes)) {
16
19
  const ref = localRef(schema.origin, name);
17
20
  const pointer = `/classes/${escapePointer(name)}`;
@@ -26,7 +29,10 @@ export function validateDefinitions(environment, analysis) {
26
29
  validateParents(environment, ref, declaration.kind, declaration.extends, diagnostics);
27
30
  if (declaration.kind === 'edge') {
28
31
  validateEdgeContract(environment, ref, declaration, diagnostics);
32
+ scope ??= lifetimeScope([...environment.definitions.values()].map(({ declaration }) => declaration));
33
+ validateEdgeLifetimes(scope, ref, declaration, pointer, diagnostics);
29
34
  }
35
+ validateUniqueProperties(environment, analysis, ref, declaration, pointer, diagnostics);
30
36
  }
31
37
  validateEdgeClassComposition(environment, diagnostics);
32
38
  return Object.freeze(diagnostics);
@@ -132,6 +138,56 @@ function validateEdgeContract(environment, owner, declaration, diagnostics) {
132
138
  }
133
139
  }
134
140
  }
141
+ /**
142
+ * Each Property a key tuple names is one of the Class's own or inherited Properties, and keyable:
143
+ * a single-kind scalar by its ordering facet that no automatic program writes (D-S1). Properties
144
+ * inherit without redefinition, so the key reads one meaning on every descendant it covers. A Node
145
+ * tuple names Properties only; an Edge tuple names them as `{ property }` beside its other fields.
146
+ */
147
+ function validateUniqueProperties(environment, analysis, owner, declaration, pointer, diagnostics) {
148
+ if (declaration.kind === 'edge' && declaration.orientation !== 'directed')
149
+ return;
150
+ if (declaration.unique === undefined)
151
+ return;
152
+ const properties = analysis.definitions.get(definitionRefIndex(owner))?.properties;
153
+ if (properties === undefined)
154
+ return;
155
+ const code = declaration.kind === 'node' ? 'DV_NODE_UNIQUE' : 'DV_EDGE_UNIQUE';
156
+ const registries = new Map();
157
+ const registryOf = (origin) => {
158
+ if (!registries.has(origin)) {
159
+ const schema = environment.dependencies.get(origin);
160
+ const registry = origin === environment.candidate.origin
161
+ ? createValueSchemaRegistry(environment)
162
+ : schema === undefined
163
+ ? undefined
164
+ : createDomainValueSchemaRegistry(schema);
165
+ if (registry !== undefined)
166
+ registries.set(origin, registry);
167
+ }
168
+ return registries.get(origin);
169
+ };
170
+ const check = (key, at) => {
171
+ const property = properties.get(key);
172
+ if (property === undefined) {
173
+ diagnostics.push(diagnostic(code, at, `${key} is not a Property of the Class.`));
174
+ return;
175
+ }
176
+ const registry = registryOf(property.owner.origin);
177
+ const ordering = registry === undefined ? undefined : classifyOrdering(property.schema, registry);
178
+ if (!keyableProperty(ordering, property.auto)) {
179
+ diagnostics.push(diagnostic(code, at, 'A key Property is a string, number, boolean or instant that no automatic program writes.'));
180
+ }
181
+ };
182
+ if (declaration.kind === 'node') {
183
+ declaration.unique.forEach((tuple, t) => tuple.forEach((key, f) => check(key, `${pointer}/unique/${t}/${f}`)));
184
+ return;
185
+ }
186
+ declaration.unique.forEach((tuple, t) => tuple.forEach((field, f) => {
187
+ if (typeof field !== 'string')
188
+ check(field.property, `${pointer}/unique/${t}/${f}/property`);
189
+ }));
190
+ }
135
191
  function validateEndpoint(environment, owner, endpoint, count, side, diagnostics) {
136
192
  // An Edge Class binds only the Nodes of its Domain and its dependents: it cannot oblige the
137
193
  // Nodes of a Class another Domain owns to carry its Edges.
@@ -154,6 +210,19 @@ function validateEndpoint(environment, owner, endpoint, count, side, diagnostics
154
210
  }
155
211
  }
156
212
  }
213
+ /**
214
+ * The lifetime laws over the lifetimes a Schema declares. What an endpoint derives where it
215
+ * declares nothing is never refused here: a stored Registry decoded again keeps it.
216
+ */
217
+ function validateEdgeLifetimes(scope, owner, declaration, pointer, diagnostics) {
218
+ const sides = declaration.orientation === 'directed'
219
+ ? ['source', 'target']
220
+ : ['first endpoint', 'second endpoint'];
221
+ const keys = declaration.orientation === 'directed' ? ['source', 'target'] : ['0', '1'];
222
+ for (const { index, reason } of lifetimeBreaches(declaration, scope)) {
223
+ diagnostics.push(diagnostic('DV_EDGE_LIFETIME', `${pointer}/endpoints/${keys[index]}/onDelete`, `${formatDefinitionRef(owner)} ${sides[index]} ${reason}`));
224
+ }
225
+ }
157
226
  /** Edge inheritance is refinement, not a second interface composition mechanism. */
158
227
  function validateEdgeClassComposition(environment, diagnostics) {
159
228
  for (const resolved of environment.definitions.values()) {
@@ -1,6 +1,7 @@
1
1
  import { Key } from '../../addressing/key.js';
2
2
  import { compareUnicode, definitionRefIndex } from '../../addressing/ordering.js';
3
3
  import { isMethodExecutable } from '../../language/callable/executable.js';
4
+ import { coveringEdgeUniqueTuples, coveringNodeUniqueTuples, edgeUniqueKeys, nodeUniqueKeys, } from '../../language/class/unique.js';
4
5
  import { policyObjectKey, policyObjectOrigin, projectedMemberClass, } from '../../language/policy/model/policy.js';
5
6
  import { validateJsonInstance } from '../../language/value/evaluate/validate.js';
6
7
  import { diagnostic } from '../diagnostic.js';
@@ -10,9 +11,34 @@ export function validateCore(environment, analysis, registry) {
10
11
  const diagnostics = [];
11
12
  const nodesByRef = resolveCoreNodes(environment, analysis, registry, diagnostics);
12
13
  const edges = resolveCoreEdges(environment, analysis, registry, nodesByRef, diagnostics);
14
+ validateCoreNodeKeys(environment, nodesByRef, diagnostics);
13
15
  validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics);
14
16
  return Object.freeze(diagnostics);
15
17
  }
18
+ /**
19
+ * No two Core Nodes hold one key under a tuple covering their Classes, whichever Class or ancestor
20
+ * declares it (DV-NODE-UNIQUE); Activation checks them against stored data.
21
+ */
22
+ function validateCoreNodeKeys(environment, nodesByRef, diagnostics) {
23
+ const resolve = (ref) => environment.resolveAny(ref)?.declaration;
24
+ const holders = new Map();
25
+ for (const node of nodesByRef.values()) {
26
+ const declaration = resolve(node.classRef);
27
+ if (declaration === undefined)
28
+ continue;
29
+ const pointer = `/core/nodes/${escapePointer(node.slug)}`;
30
+ const keys = nodeUniqueKeys(coveringNodeUniqueTuples(declaration, resolve), {
31
+ props: environment.candidate.core.nodes[node.slug].properties,
32
+ });
33
+ const previous = keys.map((key) => holders.get(key)).find((at) => at !== undefined);
34
+ if (previous !== undefined) {
35
+ diagnostics.push(diagnostic('DV_CORE_NODE_UNIQUE', pointer, `Core Node holds a key the Core Node at ${previous} holds.`));
36
+ }
37
+ for (const key of keys)
38
+ if (!holders.has(key))
39
+ holders.set(key, pointer);
40
+ }
41
+ }
16
42
  function resolveCoreNodes(environment, analysis, registry, diagnostics) {
17
43
  const nodes = new Map();
18
44
  for (const [slug, node] of Object.entries(environment.candidate.core.nodes)) {
@@ -175,6 +201,7 @@ function validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics) {
175
201
  diagnostics.push(diagnostic('DV_CORE_EDGE_CONSTRAINTS', edge.pointer, 'Core Edge violates noSelf.'));
176
202
  }
177
203
  }
204
+ validateCoreEdgeKeys(environment, edges, diagnostics);
178
205
  for (const { declaration, edges: classEdges } of edgeSlices.values()) {
179
206
  if (declaration.constraints.acyclic === true && hasDirectedCycle(classEdges)) {
180
207
  diagnostics.push(diagnostic('DV_CORE_EDGE_CONSTRAINTS', '/core/edges', 'Core Edge set violates exact-Class acyclicity.'));
@@ -182,6 +209,29 @@ function validateCoreEdgeSet(environment, edges, nodesByRef, diagnostics) {
182
209
  validateCardinality(environment, declaration, classEdges, nodesByRef, diagnostics);
183
210
  }
184
211
  }
212
+ /**
213
+ * No two Core Edges hold one key under a tuple covering their Classes, whichever Class or ancestor
214
+ * declares it (DV-EDGE-UNIQUE). Core Edges have no slug, so only tuples of endpoints and
215
+ * Properties can bind them; Activation checks them against stored data.
216
+ */
217
+ function validateCoreEdgeKeys(environment, edges, diagnostics) {
218
+ const resolve = (ref) => environment.resolveAny(ref)?.declaration;
219
+ const holders = new Map();
220
+ for (const edge of edges) {
221
+ const keys = edgeUniqueKeys(coveringEdgeUniqueTuples(edge.declaration, resolve), {
222
+ source: edge.source.key,
223
+ target: edge.target.key,
224
+ props: edge.value.properties,
225
+ });
226
+ const previous = keys.map((key) => holders.get(key)).find((pointer) => pointer !== undefined);
227
+ if (previous !== undefined) {
228
+ diagnostics.push(diagnostic('DV_CORE_EDGE_UNIQUE', edge.pointer, `Core Edge holds a key the Core Edge at ${previous} holds.`));
229
+ }
230
+ for (const key of keys)
231
+ if (!holders.has(key))
232
+ holders.set(key, edge.pointer);
233
+ }
234
+ }
185
235
  function validateCardinality(environment, declaration, edges, nodesByRef, diagnostics) {
186
236
  const subjects = cardinalitySubjects(edges, nodesByRef);
187
237
  if (declaration.orientation === 'undirected') {
@@ -83,6 +83,7 @@ const coreEndpointRefSchema = discriminatedUnion('kind', [
83
83
  const edgeEndpointBaseShape = {
84
84
  role: memberNameSchema,
85
85
  accepts: uniqueArray(classRefSchema).min(1),
86
+ onDelete: enumeration(['restrict', 'detach']).optional(),
86
87
  };
87
88
  const edgeCountSchema = enumeration(['0..*', '1..*', '0..1', '1']);
88
89
  const directedEdgeEndpointsSchema = strictObject({
@@ -98,9 +99,21 @@ const edgeConstraintsSchema = strictObject({
98
99
  noSelf: literal(true).optional(),
99
100
  acyclic: literal(true).optional(),
100
101
  });
101
- // A tuple names an endpoint, then the slug (`EDGE_UNIQUE_TUPLES`). No tuple is spelled by the
102
+ // A tuple names distinct fields: endpoints, the slug and Properties by key. Normalization puts
103
+ // them, and the tuples, in canonical order (`compareEdgeUniqueFields`). No tuple is spelled by the
102
104
  // absence of the field, never by an empty list: one spelling per meaning, one revision.
103
- const edgeUniqueSchema = uniqueArray(union([tuple([literal('source'), literal('slug')]), tuple([literal('target'), literal('slug')])])).min(1);
105
+ const edgeUniqueFieldSchema = union([
106
+ literal('source'),
107
+ literal('target'),
108
+ literal('slug'),
109
+ strictObject({ property: propertyKeySchema }),
110
+ ]);
111
+ const edgeUniqueSchema = uniqueArray(uniqueArray(edgeUniqueFieldSchema).min(1), (tuple) =>
112
+ // Two spellings of one set of fields are one tuple, repeated.
113
+ JSON.stringify(tuple.map(canonicalJson).sort())).min(1);
114
+ // A Node tuple names distinct Properties by key, which normalization puts in code-point order, as
115
+ // it does the tuples (`compareNodeUniqueTuples`). Two orders of one set of keys are one tuple.
116
+ const nodeUniqueSchema = uniqueArray(uniqueArray(propertyKeySchema).min(1), (tuple) => JSON.stringify([...tuple].sort())).min(1);
104
117
  const policyPatternSubjectSchema = strictObject({ kind: literal('subject') });
105
118
  const policyPatternObjectSchema = strictObject({ kind: literal('object') });
106
119
  const policyPatternSourceSchema = strictObject({ kind: literal('source') });
@@ -250,6 +263,7 @@ const nodeClassSchema = strictObject({
250
263
  data: dataDeclarationSchema.optional(),
251
264
  icon: iconSchema.optional(),
252
265
  policies: nodeClassPoliciesSchema,
266
+ unique: nodeUniqueSchema.optional(),
253
267
  });
254
268
  const edgeClassShape = {
255
269
  ref: classRefSchema,
@@ -389,11 +403,11 @@ function isStateTransitions(input) {
389
403
  function isRecord(input) {
390
404
  return input !== null && typeof input === 'object' && !Array.isArray(input);
391
405
  }
392
- function uniqueArray(element) {
406
+ function uniqueArray(element, keyOf = canonicalJson) {
393
407
  return array(element).superRefine((values, context) => {
394
408
  const firstIndexByValue = new Map();
395
409
  values.forEach((value, index) => {
396
- const key = canonicalJson(value);
410
+ const key = keyOf(value);
397
411
  const firstIndex = firstIndexByValue.get(key);
398
412
  if (firstIndex === undefined) {
399
413
  firstIndexByValue.set(key, index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,10 +0,0 @@
1
- import type { CoreContent } from '../../language/core/model/core.js';
2
- import type { DomainSchemaLookup } from '../../schema/dependency/lookup.js';
3
- import type { DomainSchemaIR } from '../../schema/model/schema.js';
4
- import type { UnqualifiedCoreContent } from './core.js';
5
- type UnqualifiedDomainSchema = Omit<DomainSchemaIR, 'core'> & {
6
- readonly core: UnqualifiedCoreContent;
7
- };
8
- /** Lower rich builder shorthand to the canonical full-key Core representation. */
9
- export declare function canonicalizeCorePropertyKeys(candidate: UnqualifiedDomainSchema, lookup: DomainSchemaLookup): CoreContent;
10
- export {};
@@ -1,76 +0,0 @@
1
- import { PropertyKey as admitPropertyKey } from '../../addressing/index.js';
2
- import { definitionRefIndex } from '../../addressing/ordering.js';
3
- import { createPropertyValues } from '../../language/class/property-values.js';
4
- import { analyzeDefinitions } from '../../schema/analysis/class.js';
5
- import { buildDomainResolutionContext } from '../../schema/resolution/context.js';
6
- import { escapeToken, fail } from '../schema/diagnostic.js';
7
- /** Lower rich builder shorthand to the canonical full-key Core representation. */
8
- export function canonicalizeCorePropertyKeys(candidate, lookup) {
9
- // Core Property values do not participate in Class composition. Analyze the
10
- // already-canonical Definition surface, then admit Core values below.
11
- const analysisCandidate = {
12
- ...candidate,
13
- core: { nodes: {}, edges: [] },
14
- };
15
- const definitions = analyzeDefinitions(buildDomainResolutionContext(analysisCandidate, lookup)).definitions;
16
- const nodes = Object.fromEntries(Object.entries(candidate.core.nodes).map(([slug, node]) => {
17
- const definition = definitions.get(definitionRefIndex(node.class));
18
- if (definition === undefined) {
19
- fail('BUILDER_CORE_PROPERTY', `/core/nodes/${escapeToken(slug)}/class`, 'Core property shorthand requires a resolvable concrete Class.');
20
- }
21
- return [
22
- slug,
23
- {
24
- ...node,
25
- properties: qualify(node.properties, definition, `/core/nodes/${escapeToken(slug)}/properties`),
26
- },
27
- ];
28
- }));
29
- const edges = candidate.core.edges.map((edge, index) => {
30
- const definition = definitions.get(definitionRefIndex(edge.class));
31
- if (definition === undefined) {
32
- fail('BUILDER_CORE_PROPERTY', `/core/edges/${index}/class`, 'Core property shorthand requires a resolvable concrete Class.');
33
- }
34
- return {
35
- ...edge,
36
- properties: qualify(edge.properties, definition, `/core/edges/${index}/properties`),
37
- };
38
- });
39
- return { nodes, edges };
40
- }
41
- function qualify(input, definition, pointer) {
42
- const byName = new Map();
43
- for (const [key, field] of definition.properties) {
44
- const candidates = byName.get(field.name) ?? [];
45
- candidates.push(key);
46
- byName.set(field.name, candidates);
47
- }
48
- const result = {};
49
- const sourceByKey = new Map();
50
- for (const [inputKey, value] of Object.entries(input)) {
51
- const key = resolveInputKey(inputKey, definition, byName, `${pointer}/${escapeToken(inputKey)}`);
52
- const previous = sourceByKey.get(key);
53
- if (previous !== undefined) {
54
- fail('BUILDER_CORE_PROPERTY', `${pointer}/${escapeToken(inputKey)}`, `Property is supplied twice through ${JSON.stringify(previous)} and ${JSON.stringify(inputKey)}.`);
55
- }
56
- sourceByKey.set(key, inputKey);
57
- result[key] = value;
58
- }
59
- return createPropertyValues(Object.entries(result).map(([key, value]) => [admitPropertyKey(key), value]));
60
- }
61
- function resolveInputKey(input, definition, byName, pointer) {
62
- if (admitPropertyKey.is(input)) {
63
- const key = admitPropertyKey(input);
64
- if (!definition.properties.has(key)) {
65
- fail('BUILDER_CORE_PROPERTY', pointer, `Property ${input} is not present on the Core Class.`);
66
- }
67
- return key;
68
- }
69
- const candidates = byName.get(input) ?? [];
70
- if (candidates.length === 1)
71
- return candidates[0];
72
- if (candidates.length === 0) {
73
- fail('BUILDER_CORE_PROPERTY', pointer, `Property ${JSON.stringify(input)} does not exist.`);
74
- }
75
- fail('BUILDER_CORE_PROPERTY', pointer, `Property ${JSON.stringify(input)} is ambiguous; use one of: ${candidates.join(', ')}.`);
76
- }