@astrale-os/kernel-dsl 1.0.0-beta.7 → 1.0.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 (31) 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 +11 -1
  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/lifetime.d.ts +11 -0
  9. package/dist/v1/builder/lowering/schema/lifetime.js +55 -0
  10. package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
  11. package/dist/v1/compiled/admission/semantics/cardinality.d.ts +1 -1
  12. package/dist/v1/compiled/admission/semantics/cardinality.js +16 -1
  13. package/dist/v1/compiled/admission/semantics/class.js +23 -0
  14. package/dist/v1/compiled/admission/semantics/core.js +16 -0
  15. package/dist/v1/compiled/admission/wire/class.js +24 -4
  16. package/dist/v1/language/class/lifetime.d.ts +48 -11
  17. package/dist/v1/language/class/lifetime.js +86 -20
  18. package/dist/v1/language/class/model/class.d.ts +16 -1
  19. package/dist/v1/language/class/model/edge.d.ts +22 -6
  20. package/dist/v1/language/class/unique.d.ts +123 -13
  21. package/dist/v1/language/class/unique.js +238 -27
  22. package/dist/v1/language/index.d.ts +4 -4
  23. package/dist/v1/language/index.js +2 -2
  24. package/dist/v1/schema/diagnostic.d.ts +1 -1
  25. package/dist/v1/schema/normalization/domain.js +15 -4
  26. package/dist/v1/schema/validation/class.js +70 -1
  27. package/dist/v1/schema/validation/core.js +50 -0
  28. package/dist/v1/schema/validation/domain.structure.js +18 -4
  29. package/package.json +1 -1
  30. package/dist/v1/builder/lowering/core-properties.d.ts +0 -10
  31. package/dist/v1/builder/lowering/core-properties.js +0 -76
@@ -5,7 +5,7 @@ import type { MethodRecord } from '../../language/callable/model/callable.js';
5
5
  import type { ResolvedMethod } from '../../language/callable/resolved.js';
6
6
  import type { Inheritance } from '../../language/class/model/class.js';
7
7
  import type { MediaType } from '../../language/class/model/data.js';
8
- import type { EdgeCount, EdgeUniqueTuple } from '../../language/class/model/edge.js';
8
+ import type { EdgeCount, EdgeLifetime } from '../../language/class/model/edge.js';
9
9
  import type { ResolvedClass, ResolvedProperty } from '../../language/class/resolved.js';
10
10
  import type { ClassRuleChecks, ClassRuleObject } from '../../language/policy/model/policy.js';
11
11
  import type { StateTopology, StateTopologyOf } from '../../language/state/topology.js';
@@ -26,16 +26,22 @@ export interface DirectedSourceInput {
26
26
  readonly as: string;
27
27
  readonly accepts: readonly NodeClassReferenceInput[];
28
28
  readonly outgoing: EdgeCount;
29
+ /** What deleting a Node at this endpoint does to the Edge; derived when omitted. */
30
+ readonly onDelete?: EdgeLifetime;
29
31
  }
30
32
  export interface DirectedTargetInput {
31
33
  readonly as: string;
32
34
  readonly accepts: readonly NodeClassReferenceInput[];
33
35
  readonly incoming: EdgeCount;
36
+ /** What deleting a Node at this endpoint does to the Edge; derived when omitted. */
37
+ readonly onDelete?: EdgeLifetime;
34
38
  }
35
39
  export interface IncidentEndpointInput {
36
40
  readonly as: string;
37
41
  readonly accepts: readonly NodeClassReferenceInput[];
38
42
  readonly incident: EdgeCount;
43
+ /** What deleting a Node at this endpoint does to the Edge; derived when omitted. */
44
+ readonly onDelete?: EdgeLifetime;
39
45
  }
40
46
  type ExactEndpointInput<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never>;
41
47
  /** One observation slot: a local Policy, or its constant value. Absent means `true`. */
@@ -69,7 +75,17 @@ export interface NodeClassInput {
69
75
  readonly icon?: string;
70
76
  /** Local observation Policies; inherited by no descendant and forbidden on abstract Classes. */
71
77
  readonly policies?: NodeClassPoliciesInput;
78
+ /**
79
+ * The tuples no two Nodes of this Class and of its active concrete descendants share while every
80
+ * Property is present; omitted, none. A descendant is covered without declaring them again.
81
+ */
82
+ readonly unique?: readonly NodeUniqueTupleInput[];
72
83
  }
84
+ /**
85
+ * The distinct Properties of one Node key, in any order, each a Property of the Class, own or
86
+ * inherited, by its name or, when names collide, by its full key.
87
+ */
88
+ export type NodeUniqueTupleInput = readonly [string, ...string[]];
73
89
  export interface EdgeClassOptions {
74
90
  readonly abstract?: boolean;
75
91
  /** Which Classes may extend this one; omitted, those of its own Domain only (`'internal'`). */
@@ -84,15 +100,24 @@ export interface EdgeClassOptions {
84
100
  readonly acyclic?: true;
85
101
  };
86
102
  }
103
+ /**
104
+ * A key field as authored: an endpoint, the slug, or a Property of the Class, own or inherited, by
105
+ * its name or, when names collide, by its full key.
106
+ */
107
+ export type EdgeUniqueFieldInput = 'source' | 'target' | 'slug' | {
108
+ readonly property: string;
109
+ };
110
+ /** The distinct fields of one key, in any order. */
111
+ export type EdgeUniqueTupleInput = readonly [EdgeUniqueFieldInput, ...EdgeUniqueFieldInput[]];
87
112
  export interface DirectedEdgeInput extends EdgeClassOptions {
88
113
  readonly orientation: 'directed';
89
114
  readonly source: DirectedSourceInput;
90
115
  readonly target: DirectedTargetInput;
91
116
  /**
92
- * The tuples no two Edges of this Class and of its active concrete descendants share; omitted,
93
- * none. A descendant is covered without declaring them again.
117
+ * The tuples no two Edges of this Class and of its active concrete descendants share while every
118
+ * field is present; omitted, none. A descendant is covered without declaring them again.
94
119
  */
95
- readonly unique?: readonly EdgeUniqueTuple[];
120
+ readonly unique?: readonly EdgeUniqueTupleInput[];
96
121
  }
97
122
  export interface UndirectedEdgeInput extends Omit<EdgeClassOptions, 'constraints'> {
98
123
  readonly orientation: 'undirected';
@@ -25,13 +25,21 @@ export function nodeClass(config) {
25
25
  'methods',
26
26
  'icon',
27
27
  'policies',
28
+ 'unique',
28
29
  ]);
29
30
  if (admitted.policies !== undefined)
30
31
  assertKnownKeys(admitted.policies, ['read', 'traverse']);
31
32
  const value = authoringValue('definition', {
32
33
  kind: 'node',
33
34
  });
34
- classInputs.set(value, { ...admitted, abstract: admitted.abstract ?? false });
35
+ classInputs.set(value, {
36
+ ...admitted,
37
+ // Copied, so the declaration does not follow later changes to the author's arrays.
38
+ ...(admitted.unique === undefined
39
+ ? {}
40
+ : { unique: admitted.unique.map((tuple) => [...tuple]) }),
41
+ abstract: admitted.abstract ?? false,
42
+ });
35
43
  return value;
36
44
  }
37
45
  export const edgeClass = Object.freeze({
@@ -62,7 +70,9 @@ export const edgeClass = Object.freeze({
62
70
  // Copied, so the declaration does not follow later changes to the author's arrays.
63
71
  ...(unique === undefined
64
72
  ? {}
65
- : { unique: unique.map((tuple) => [...tuple]) }),
73
+ : {
74
+ unique: unique.map((tuple) => tuple.map((field) => typeof field === 'string' ? field : { property: field.property })),
75
+ }),
66
76
  ...options,
67
77
  abstract: options.abstract ?? false,
68
78
  });
@@ -104,13 +114,13 @@ export function classConfiguration(value) {
104
114
  return input;
105
115
  }
106
116
  function validateDirectedSourceInput(value) {
107
- assertKnownKeys(value, ['as', 'accepts', 'outgoing']);
117
+ assertKnownKeys(value, ['as', 'accepts', 'outgoing', 'onDelete']);
108
118
  }
109
119
  function validateDirectedTargetInput(value) {
110
- assertKnownKeys(value, ['as', 'accepts', 'incoming']);
120
+ assertKnownKeys(value, ['as', 'accepts', 'incoming', 'onDelete']);
111
121
  }
112
122
  function validateIncidentEndpointInput(value) {
113
- assertKnownKeys(value, ['as', 'accepts', 'incident']);
123
+ assertKnownKeys(value, ['as', 'accepts', 'incident', 'onDelete']);
114
124
  }
115
125
  function validateEdgeClassOptions(value) {
116
126
  assertKnownKeys(value, [
@@ -14,7 +14,7 @@ export { BuilderError as Error } from './schema/diagnostic.js';
14
14
  export type { BuilderDiagnostic as Diagnostic, BuilderDiagnosticCode as DiagnosticCode, } from './schema/diagnostic.js';
15
15
  export type { CallableInputBase, CallableInput, BinaryInputAuthoring, CallableOutputInput, CallableProjection, CallablePolicyBuilder, CallablePolicyContext, PolicyCheckContext, BinaryOutput, Function, FunctionInput, Method, MethodInput, PolicyObjectInput, StreamOutput, } from './callable/authoring.js';
16
16
  export type { CoreEdge, CoreEndpoint, CoreInput, CoreNode } from './core/authoring.js';
17
- export type { AuthoredPropertyProjection, AuthoredMethodProjection, Class, ClassIdentityProjection, ClassOwnerProjection, ClassReferenceProjection, ClassReferenceInput as ClassReference, ClassRuleBuilder, ClassRuleContext, DirectedEdgeInput, DirectedEdgeClass, DirectedSourceInput, DirectedTargetInput, EdgeClassCommon, EdgeClassInput, EdgeClass, EdgeClassReferenceInput as EdgeClassReference, IncidentEndpointInput, NodeClass, NodeClassInput, NodeClassPoliciesInput, NodeClassPolicyInput, NodeClassReferenceInput as NodeClassReference, UndirectedEdgeInput, UndirectedEdgeClass, } from './class/authoring.js';
17
+ export type { AuthoredPropertyProjection, AuthoredMethodProjection, Class, ClassIdentityProjection, ClassOwnerProjection, ClassReferenceProjection, ClassReferenceInput as ClassReference, ClassRuleBuilder, ClassRuleContext, DirectedEdgeInput, DirectedEdgeClass, DirectedSourceInput, DirectedTargetInput, EdgeClassCommon, EdgeClassInput, EdgeClass, EdgeClassReferenceInput as EdgeClassReference, EdgeUniqueFieldInput, EdgeUniqueTupleInput, IncidentEndpointInput, NodeClass, NodeClassInput, NodeClassPoliciesInput, NodeClassPolicyInput, NodeClassReferenceInput as NodeClassReference, NodeUniqueTupleInput, UndirectedEdgeInput, UndirectedEdgeClass, } from './class/authoring.js';
18
18
  export type { PathAuthoring } from './value/path.js';
19
19
  export type { Lazy } from './schema/lazy.js';
20
20
  export type { MatchPolicyInput, Policy, PolicyBuilder as PolicyFactory, PolicyEdgeInput, PolicyExistsContext, PolicyInput, PolicyNodeBinder, PolicyPatternContext, } from './policy/authoring.js';
@@ -39,6 +39,11 @@ export class V1DefinitionCompiler {
39
39
  ...(data === undefined ? {} : { data }),
40
40
  ...(input.icon === undefined ? {} : { icon: input.icon }),
41
41
  policies: this.classPolicies(input, 'node', `${pointer}/policies`),
42
+ // Property names stay as authored until `canonicalizeUniquePropertyKeys` qualifies them
43
+ // against the Class's effective Properties, once every Class is lowered.
44
+ ...(input.unique === undefined
45
+ ? {}
46
+ : { unique: input.unique }),
42
47
  };
43
48
  }
44
49
  edgeClass(value, name, pointer) {
@@ -54,7 +59,11 @@ export class V1DefinitionCompiler {
54
59
  target: this.directedTarget(directed.target, `${pointer}/endpoints/target`),
55
60
  },
56
61
  constraints: directedConstraints(directed),
57
- ...(directed.unique === undefined ? {} : { unique: directed.unique }),
62
+ // Property names stay as authored until `canonicalizeUniquePropertyKeys` qualifies them
63
+ // against the Class's effective Properties, once every Class is lowered.
64
+ ...(directed.unique === undefined
65
+ ? {}
66
+ : { unique: directed.unique }),
58
67
  };
59
68
  }
60
69
  const undirected = classConfiguration(value);
@@ -129,6 +138,7 @@ export class V1DefinitionCompiler {
129
138
  accepts: value.accepts
130
139
  .map((entry, index) => this.context.resolveNodeClass(entry, `${pointer}/accepts/${index}`))
131
140
  .sort(compareDefinitionRefs),
141
+ ...(value.onDelete === undefined ? {} : { onDelete: value.onDelete }),
132
142
  };
133
143
  }
134
144
  directedSource(value, pointer) {
@@ -0,0 +1,27 @@
1
+ import type { ClassDefinition } from '../../language/class/model/class.js';
2
+ import type { CoreContent } from '../../language/core/model/core.js';
3
+ import type { DefinitionAnalysis } from '../../schema/analysis/class.js';
4
+ import type { DomainSchemaLookup } from '../../schema/dependency/lookup.js';
5
+ import type { DomainSchemaIR } from '../../schema/model/schema.js';
6
+ import type { UnqualifiedCoreContent } from './core.js';
7
+ type UnqualifiedDomainSchema = Omit<DomainSchemaIR, 'core'> & {
8
+ readonly core: UnqualifiedCoreContent;
9
+ };
10
+ /** The effective Properties of every Class `candidate` resolves, by `definitionRefIndex`. */
11
+ export type PropertyOwners = ReadonlyMap<string, DefinitionAnalysis>;
12
+ /**
13
+ * Analyze the already-canonical Definition surface of `candidate` once, so Property shorthand
14
+ * resolves against each Class's own and inherited Properties. Core values and key tuples take no
15
+ * part in Class composition, so they are left out of the analysis.
16
+ */
17
+ export declare function analyzePropertyOwners(candidate: UnqualifiedDomainSchema, lookup: DomainSchemaLookup): PropertyOwners;
18
+ /** Lower rich builder shorthand to the canonical full-key Core representation. */
19
+ export declare function canonicalizeCorePropertyKeys(core: UnqualifiedCoreContent, owners: PropertyOwners): CoreContent;
20
+ /**
21
+ * Lower each Property a key tuple names by its name to its full key among the Class's own and
22
+ * inherited Properties; a full key stays as authored. Ambiguity and absence are refused here, so a
23
+ * tuple never names a Property the author did not mean. A Node tuple names Properties only; an Edge
24
+ * tuple names them as `{ property }` beside its structural fields.
25
+ */
26
+ export declare function canonicalizeUniquePropertyKeys(classes: Readonly<Record<string, ClassDefinition>>, origin: string, owners: PropertyOwners): Readonly<Record<string, ClassDefinition>>;
27
+ export {};
@@ -0,0 +1,108 @@
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
+ /**
8
+ * Analyze the already-canonical Definition surface of `candidate` once, so Property shorthand
9
+ * resolves against each Class's own and inherited Properties. Core values and key tuples take no
10
+ * part in Class composition, so they are left out of the analysis.
11
+ */
12
+ export function analyzePropertyOwners(candidate, lookup) {
13
+ const analysisCandidate = {
14
+ ...candidate,
15
+ core: { nodes: {}, edges: [] },
16
+ };
17
+ return analyzeDefinitions(buildDomainResolutionContext(analysisCandidate, lookup)).definitions;
18
+ }
19
+ /** Lower rich builder shorthand to the canonical full-key Core representation. */
20
+ export function canonicalizeCorePropertyKeys(core, owners) {
21
+ const nodes = Object.fromEntries(Object.entries(core.nodes).map(([slug, node]) => {
22
+ const definition = owners.get(definitionRefIndex(node.class));
23
+ if (definition === undefined) {
24
+ fail('BUILDER_CORE_PROPERTY', `/core/nodes/${escapeToken(slug)}/class`, 'Core property shorthand requires a resolvable concrete Class.');
25
+ }
26
+ return [
27
+ slug,
28
+ {
29
+ ...node,
30
+ properties: qualify(node.properties, definition, `/core/nodes/${escapeToken(slug)}/properties`),
31
+ },
32
+ ];
33
+ }));
34
+ const edges = core.edges.map((edge, index) => {
35
+ const definition = owners.get(definitionRefIndex(edge.class));
36
+ if (definition === undefined) {
37
+ fail('BUILDER_CORE_PROPERTY', `/core/edges/${index}/class`, 'Core property shorthand requires a resolvable concrete Class.');
38
+ }
39
+ return {
40
+ ...edge,
41
+ properties: qualify(edge.properties, definition, `/core/edges/${index}/properties`),
42
+ };
43
+ });
44
+ return { nodes, edges };
45
+ }
46
+ /**
47
+ * Lower each Property a key tuple names by its name to its full key among the Class's own and
48
+ * inherited Properties; a full key stays as authored. Ambiguity and absence are refused here, so a
49
+ * tuple never names a Property the author did not mean. A Node tuple names Properties only; an Edge
50
+ * tuple names them as `{ property }` beside its structural fields.
51
+ */
52
+ export function canonicalizeUniquePropertyKeys(classes, origin, owners) {
53
+ return Object.fromEntries(Object.entries(classes).map(([name, declaration]) => {
54
+ if (declaration.kind === 'edge' && declaration.orientation !== 'directed') {
55
+ return [name, declaration];
56
+ }
57
+ if (declaration.unique === undefined)
58
+ return [name, declaration];
59
+ const ref = { origin, kind: 'class', name };
60
+ const definition = owners.get(definitionRefIndex(ref));
61
+ const pointer = `/classes/${escapeToken(name)}/unique`;
62
+ const resolve = (input, at) => {
63
+ if (definition === undefined) {
64
+ fail('BUILDER_UNIQUE_PROPERTY', at, 'A key Property requires a resolvable Class.');
65
+ }
66
+ return resolveInputKey(input, definition, 'BUILDER_UNIQUE_PROPERTY', at);
67
+ };
68
+ if (declaration.kind === 'node') {
69
+ const unique = declaration.unique.map((tuple, t) => tuple.map((key, f) => resolve(key, `${pointer}/${t}/${f}`)));
70
+ return [name, { ...declaration, unique }];
71
+ }
72
+ const unique = declaration.unique.map((tuple, t) => tuple.map((field, f) => typeof field === 'string'
73
+ ? field
74
+ : { property: resolve(field.property, `${pointer}/${t}/${f}/property`) }));
75
+ return [name, { ...declaration, unique }];
76
+ }));
77
+ }
78
+ function qualify(input, definition, pointer) {
79
+ const result = {};
80
+ const sourceByKey = new Map();
81
+ for (const [inputKey, value] of Object.entries(input)) {
82
+ const key = resolveInputKey(inputKey, definition, 'BUILDER_CORE_PROPERTY', `${pointer}/${escapeToken(inputKey)}`);
83
+ const previous = sourceByKey.get(key);
84
+ if (previous !== undefined) {
85
+ fail('BUILDER_CORE_PROPERTY', `${pointer}/${escapeToken(inputKey)}`, `Property is supplied twice through ${JSON.stringify(previous)} and ${JSON.stringify(inputKey)}.`);
86
+ }
87
+ sourceByKey.set(key, inputKey);
88
+ result[key] = value;
89
+ }
90
+ return createPropertyValues(Object.entries(result).map(([key, value]) => [admitPropertyKey(key), value]));
91
+ }
92
+ /** The full key of the Property `input` names on `definition`: its full key, or its unique name. */
93
+ function resolveInputKey(input, definition, code, pointer) {
94
+ if (admitPropertyKey.is(input)) {
95
+ const key = admitPropertyKey(input);
96
+ if (!definition.properties.has(key)) {
97
+ fail(code, pointer, `Property ${input} is not present on the Class.`);
98
+ }
99
+ return key;
100
+ }
101
+ const candidates = [...definition.properties].flatMap(([key, field]) => field.name === input ? [key] : []);
102
+ if (candidates.length === 1)
103
+ return candidates[0];
104
+ if (candidates.length === 0) {
105
+ fail(code, pointer, `Property ${JSON.stringify(input)} does not exist.`);
106
+ }
107
+ fail(code, pointer, `Property ${JSON.stringify(input)} is ambiguous; use one of: ${candidates.join(', ')}.`);
108
+ }
@@ -7,13 +7,14 @@ import { parseDomainSchemaStructure } from '../../../schema/validation/domain.st
7
7
  import { assertKnownKeys, escapeToken } from '../../schema/diagnostic.js';
8
8
  import { V1CallableCompiler } from '../callable.js';
9
9
  import { V1DefinitionCompiler } from '../class.js';
10
- import { canonicalizeCorePropertyKeys } from '../core-properties.js';
11
10
  import { compileCore } from '../core.js';
12
11
  import { assertPolicyCompositionAcyclic, V1PolicyCompiler } from '../policy.js';
12
+ import { analyzePropertyOwners, canonicalizeCorePropertyKeys, canonicalizeUniquePropertyKeys, } from '../property-keys.js';
13
13
  import { BuilderValueSchemas } from '../value/compile.js';
14
14
  import { V1ViewCompiler } from '../view.js';
15
15
  import { captureSchema, commitSchemaOwnership } from './capture.js';
16
16
  import { BuilderContext } from './context.js';
17
+ import { assertAuthoredLifetimes } from './lifetime.js';
17
18
  /**
18
19
  * Compile the V1 TypeScript builder graph through the same semantic acceptance
19
20
  * path used for untrusted decoded schemas.
@@ -21,9 +22,11 @@ import { BuilderContext } from './context.js';
21
22
  * This is the private implementation behind defineSchema.
22
23
  */
23
24
  export function compileSchema(origin, input) {
24
- const lookup = new MemoryDomainSchemaLookup(lookupEntriesForDependencies(input.dependencies ?? {}));
25
+ const dependencies = lookupEntriesForDependencies(input.dependencies ?? {});
26
+ const lookup = new MemoryDomainSchemaLookup(dependencies);
25
27
  const candidate = assembleDomainSchema(origin, input, lookup);
26
28
  const accepted = acceptDomainSchema(candidate, lookup);
29
+ assertAuthoredLifetimes(accepted, dependencies);
27
30
  commitSchemaOwnership(input, origin);
28
31
  return accepted;
29
32
  }
@@ -75,9 +78,11 @@ function assembleDomainSchema(origin, input, lookup) {
75
78
  views,
76
79
  core,
77
80
  };
81
+ const owners = analyzePropertyOwners(semanticContent, lookup);
78
82
  const qualifiedContent = {
79
83
  ...semanticContent,
80
- core: canonicalizeCorePropertyKeys(semanticContent, lookup),
84
+ classes: canonicalizeUniquePropertyKeys(classes, origin, owners),
85
+ core: canonicalizeCorePropertyKeys(core, owners),
81
86
  };
82
87
  context.noteDomainEndpointReferences(qualifiedContent);
83
88
  const candidate = {
@@ -0,0 +1,11 @@
1
+ import type { DomainSchemaEntry } from '../../../schema/dependency/lookup.js';
2
+ import type { DomainSchema } from '../../../schema/model/schema.js';
3
+ /**
4
+ * Authoring refuses two Edge Classes that acceptance keeps, because a stored Schema must decode
5
+ * again with the lifetimes it derives (BUILDER-EDGE-LIFETIMES):
6
+ * - one with an endpoint that can hold a Node of another Domain while an endpoint requires an Edge.
7
+ * It can neither restrict that Domain's deletions nor detach below the required count, so the
8
+ * required endpoint must become optional;
9
+ * - one whose lifetimes differ from those of an Edge Class it extends, the root Edge aside.
10
+ */
11
+ export declare function assertAuthoredLifetimes(schema: DomainSchema, dependencies: readonly DomainSchemaEntry[]): void;
@@ -0,0 +1,55 @@
1
+ import { Key } from '../../../addressing/key.js';
2
+ import { definitionRefIndex } from '../../../addressing/ordering.js';
3
+ import { isGraphRoot } from '../../../language/class/inheritance.js';
4
+ import { contractEndpoints, edgeLifetimes, lifetimeScope, reachesAnotherDomain, requiredEndpoint, } from '../../../language/class/lifetime.js';
5
+ import { BuilderError, diagnostic, escapeToken } from '../../schema/diagnostic.js';
6
+ /**
7
+ * Authoring refuses two Edge Classes that acceptance keeps, because a stored Schema must decode
8
+ * again with the lifetimes it derives (BUILDER-EDGE-LIFETIMES):
9
+ * - one with an endpoint that can hold a Node of another Domain while an endpoint requires an Edge.
10
+ * It can neither restrict that Domain's deletions nor detach below the required count, so the
11
+ * required endpoint must become optional;
12
+ * - one whose lifetimes differ from those of an Edge Class it extends, the root Edge aside.
13
+ */
14
+ export function assertAuthoredLifetimes(schema, dependencies) {
15
+ const classes = [
16
+ ...Object.values(schema.classes),
17
+ ...dependencies.flatMap((entry) => Object.values(entry.schema.classes)),
18
+ ];
19
+ const scope = lifetimeScope(classes);
20
+ const declarations = new Map(classes.map((declaration) => [definitionRefIndex(declaration.ref), declaration]));
21
+ const diagnostics = Object.entries(schema.classes).flatMap(([name, declaration]) => {
22
+ if (declaration.kind !== 'edge')
23
+ return [];
24
+ const pointer = `/classes/${escapeToken(name)}`;
25
+ const endpoints = contractEndpoints(declaration);
26
+ const keys = declaration.orientation === 'directed' ? ['source', 'target'] : ['0', '1'];
27
+ const counts = declaration.orientation === 'directed' ? ['outgoing', 'incoming'] : ['incident', 'incident'];
28
+ const found = [];
29
+ const required = requiredEndpoint(declaration);
30
+ const reaching = endpoints.find(({ endpoint }) => reachesAnotherDomain(schema.origin, endpoint, scope));
31
+ if (required !== undefined && reaching !== undefined)
32
+ found.push(diagnostic('BUILDER_EDGE_LIFETIME', `${pointer}/endpoints/${keys[required.index]}/${counts[required.index]}`, `${Key.of(declaration.ref)} endpoint ${reaching.endpoint.role} can hold Nodes of another ` +
33
+ `Domain, so it cannot restrict their deletion, and endpoint ${required.endpoint.role} ` +
34
+ `requires ${required.count} Edges, so it cannot detach: make ${required.endpoint.role} optional.`));
35
+ const lifetimes = edgeLifetimes(declaration, scope);
36
+ declaration.extends.forEach((ref, index) => {
37
+ const parent = declarations.get(definitionRefIndex(ref));
38
+ if (parent?.kind !== 'edge' || isGraphRoot(parent))
39
+ return;
40
+ const inherited = edgeLifetimes(parent, scope);
41
+ const same = (order) => order.every((at, position) => lifetimes[position] === inherited[at]);
42
+ if (same([0, 1]) || (declaration.orientation === 'undirected' && same([1, 0])))
43
+ return;
44
+ found.push(diagnostic('BUILDER_EDGE_LIFETIME', `${pointer}/extends/${index}`, `${Key.of(declaration.ref)} has lifetimes ${spell(endpoints, lifetimes)}, and ` +
45
+ `${Key.of(parent.ref)} it extends has ${spell(contractEndpoints(parent), inherited)}: ` +
46
+ `an Edge Class keeps the lifetimes of the Classes it extends.`));
47
+ });
48
+ return found;
49
+ });
50
+ if (diagnostics.length > 0)
51
+ throw new BuilderError(diagnostics);
52
+ }
53
+ function spell(endpoints, lifetimes) {
54
+ return `${endpoints[0].endpoint.role} ${lifetimes[0]} and ${endpoints[1].endpoint.role} ${lifetimes[1]}`;
55
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DiagnosticCode as LanguageDiagnosticCode } from '../../language/validation/diagnostic.js';
2
- export type BuilderDiagnosticCode = LanguageDiagnosticCode | 'BUILDER_ABSTRACT_POLICY' | 'BUILDER_AMBIGUOUS_OWNERSHIP' | 'BUILDER_AUTO_EVENT_INVALID' | 'BUILDER_AUTO_IMMUTABLE' | 'BUILDER_AUTO_NESTED' | 'BUILDER_AUTO_SET_INVALID' | 'BUILDER_CALLABLE_AUTH' | 'BUILDER_CALLABLE_PLACEMENT' | 'BUILDER_CALLABLE_POLICY_AUTH' | 'BUILDER_CORE_CLASS' | 'BUILDER_CORE_EDGE' | 'BUILDER_CORE_NODE' | 'BUILDER_CORE_PROPERTY' | 'BUILDER_CORE_REF' | 'BUILDER_DEFINITION' | 'BUILDER_DEPENDENCY_CONFLICT' | 'BUILDER_DEPENDENCY_ALIAS' | 'BUILDER_DEPENDENCY_DIRECT' | 'BUILDER_DUPLICATE_MEMBER' | 'BUILDER_FOREIGN_POLICY' | 'BUILDER_FULL_MATCH_INVALID' | 'BUILDER_FUNCTION' | 'BUILDER_METHOD' | 'BUILDER_PATH_DUPLICATE' | 'BUILDER_PATH_EMPTY' | 'BUILDER_POLICY' | 'BUILDER_POLICY_COMPOSITION' | 'BUILDER_POLICY_CYCLE' | 'BUILDER_POLICY_EMPTY_COMPOSITION' | 'BUILDER_POLICY_EMPTY_EXISTS' | 'BUILDER_POLICY_INPUT' | 'BUILDER_POLICY_REF' | 'BUILDER_POLICY_SELF' | 'BUILDER_PROPERTY_IMMUTABLE_INVALID' | 'BUILDER_REFERENCE_CYCLE' | 'BUILDER_REFERENCE_KIND' | 'BUILDER_REFERENCE_MISSING' | 'BUILDER_UNKNOWN_FIELD' | 'BUILDER_UNOWNED_REFERENCE' | 'BUILDER_VIEW' | 'ZOD_SCHEMA_NOT_LOSSLESS';
2
+ export type BuilderDiagnosticCode = LanguageDiagnosticCode | 'BUILDER_ABSTRACT_POLICY' | 'BUILDER_AMBIGUOUS_OWNERSHIP' | 'BUILDER_AUTO_EVENT_INVALID' | 'BUILDER_AUTO_IMMUTABLE' | 'BUILDER_AUTO_NESTED' | 'BUILDER_AUTO_SET_INVALID' | 'BUILDER_CALLABLE_AUTH' | 'BUILDER_CALLABLE_PLACEMENT' | 'BUILDER_CALLABLE_POLICY_AUTH' | 'BUILDER_CORE_CLASS' | 'BUILDER_CORE_EDGE' | 'BUILDER_CORE_NODE' | 'BUILDER_CORE_PROPERTY' | 'BUILDER_CORE_REF' | 'BUILDER_DEFINITION' | 'BUILDER_DEPENDENCY_CONFLICT' | 'BUILDER_DEPENDENCY_ALIAS' | 'BUILDER_DEPENDENCY_DIRECT' | 'BUILDER_DUPLICATE_MEMBER' | 'BUILDER_EDGE_LIFETIME' | 'BUILDER_FOREIGN_POLICY' | 'BUILDER_FULL_MATCH_INVALID' | 'BUILDER_FUNCTION' | 'BUILDER_METHOD' | 'BUILDER_PATH_DUPLICATE' | 'BUILDER_PATH_EMPTY' | 'BUILDER_POLICY' | 'BUILDER_POLICY_COMPOSITION' | 'BUILDER_POLICY_CYCLE' | 'BUILDER_POLICY_EMPTY_COMPOSITION' | 'BUILDER_POLICY_EMPTY_EXISTS' | 'BUILDER_POLICY_INPUT' | 'BUILDER_POLICY_REF' | 'BUILDER_POLICY_SELF' | 'BUILDER_PROPERTY_IMMUTABLE_INVALID' | 'BUILDER_REFERENCE_CYCLE' | 'BUILDER_REFERENCE_KIND' | 'BUILDER_REFERENCE_MISSING' | 'BUILDER_UNIQUE_PROPERTY' | 'BUILDER_UNKNOWN_FIELD' | 'BUILDER_UNOWNED_REFERENCE' | 'BUILDER_VIEW' | 'ZOD_SCHEMA_NOT_LOSSLESS';
3
3
  export interface BuilderDiagnostic<out Code extends BuilderDiagnosticCode = BuilderDiagnosticCode> {
4
4
  readonly code: Code;
5
5
  readonly pointer: string;
@@ -1,5 +1,5 @@
1
1
  import { type CompiledSchemaEntry } from '../../model/schema.js';
2
2
  import { type SemanticIndex } from './context.js';
3
3
  import { type CoreNode, type CoreEdgeValue } from './core.js';
4
- /** Check normalized Edge uniqueness, noSelf, cycles and complete Core degrees. */
4
+ /** Check normalized Edge identity, keys, noSelf, cycles and complete Core degrees. */
5
5
  export declare function validateCoreEdgeSet(context: SemanticIndex, entry: CompiledSchemaEntry, entryIndex: number, nodes: ReadonlyMap<string, CoreNode>, edges: readonly CoreEdgeValue[]): void;
@@ -1,12 +1,13 @@
1
1
  import { Key } from '../../../addressing/index.js';
2
2
  import { compareUnicode } from '../../../addressing/ordering.js';
3
3
  import {} from '../../../language/class/model/edge.js';
4
+ import { coveringEdgeUniqueTuples, edgeUniqueKeys } from '../../../language/class/unique.js';
4
5
  import {} from '../../model/schema.js';
5
6
  import { fail } from '../diagnostic.js';
6
7
  import { isCompiledEdge, classSatisfiesAny, } from './context.js';
7
8
  import {} from './core.js';
8
9
  import { sameIncidentEndpoint, countBounds } from './edge.js';
9
- /** Check normalized Edge uniqueness, noSelf, cycles and complete Core degrees. */
10
+ /** Check normalized Edge identity, keys, noSelf, cycles and complete Core degrees. */
10
11
  export function validateCoreEdgeSet(context, entry, entryIndex, nodes, edges) {
11
12
  const seen = new Set();
12
13
  const slices = new Map();
@@ -26,6 +27,20 @@ export function validateCoreEdgeSet(context, entry, entryIndex, nodes, edges) {
26
27
  if (edge.class.value.constraints.noSelf === true && edge.source.key === edge.target.key)
27
28
  fail(edge.pointer, 'Core Edge violates noSelf.');
28
29
  }
30
+ // No two Core Edges hold one key under a tuple covering their Classes (DV-EDGE-UNIQUE).
31
+ const resolve = (ref) => context.classes.get(Key.of(ref))?.value;
32
+ const holders = new Set();
33
+ for (const edge of edges) {
34
+ const keys = edgeUniqueKeys(coveringEdgeUniqueTuples(edge.class.value, resolve), {
35
+ source: edge.source.key,
36
+ target: edge.target.key,
37
+ props: edge.value.properties,
38
+ });
39
+ if (keys.some((key) => holders.has(key)))
40
+ fail(edge.pointer, 'Core Edge holds a key another Core Edge holds.');
41
+ for (const key of keys)
42
+ holders.add(key);
43
+ }
29
44
  for (const edgeClass of entry.definitions.classes) {
30
45
  if (!isCompiledEdge(edgeClass))
31
46
  continue;
@@ -2,6 +2,7 @@ import { json } from '@astrale-os/kernel-dsl/value';
2
2
  import { Key, PropertyKey as PropertyKeyValue, } from '../../../addressing/index.js';
3
3
  import { composeMethods } from '../../../language/callable/composition.js';
4
4
  import { isMethodExecutable } from '../../../language/callable/executable.js';
5
+ import { keyableProperty } from '../../../language/class/unique.js';
5
6
  import { bindValueProgram } from '../../../language/value/compile/load.js';
6
7
  import {} from '../../model/schema.js';
7
8
  import { fail } from '../diagnostic.js';
@@ -116,6 +117,28 @@ export function validateClassFacets(context) {
116
117
  }
117
118
  }
118
119
  }
120
+ if (located.value.unique !== undefined) {
121
+ const properties = context.analyses.get(Key.of(located.value.ref))?.properties;
122
+ const keyable = (key, at) => {
123
+ const property = properties?.get(key)?.declaration;
124
+ if (property === undefined || !keyableProperty(property.ordering, property.auto)) {
125
+ fail(at, 'A key Property is a keyable Property of the Class.');
126
+ }
127
+ };
128
+ // A Node tuple names Properties only; an Edge tuple names them as `{ property }`.
129
+ const value = located.value;
130
+ if (value.kind === 'node') {
131
+ for (const [t, tuple] of (value.unique ?? []).entries())
132
+ for (const [f, key] of tuple.entries())
133
+ keyable(key, `${located.pointer}/unique/${t}/${f}`);
134
+ }
135
+ else {
136
+ for (const [t, tuple] of (value.unique ?? []).entries())
137
+ for (const [f, field] of tuple.entries())
138
+ if (typeof field !== 'string')
139
+ keyable(field.property, `${located.pointer}/unique/${t}/${f}/property`);
140
+ }
141
+ }
119
142
  if (located.value.kind === 'node') {
120
143
  for (const [index, method] of located.value.methods.entries()) {
121
144
  if (isMethodExecutable(located.value, method))
@@ -1,5 +1,6 @@
1
1
  import { Key } from '../../../addressing/index.js';
2
2
  import { isMethodExecutable } from '../../../language/callable/executable.js';
3
+ import { coveringNodeUniqueTuples, nodeUniqueKeys } from '../../../language/class/unique.js';
3
4
  import {} from '../../../language/core/model/core.js';
4
5
  import { policyObjectOrigin, policyObjectKey, projectedMemberClass, } from '../../../language/index.js';
5
6
  import { bindValueProgram } from '../../../language/value/compile/load.js';
@@ -31,6 +32,7 @@ export function validateCore(candidate, context) {
31
32
  pointer: path,
32
33
  });
33
34
  }
35
+ validateCoreNodeKeys(context, nodes);
34
36
  const edges = [];
35
37
  for (const [index, value] of entry.facts.core.entries()) {
36
38
  const path = `/closure/${entryIndex}/facts/core/${index}`;
@@ -67,6 +69,20 @@ export function validateCore(candidate, context) {
67
69
  validateCoreEdgeSet(context, entry, entryIndex, nodes, edges);
68
70
  }
69
71
  }
72
+ /** No two Core Nodes hold one key under a tuple covering their Classes (DV-NODE-UNIQUE). */
73
+ function validateCoreNodeKeys(context, nodes) {
74
+ const resolve = (ref) => context.classes.get(Key.of(ref))?.value;
75
+ const holders = new Set();
76
+ for (const node of nodes.values()) {
77
+ const keys = nodeUniqueKeys(coveringNodeUniqueTuples(node.class.value, resolve), {
78
+ props: node.value.properties,
79
+ });
80
+ if (keys.some((key) => holders.has(key)))
81
+ fail(node.pointer, 'Core Node holds a key another Core Node holds.');
82
+ for (const key of keys)
83
+ holders.add(key);
84
+ }
85
+ }
70
86
  function validateCoreProperties(values, analysis, path) {
71
87
  for (const key of Object.keys(values))
72
88
  if (!analysis.properties.has(key))
@@ -1,7 +1,7 @@
1
1
  import {} from '@astrale-os/kernel-dsl/value';
2
2
  import { Key } from '../../../addressing/index.js';
3
3
  import { MEDIA_TYPE_PATTERN } from '../../../language/class/model/data.js';
4
- import { compareEdgeUniqueTuples, edgeUniqueTuple } from '../../../language/class/unique.js';
4
+ import { compareEdgeUniqueTuples, compareNodeUniqueTuples, edgeUniqueTuple, nodeUniqueTuple, } from '../../../language/class/unique.js';
5
5
  import {} from '../../model/schema.js';
6
6
  import { invalid } from '../diagnostic.js';
7
7
  import { compiledCallable } from './callable.js';
@@ -27,7 +27,7 @@ export function compiledClass(input, values, path) {
27
27
  'properties',
28
28
  'policies',
29
29
  ], 'Compiled Class', path, value.kind === 'node'
30
- ? ['data', 'description', 'icon', 'inheritance']
30
+ ? ['data', 'description', 'icon', 'inheritance', 'unique']
31
31
  : ['description', 'inheritance', 'unique']);
32
32
  definitionRef(value.ref, 'class', 'Class ref', `${path}/ref`);
33
33
  boolean(value.abstract, 'Class abstract', `${path}/abstract`);
@@ -47,6 +47,8 @@ export function compiledClass(input, values, path) {
47
47
  dataDefinition(value.data, `${path}/data`);
48
48
  boundedString(value.icon, 1, 8000, 'Class icon', `${path}/icon`);
49
49
  classPolicies(value.policies, 'node', `${path}/policies`);
50
+ if (value.unique !== undefined)
51
+ nodeUniqueTuples(value.unique, `${path}/unique`);
50
52
  const methodsPath = `${path}/methods`;
51
53
  const methods = array(value.methods, 'Class methods', methodsPath);
52
54
  for (const [index, method] of methods.entries())
@@ -120,7 +122,7 @@ function edgeContract(value, path) {
120
122
  invalid('Edge endpoint roles', undefined, endpointsPath);
121
123
  }
122
124
  }
123
- /** A nonempty list of tuples of `EDGE_UNIQUE_TUPLES`, distinct and in their canonical order. */
125
+ /** A nonempty list of canonical tuples (`edgeUniqueTuple`), distinct and in canonical order. */
124
126
  function uniqueTuples(input, path) {
125
127
  const tuples = array(input, 'Edge unique tuples', path);
126
128
  if (tuples.length === 0)
@@ -136,9 +138,25 @@ function uniqueTuples(input, path) {
136
138
  invalid('Edge unique tuples', undefined, `${path}/${index}`);
137
139
  }
138
140
  }
141
+ /** A nonempty list of canonical Node tuples (`nodeUniqueTuple`), distinct and in canonical order. */
142
+ function nodeUniqueTuples(input, path) {
143
+ const tuples = array(input, 'Node unique tuples', path);
144
+ if (tuples.length === 0)
145
+ invalid('Node unique tuples', undefined, path);
146
+ const known = tuples.map((tuple, index) => {
147
+ const keys = nodeUniqueTuple(array(tuple, 'Node unique tuple', `${path}/${index}`));
148
+ if (keys === undefined)
149
+ invalid('Node unique tuple', undefined, `${path}/${index}`);
150
+ return keys;
151
+ });
152
+ for (const [index, tuple] of known.entries()) {
153
+ if (index > 0 && compareNodeUniqueTuples(known[index - 1], tuple) >= 0)
154
+ invalid('Node unique tuples', undefined, `${path}/${index}`);
155
+ }
156
+ }
139
157
  function edgeEndpoint(input, count, label, path) {
140
158
  const value = object(input, label, path);
141
- exact(value, ['role', 'accepts', count], label, path);
159
+ exact(value, ['role', 'accepts', count], label, path, ['onDelete']);
142
160
  memberName(value.role, `${label} role`, `${path}/role`);
143
161
  const acceptsPath = `${path}/accepts`;
144
162
  const accepts = array(value.accepts, `${label} accepts`, acceptsPath).map((item, index) => definitionRef(item, 'class', `${label} accepted Class`, `${acceptsPath}/${index}`));
@@ -151,6 +169,8 @@ function edgeEndpoint(input, count, label, path) {
151
169
  value[count] !== '1') {
152
170
  invalid(`${label} ${count}`, undefined, `${path}/${count}`);
153
171
  }
172
+ if (value.onDelete !== undefined && value.onDelete !== 'restrict' && value.onDelete !== 'detach')
173
+ invalid(`${label} onDelete`, undefined, `${path}/onDelete`);
154
174
  }
155
175
  function dataDefinition(input, path) {
156
176
  const value = object(input, 'Class data', path);