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

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 (30) hide show
  1. package/dist/v1/builder/class/authoring.d.ts +11 -1
  2. package/dist/v1/builder/class/authoring.js +11 -1
  3. package/dist/v1/builder/lowering/class.js +7 -0
  4. package/dist/v1/compiled/admission/semantics/class.js +2 -0
  5. package/dist/v1/compiled/admission/wire/class.js +29 -2
  6. package/dist/v1/index.d.ts +1 -1
  7. package/dist/v1/language/class/inheritance.d.ts +9 -5
  8. package/dist/v1/language/class/inheritance.js +4 -0
  9. package/dist/v1/language/class/model/class.d.ts +9 -0
  10. package/dist/v1/language/class/model/edge.d.ts +13 -0
  11. package/dist/v1/language/class/unique.d.ts +24 -0
  12. package/dist/v1/language/class/unique.js +46 -0
  13. package/dist/v1/language/index.d.ts +3 -0
  14. package/dist/v1/language/index.js +2 -0
  15. package/dist/v1/language/value/profile/canonicalize.js +6 -5
  16. package/dist/v1/schema/compatibility/comparators/classes.d.ts +3 -0
  17. package/dist/v1/schema/compatibility/comparators/classes.js +4 -0
  18. package/dist/v1/schema/compatibility/comparators/index.d.ts +1 -1
  19. package/dist/v1/schema/compatibility/fields.d.ts +9 -1
  20. package/dist/v1/schema/compatibility/fields.js +6 -0
  21. package/dist/v1/schema/compatibility/model.d.ts +4 -3
  22. package/dist/v1/schema/compatibility/structure/index.js +6 -4
  23. package/dist/v1/schema/compatibility/structure/requirements.js +29 -1
  24. package/dist/v1/schema/compatibility/versioning/surface.d.ts +1 -1
  25. package/dist/v1/schema/compatibility/versioning/surface.js +25 -2
  26. package/dist/v1/schema/diagnostic.d.ts +1 -1
  27. package/dist/v1/schema/normalization/domain.js +5 -0
  28. package/dist/v1/schema/validation/class.js +3 -0
  29. package/dist/v1/schema/validation/domain.structure.js +24 -8
  30. package/package.json +1 -1
@@ -3,8 +3,9 @@ import type { z } from 'zod';
3
3
  import type { ClassRef } from '../../addressing/index.js';
4
4
  import type { MethodRecord } from '../../language/callable/model/callable.js';
5
5
  import type { ResolvedMethod } from '../../language/callable/resolved.js';
6
+ import type { Inheritance } from '../../language/class/model/class.js';
6
7
  import type { MediaType } from '../../language/class/model/data.js';
7
- import type { EdgeCount } from '../../language/class/model/edge.js';
8
+ import type { EdgeCount, EdgeUniqueTuple } from '../../language/class/model/edge.js';
8
9
  import type { ResolvedClass, ResolvedProperty } from '../../language/class/resolved.js';
9
10
  import type { ClassRuleChecks, ClassRuleObject } from '../../language/policy/model/policy.js';
10
11
  import type { StateTopology, StateTopologyOf } from '../../language/state/topology.js';
@@ -58,6 +59,8 @@ export interface NodeClassPoliciesInput {
58
59
  }
59
60
  export interface NodeClassInput {
60
61
  readonly abstract?: boolean;
62
+ /** Which Classes may extend this one; omitted, those of its own Domain only (`'internal'`). */
63
+ readonly inheritance?: Inheritance;
61
64
  readonly description?: string;
62
65
  readonly extends?: readonly NodeClassReferenceInput[];
63
66
  readonly properties?: PropertyRecordInput;
@@ -69,6 +72,8 @@ export interface NodeClassInput {
69
72
  }
70
73
  export interface EdgeClassOptions {
71
74
  readonly abstract?: boolean;
75
+ /** Which Classes may extend this one; omitted, those of its own Domain only (`'internal'`). */
76
+ readonly inheritance?: Inheritance;
72
77
  readonly description?: string;
73
78
  readonly extends?: readonly EdgeClassReferenceInput[];
74
79
  readonly properties?: PropertyRecordInput;
@@ -83,6 +88,11 @@ export interface DirectedEdgeInput extends EdgeClassOptions {
83
88
  readonly orientation: 'directed';
84
89
  readonly source: DirectedSourceInput;
85
90
  readonly target: DirectedTargetInput;
91
+ /**
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.
94
+ */
95
+ readonly unique?: readonly EdgeUniqueTuple[];
86
96
  }
87
97
  export interface UndirectedEdgeInput extends Omit<EdgeClassOptions, 'constraints'> {
88
98
  readonly orientation: 'undirected';
@@ -17,6 +17,7 @@ export function nodeClass(config) {
17
17
  const admitted = config ?? {};
18
18
  assertKnownKeys(admitted, [
19
19
  'abstract',
20
+ 'inheritance',
20
21
  'description',
21
22
  'extends',
22
23
  'properties',
@@ -39,15 +40,17 @@ export const edgeClass = Object.freeze({
39
40
  'source',
40
41
  'target',
41
42
  'abstract',
43
+ 'inheritance',
42
44
  'description',
43
45
  'extends',
44
46
  'properties',
45
47
  'policies',
46
48
  'constraints',
49
+ 'unique',
47
50
  ]);
48
51
  validateDirectedSourceInput(input.source);
49
52
  validateDirectedTargetInput(input.target);
50
- const { source, target, ...options } = input;
53
+ const { source, target, unique, ...options } = input;
51
54
  validateEdgeClassOptions(options);
52
55
  const value = authoringValue('definition', {
53
56
  kind: 'edge',
@@ -56,6 +59,10 @@ export const edgeClass = Object.freeze({
56
59
  classInputs.set(value, {
57
60
  source: { ...source },
58
61
  target: { ...target },
62
+ // Copied, so the declaration does not follow later changes to the author's arrays.
63
+ ...(unique === undefined
64
+ ? {}
65
+ : { unique: unique.map((tuple) => [...tuple]) }),
59
66
  ...options,
60
67
  abstract: options.abstract ?? false,
61
68
  });
@@ -65,6 +72,7 @@ export const edgeClass = Object.freeze({
65
72
  assertKnownKeys(input, [
66
73
  'endpoints',
67
74
  'abstract',
75
+ 'inheritance',
68
76
  'description',
69
77
  'extends',
70
78
  'properties',
@@ -107,6 +115,7 @@ function validateIncidentEndpointInput(value) {
107
115
  function validateEdgeClassOptions(value) {
108
116
  assertKnownKeys(value, [
109
117
  'abstract',
118
+ 'inheritance',
110
119
  'description',
111
120
  'extends',
112
121
  'properties',
@@ -121,6 +130,7 @@ function validateEdgeClassOptions(value) {
121
130
  function validateUndirectedEdgeClassOptions(value) {
122
131
  assertKnownKeys(value, [
123
132
  'abstract',
133
+ 'inheritance',
124
134
  'description',
125
135
  'extends',
126
136
  'properties',
@@ -31,6 +31,7 @@ export class V1DefinitionCompiler {
31
31
  ref: { origin: this.context.origin, kind: 'class', name },
32
32
  kind: 'node',
33
33
  abstract: input.abstract ?? false,
34
+ ...declaredInheritance(input.inheritance),
34
35
  ...(input.description === undefined ? {} : { description: input.description }),
35
36
  extends: this.parents(input.extends, 'node', `${pointer}/extends`),
36
37
  properties: facet,
@@ -53,6 +54,7 @@ export class V1DefinitionCompiler {
53
54
  target: this.directedTarget(directed.target, `${pointer}/endpoints/target`),
54
55
  },
55
56
  constraints: directedConstraints(directed),
57
+ ...(directed.unique === undefined ? {} : { unique: directed.unique }),
56
58
  };
57
59
  }
58
60
  const undirected = classConfiguration(value);
@@ -69,6 +71,7 @@ export class V1DefinitionCompiler {
69
71
  ref: { origin: this.context.origin, kind: 'class', name },
70
72
  kind: 'edge',
71
73
  abstract: input.abstract ?? false,
74
+ ...declaredInheritance(input.inheritance),
72
75
  ...(input.description === undefined ? {} : { description: input.description }),
73
76
  extends: this.parents(input.extends, 'edge', `${pointer}/extends`),
74
77
  properties: facet,
@@ -158,6 +161,10 @@ export class V1DefinitionCompiler {
158
161
  }));
159
162
  }
160
163
  }
164
+ /** The default `'internal'` is never declared, so a Class that keeps it keeps its revision. */
165
+ function declaredInheritance(inheritance) {
166
+ return inheritance === undefined || inheritance === 'internal' ? {} : { inheritance };
167
+ }
161
168
  function dataDeclaration(mediaType) {
162
169
  return mediaType === undefined ? undefined : { mediaType };
163
170
  }
@@ -22,6 +22,8 @@ export function analyzeClasses(classes) {
22
22
  const parent = classes.get(Key.of(ref));
23
23
  if (parent === undefined)
24
24
  fail(`${located.pointer}/extends/${index}`, 'Parent Class is absent.');
25
+ if (parent.value.inheritance === 'final')
26
+ fail(`${located.pointer}/extends/${index}`, 'A final Class has no subclass.');
25
27
  return analyze(parent);
26
28
  });
27
29
  const properties = new Map();
@@ -1,6 +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
5
  import {} from '../../model/schema.js';
5
6
  import { invalid } from '../diagnostic.js';
6
7
  import { compiledCallable } from './callable.js';
@@ -25,9 +26,15 @@ export function compiledClass(input, values, path) {
25
26
  'constraints',
26
27
  'properties',
27
28
  'policies',
28
- ], 'Compiled Class', path, value.kind === 'node' ? ['data', 'description', 'icon'] : ['description']);
29
+ ], 'Compiled Class', path, value.kind === 'node'
30
+ ? ['data', 'description', 'icon', 'inheritance']
31
+ : ['description', 'inheritance', 'unique']);
29
32
  definitionRef(value.ref, 'class', 'Class ref', `${path}/ref`);
30
33
  boolean(value.abstract, 'Class abstract', `${path}/abstract`);
34
+ if (value.inheritance !== undefined &&
35
+ value.inheritance !== 'open' &&
36
+ value.inheritance !== 'final')
37
+ invalid('Class inheritance', undefined, `${path}/inheritance`);
31
38
  description(value.description, 'Class description', `${path}/description`);
32
39
  const extendsPath = `${path}/extends`;
33
40
  const parents = array(value.extends, 'Class extends', extendsPath).map((parent, index) => definitionRef(parent, 'class', 'Class parent', `${extendsPath}/${index}`));
@@ -93,9 +100,13 @@ function edgeContract(value, path) {
93
100
  object(endpoints.target, 'directed Edge target', `${endpointsPath}/target`).role) {
94
101
  invalid('Edge endpoint roles', undefined, endpointsPath);
95
102
  }
103
+ if (value.unique !== undefined)
104
+ uniqueTuples(value.unique, `${path}/unique`);
96
105
  return;
97
106
  }
98
- if (value.orientation !== 'undirected' || constraints.acyclic !== undefined) {
107
+ if (value.orientation !== 'undirected' ||
108
+ constraints.acyclic !== undefined ||
109
+ value.unique !== undefined) {
99
110
  invalid('Edge orientation', undefined, `${path}/orientation`);
100
111
  }
101
112
  const endpointsPath = `${path}/endpoints`;
@@ -109,6 +120,22 @@ function edgeContract(value, path) {
109
120
  invalid('Edge endpoint roles', undefined, endpointsPath);
110
121
  }
111
122
  }
123
+ /** A nonempty list of tuples of `EDGE_UNIQUE_TUPLES`, distinct and in their canonical order. */
124
+ function uniqueTuples(input, path) {
125
+ const tuples = array(input, 'Edge unique tuples', path);
126
+ if (tuples.length === 0)
127
+ invalid('Edge unique tuples', undefined, path);
128
+ const known = tuples.map((tuple, index) => {
129
+ const fields = edgeUniqueTuple(array(tuple, 'Edge unique tuple', `${path}/${index}`));
130
+ if (fields === undefined)
131
+ invalid('Edge unique tuple', undefined, `${path}/${index}`);
132
+ return fields;
133
+ });
134
+ for (const [index, tuple] of known.entries()) {
135
+ if (index > 0 && compareEdgeUniqueTuples(known[index - 1], tuple) >= 0)
136
+ invalid('Edge unique tuples', undefined, `${path}/${index}`);
137
+ }
138
+ }
112
139
  function edgeEndpoint(input, count, label, path) {
113
140
  const value = object(input, label, path);
114
141
  exact(value, ['role', 'accepts', count], label, path);
@@ -5,7 +5,7 @@ export { isBinaryInput } from './language/callable/model/callable.js';
5
5
  export type { EventOf, StateMachine, StateOf } from './language/state/index.js';
6
6
  export { PropertyValues } from './language/index.js';
7
7
  export * as metadata from './language/definition/metadata.js';
8
- export type { AnyResolvedClass, BoundMethodOf, BoundMethodShortcutsOf, CallableResultOf, CallableInputOf, CallableInputModeOf, CallableOutputOf, ClassSatisfactionKeysOf, ClassDefinition, CoreDefinition, CoreEdge, FunctionAuth, FunctionDefinition, BinaryInput, BinaryInputProjection, CallableInputContract, MethodDefinition, MethodOwnerKeyOf, CallablePolicy, ClassPolicy, ClassRuleChecks, ClassRuleObject, NodeClassPolicy, PolicyCheckExpression, PolicyDefinition, PropertyImmutability, PropertyInputOf, PropertyAccessOf, PropertyOutputOf, PropertyShortcutsOf, PropertyValuesOf, ResolvedClass, ResolvedCoreDefinition, ResolvedCoreEndpoint, ResolvedData, ResolvedFunction, ExecutableMethod, ResolvedMethod, ResolvedMethodCollection, ResolvedPolicy, ResolvedProperty, ResolvedView, MethodAccessOf, SelfMethodsOf, SelfPropertiesOf, SelfStaticMethodsOf, StaticMethodsOf, ViewDefinition, } from './language/index.js';
8
+ export type { AnyResolvedClass, BoundMethodOf, BoundMethodShortcutsOf, CallableResultOf, CallableInputOf, CallableInputModeOf, CallableOutputOf, ClassSatisfactionKeysOf, ClassDefinition, CoreDefinition, CoreEdge, FunctionAuth, FunctionDefinition, Inheritance, BinaryInput, BinaryInputProjection, CallableInputContract, MethodDefinition, MethodOwnerKeyOf, CallablePolicy, ClassPolicy, ClassRuleChecks, ClassRuleObject, NodeClassPolicy, PolicyCheckExpression, PolicyDefinition, PropertyImmutability, PropertyInputOf, PropertyAccessOf, PropertyOutputOf, PropertyShortcutsOf, PropertyValuesOf, ResolvedClass, ResolvedCoreDefinition, ResolvedCoreEndpoint, ResolvedData, ResolvedFunction, ExecutableMethod, ResolvedMethod, ResolvedMethodCollection, ResolvedPolicy, ResolvedProperty, ResolvedView, MethodAccessOf, SelfMethodsOf, SelfPropertiesOf, SelfStaticMethodsOf, StaticMethodsOf, ViewDefinition, } from './language/index.js';
9
9
  export { acceptDefinitionRef, callableAddress, ClassKey, equalDefinitionRefs, isDefinitionRef, Key, MethodKey, PropertyKey, } from './addressing/index.js';
10
10
  export type { ClassRef, CoreRef, DefinitionBase, DefinitionKey, DefinitionRef, FunctionRef, MethodAddress, PolicyRef, Revision, ViewRef, } from './addressing/index.js';
11
11
  export type { Definition } from './language/index.js';
@@ -1,12 +1,16 @@
1
1
  import type { ClassRef } from '../../addressing/index.js';
2
- import type { ClassKind } from './model/class.js';
3
- /** The representation shared by source, compiled and resolved Class admission. */
4
- export interface ClassInheritance {
2
+ import type { ClassKind, Inheritance } from './model/class.js';
3
+ /** A Class as satisfaction reads it, shared by source, compiled and resolved Class admission. */
4
+ export interface ClassAncestry {
5
5
  readonly ref: ClassRef;
6
6
  readonly kind: ClassKind;
7
7
  readonly extends: readonly ClassRef[];
8
8
  }
9
9
  /** Kernel graph roots are intrinsic supertypes, not authored structural Edge contracts. */
10
- export declare function isGraphRoot(value: Pick<ClassInheritance, 'ref' | 'kind'>): boolean;
10
+ export declare function isGraphRoot(value: Pick<ClassAncestry, 'ref' | 'kind'>): boolean;
11
11
  /** Same-kind satisfaction includes the intrinsic root and the declared parent closure. */
12
- export declare function classSatisfies(candidate: ClassInheritance, required: ClassInheritance, resolve: (ref: ClassRef) => ClassInheritance | undefined): boolean;
12
+ export declare function classSatisfies(candidate: ClassAncestry, required: ClassAncestry, resolve: (ref: ClassRef) => ClassAncestry | undefined): boolean;
13
+ /** The inheritance a Class declares, `'internal'` when it declares none. */
14
+ export declare function inheritanceOf(declaration: {
15
+ readonly inheritance?: Inheritance;
16
+ }): Inheritance;
@@ -23,3 +23,7 @@ export function classSatisfies(candidate, required, resolve) {
23
23
  };
24
24
  return visit(candidate);
25
25
  }
26
+ /** The inheritance a Class declares, `'internal'` when it declares none. */
27
+ export function inheritanceOf(declaration) {
28
+ return declaration.inheritance ?? 'internal';
29
+ }
@@ -5,6 +5,11 @@ import type { DataFacet } from './data.js';
5
5
  import type { EdgeContract } from './edge.js';
6
6
  import type { PropertyFacet } from './property.js';
7
7
  export type ClassKind = 'node' | 'edge';
8
+ /**
9
+ * Which Classes may extend a Class: `'open'`, those of its Domain and of every Domain that depends
10
+ * on that Domain; `'internal'`, those of its Domain only; `'final'`, none.
11
+ */
12
+ export type Inheritance = 'open' | 'internal' | 'final';
8
13
  /** An absent observation rule admits every caller; a delegation is still scoped by its ceiling. */
9
14
  export interface NodeClassPolicies {
10
15
  readonly read?: NodeClassPolicy;
@@ -18,6 +23,8 @@ export interface EdgeClassPolicies {
18
23
  export interface NodeClassDefinition extends DefinitionBase<ClassRef>, PropertyFacet, DataFacet {
19
24
  readonly kind: 'node';
20
25
  readonly abstract: boolean;
26
+ /** Absent when `'internal'`, the default, so a Class that keeps the default keeps its revision. */
27
+ readonly inheritance?: Exclude<Inheritance, 'internal'>;
21
28
  readonly description?: string;
22
29
  readonly extends: readonly ClassRef[];
23
30
  readonly methods: MethodRecord;
@@ -27,6 +34,8 @@ export interface NodeClassDefinition extends DefinitionBase<ClassRef>, PropertyF
27
34
  export type EdgeClassDefinition = DefinitionBase<ClassRef> & PropertyFacet & EdgeContract & {
28
35
  readonly kind: 'edge';
29
36
  readonly abstract: boolean;
37
+ /** Absent when `'internal'`, the default, so a Class that keeps the default keeps its revision. */
38
+ readonly inheritance?: Exclude<Inheritance, 'internal'>;
30
39
  readonly description?: string;
31
40
  readonly extends: readonly ClassRef[];
32
41
  readonly policies: EdgeClassPolicies;
@@ -22,6 +22,13 @@ export interface UndirectedEdgeConstraints {
22
22
  readonly noSelf?: true;
23
23
  readonly acyclic?: never;
24
24
  }
25
+ /** A field a uniqueness tuple names: an endpoint of the Edge, or its slug. */
26
+ export type EdgeUniqueField = 'source' | 'target' | 'slug';
27
+ /**
28
+ * The fields one uniqueness tuple of a directed Edge Class names, which no two Edges of the Classes
29
+ * it covers share: one endpoint, then the slug. `EDGE_UNIQUE_TUPLES` lists them in canonical order.
30
+ */
31
+ export type EdgeUniqueTuple = readonly ['source', 'slug'] | readonly ['target', 'slug'];
25
32
  export interface DirectedEdgeContract {
26
33
  readonly orientation: 'directed';
27
34
  readonly endpoints: {
@@ -29,10 +36,16 @@ export interface DirectedEdgeContract {
29
36
  readonly target: DirectedTargetEndpoint;
30
37
  };
31
38
  readonly constraints: DirectedEdgeConstraints;
39
+ /**
40
+ * The tuples no two Edges of this Class and of its active concrete descendants share. Absent
41
+ * when the Class declares none, so a Class without one keeps its revision; never empty.
42
+ */
43
+ readonly unique?: readonly EdgeUniqueTuple[];
32
44
  }
33
45
  export interface UndirectedEdgeContract {
34
46
  readonly orientation: 'undirected';
35
47
  readonly endpoints: readonly [IncidentEndpoint, IncidentEndpoint];
36
48
  readonly constraints: UndirectedEdgeConstraints;
49
+ readonly unique?: never;
37
50
  }
38
51
  export type EdgeContract = DirectedEdgeContract | UndirectedEdgeContract;
@@ -0,0 +1,24 @@
1
+ import type { EdgeUniqueTuple } from './model/edge.js';
2
+ /**
3
+ * Every uniqueness tuple a directed Edge Class may declare, in canonical order: an endpoint, then
4
+ * the slug. The source tuple names entries under their source; the target tuple, under their target.
5
+ */
6
+ export declare const EDGE_UNIQUE_TUPLES: readonly EdgeUniqueTuple[];
7
+ /** The fields of an Edge a uniqueness tuple reads; the slug is absent from an Edge without one. */
8
+ export interface EdgeUniqueFacts {
9
+ readonly source: string;
10
+ readonly target: string;
11
+ readonly slug?: string;
12
+ }
13
+ /** The tuple `value` spells, or undefined when it spells none. */
14
+ export declare function edgeUniqueTuple(value: unknown): EdgeUniqueTuple | undefined;
15
+ /** Whether `tuples` lists the tuple `value` spells: false when it spells none. */
16
+ export declare function includesEdgeUniqueTuple(tuples: readonly EdgeUniqueTuple[], value: unknown): boolean;
17
+ /** The canonical order of tuples: field by field, source before target before slug. */
18
+ export declare function compareEdgeUniqueTuples(left: EdgeUniqueTuple, right: EdgeUniqueTuple): number;
19
+ /**
20
+ * The value `edge` takes under `tuple`, or undefined when one of its fields is absent, so that an
21
+ * Edge without a slug never collides: two distinct Edges of the Classes a tuple covers collide
22
+ * exactly when they take one value.
23
+ */
24
+ export declare function edgeUniqueValue(tuple: EdgeUniqueTuple, edge: EdgeUniqueFacts): string | undefined;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Every uniqueness tuple a directed Edge Class may declare, in canonical order: an endpoint, then
3
+ * the slug. The source tuple names entries under their source; the target tuple, under their target.
4
+ */
5
+ export const EDGE_UNIQUE_TUPLES = Object.freeze([
6
+ Object.freeze(['source', 'slug']),
7
+ Object.freeze(['target', 'slug']),
8
+ ]);
9
+ /** The fields a tuple names, in canonical order. */
10
+ const FIELDS = Object.freeze(['source', 'target', 'slug']);
11
+ /** The tuple `value` spells, or undefined when it spells none. */
12
+ export function edgeUniqueTuple(value) {
13
+ if (!Array.isArray(value))
14
+ return undefined;
15
+ return EDGE_UNIQUE_TUPLES.find((tuple) => tuple.length === value.length && tuple.every((field, index) => field === value[index]));
16
+ }
17
+ /** Whether `tuples` lists the tuple `value` spells: false when it spells none. */
18
+ export function includesEdgeUniqueTuple(tuples, value) {
19
+ const tuple = edgeUniqueTuple(value);
20
+ return (tuple !== undefined &&
21
+ tuples.some((candidate) => compareEdgeUniqueTuples(candidate, tuple) === 0));
22
+ }
23
+ /** The canonical order of tuples: field by field, source before target before slug. */
24
+ export function compareEdgeUniqueTuples(left, right) {
25
+ for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
26
+ const order = FIELDS.indexOf(left[index]) - FIELDS.indexOf(right[index]);
27
+ if (order !== 0)
28
+ return order;
29
+ }
30
+ return left.length - right.length;
31
+ }
32
+ /**
33
+ * The value `edge` takes under `tuple`, or undefined when one of its fields is absent, so that an
34
+ * Edge without a slug never collides: two distinct Edges of the Classes a tuple covers collide
35
+ * exactly when they take one value.
36
+ */
37
+ export function edgeUniqueValue(tuple, edge) {
38
+ const values = [];
39
+ for (const field of tuple) {
40
+ const value = edge[field];
41
+ if (value === undefined)
42
+ return undefined;
43
+ values.push(value);
44
+ }
45
+ return JSON.stringify(values);
46
+ }
@@ -14,8 +14,11 @@ export type * from './callable/resolved.js';
14
14
  export type * from './class/model/class.js';
15
15
  export type { DataDefinition, DataFacet, MediaType } from './class/model/data.js';
16
16
  export type * from './class/model/edge.js';
17
+ export { inheritanceOf } from './class/inheritance.js';
17
18
  export { edgeLifetimes } from './class/lifetime.js';
18
19
  export type { EdgeLifetime } from './class/lifetime.js';
20
+ export { EDGE_UNIQUE_TUPLES, compareEdgeUniqueTuples, edgeUniqueTuple, edgeUniqueValue, includesEdgeUniqueTuple, } from './class/unique.js';
21
+ export type { EdgeUniqueFacts } from './class/unique.js';
19
22
  export type * from './class/model/property.js';
20
23
  export type * from './class/resolved.js';
21
24
  export type * from './class/facets.js';
@@ -3,6 +3,8 @@ export { isMethodExecutable } from './callable/executable.js';
3
3
  export { isBinaryInput } from './callable/model/callable.js';
4
4
  export { valueProgramIncludes } from './value/inclusion/includes.js';
5
5
  export { stateMachine } from './state/index.js';
6
+ export { inheritanceOf } from './class/inheritance.js';
6
7
  export { edgeLifetimes } from './class/lifetime.js';
8
+ export { EDGE_UNIQUE_TUPLES, compareEdgeUniqueTuples, edgeUniqueTuple, edgeUniqueValue, includesEdgeUniqueTuple, } from './class/unique.js';
7
9
  export { PropertyValues } from './class/property-values.js';
8
10
  export { isClassRuleChecks, policyVariableClass, policyVariableSelector, policyObjectKey, policyObjectOrigin, projectedMemberClass, } from './policy/model/policy.js';
@@ -7,10 +7,11 @@ const TYPE_RANK = new Map(TYPE_ORDER.map((name, index) => [name, index]));
7
7
  const encoder = new TextEncoder();
8
8
  /** Canonicalizes syntax without rewriting the schema's algebra. Validate first. */
9
9
  export function canonicalizeValueSchema(schema, context, registry) {
10
+ void context;
10
11
  void registry;
11
- return canonicalizeSchema(schema, true, context);
12
+ return canonicalizeSchema(schema, true);
12
13
  }
13
- function canonicalizeSchema(schema, root, context) {
14
+ function canonicalizeSchema(schema, root) {
14
15
  if (typeof schema === 'boolean')
15
16
  return schema;
16
17
  const result = {};
@@ -39,17 +40,17 @@ function canonicalizeSchema(schema, root, context) {
39
40
  result[keyword] = canonicalizePathAnnotation(value);
40
41
  }
41
42
  else if (SCHEMA_SINGLE_KEYWORDS.has(keyword) && isJsonSchema(value)) {
42
- result[keyword] = canonicalizeSchema(value, false, context);
43
+ result[keyword] = canonicalizeSchema(value, false);
43
44
  }
44
45
  else if (SCHEMA_ARRAY_KEYWORDS.has(keyword) && Array.isArray(value)) {
45
- result[keyword] = value.map((entry) => isJsonSchema(entry) ? canonicalizeSchema(entry, false, context) : canonicalizeJson(entry));
46
+ result[keyword] = value.map((entry) => isJsonSchema(entry) ? canonicalizeSchema(entry, false) : canonicalizeJson(entry));
46
47
  }
47
48
  else if (SCHEMA_MAP_KEYWORDS.has(keyword) && isJsonObject(value)) {
48
49
  const schemas = {};
49
50
  for (const name of Object.keys(value).sort()) {
50
51
  const entry = value[name];
51
52
  schemas[name] = isJsonSchema(entry)
52
- ? canonicalizeSchema(entry, false, context)
53
+ ? canonicalizeSchema(entry, false)
53
54
  : canonicalizeJson(entry);
54
55
  }
55
56
  result[keyword] = schemas;
@@ -1,5 +1,6 @@
1
1
  import type { Value as JsonValue } from '@astrale-os/kernel-dsl/value';
2
2
  import type { Key as SemanticKey } from '../../../addressing/index.js';
3
+ import type { Inheritance } from '../../../language/class/model/class.js';
3
4
  import type { EffectiveField } from '../../analysis/class.js';
4
5
  import type { Context } from '../context.js';
5
6
  import type { ClassCoordinate } from '../coordinate.js';
@@ -38,6 +39,8 @@ export declare function compareLostProperties({ pair, report, }: {
38
39
  readonly pair: SubjectPair;
39
40
  readonly report: Report;
40
41
  }): void;
42
+ /** Which Classes may extend a Class, as an extension facet compares it. */
43
+ export declare function inheritanceMeaning(inheritance: Inheritance): JsonValue;
41
44
  /** Newly required data is an obligation even though it was absent from the source footprint. */
42
45
  export declare function compareAddedProperties({ context, pair, report, }: {
43
46
  readonly context: ComparisonContext;
@@ -139,6 +139,10 @@ function propertyIdentities(identities) {
139
139
  properties: Object.fromEntries(identities.map((identity) => [identity, true])),
140
140
  });
141
141
  }
142
+ /** Which Classes may extend a Class, as an extension facet compares it. */
143
+ export function inheritanceMeaning(inheritance) {
144
+ return deepFreeze({ coordinate: 'class', inheritance });
145
+ }
142
146
  /** Newly required data is an obligation even though it was absent from the source footprint. */
143
147
  export function compareAddedProperties({ context, pair, report, }) {
144
148
  if (pair.source?.kind !== 'class' || pair.target?.kind !== 'class')
@@ -5,7 +5,7 @@ import type { ComparisonContext, SubjectPair } from '../scope.js';
5
5
  export declare function compareFacet({ context, pair, facet, }: {
6
6
  readonly context: ComparisonContext;
7
7
  readonly pair: SubjectPair;
8
- readonly facet: Exclude<Facet, 'class.composition' | 'class.properties' | 'property.required'>;
8
+ readonly facet: Exclude<Facet, 'class.composition' | 'class.properties' | 'class.inheritance' | 'property.required'>;
9
9
  }): {
10
10
  readonly before?: Value;
11
11
  readonly after?: Value;
@@ -15,7 +15,7 @@ export type DependencyMeaningLevel = 'reference' | 'inherit';
15
15
  * compile until it is classified, so meaning can only be narrowed by an explicit decision.
16
16
  */
17
17
  export type FieldRole = 'always' | 'inherit' | 'members' | {
18
- readonly excluded: 'identity' | 'annotation' | 'authorization' | 'presentation';
18
+ readonly excluded: 'identity' | 'annotation' | 'authorization' | 'extension' | 'presentation';
19
19
  };
20
20
  /** The coordinate Key already names the declaration; its `ref` is never repeated in the meaning. */
21
21
  export declare const CLASS_FIELDS: Readonly<{
@@ -24,12 +24,16 @@ export declare const CLASS_FIELDS: Readonly<{
24
24
  }>;
25
25
  kind: "always";
26
26
  abstract: "always";
27
+ inheritance: Readonly<{
28
+ excluded: 'extension';
29
+ }>;
27
30
  orientation: "always";
28
31
  extends: "inherit";
29
32
  policies: "inherit";
30
33
  data: "inherit";
31
34
  endpoints: "inherit";
32
35
  constraints: "inherit";
36
+ unique: "always";
33
37
  properties: "members";
34
38
  methods: "members";
35
39
  description: Readonly<{
@@ -119,12 +123,16 @@ export declare const FIELD_ROLES: Readonly<{
119
123
  }>;
120
124
  kind: "always";
121
125
  abstract: "always";
126
+ inheritance: Readonly<{
127
+ excluded: 'extension';
128
+ }>;
122
129
  orientation: "always";
123
130
  extends: "inherit";
124
131
  policies: "inherit";
125
132
  data: "inherit";
126
133
  endpoints: "inherit";
127
134
  constraints: "inherit";
135
+ unique: "always";
128
136
  properties: "members";
129
137
  methods: "members";
130
138
  description: Readonly<{
@@ -1,18 +1,24 @@
1
1
  const IDENTITY = Object.freeze({ excluded: 'identity' });
2
2
  const ANNOTATION = Object.freeze({ excluded: 'annotation' });
3
3
  const AUTHORIZATION = Object.freeze({ excluded: 'authorization' });
4
+ /** Who may extend a Class is no meaning a dependent compiles against. */
5
+ const EXTENSION = Object.freeze({ excluded: 'extension' });
4
6
  const PRESENTATION = Object.freeze({ excluded: 'presentation' });
5
7
  /** The coordinate Key already names the declaration; its `ref` is never repeated in the meaning. */
6
8
  export const CLASS_FIELDS = Object.freeze({
7
9
  ref: IDENTITY,
8
10
  kind: 'always',
9
11
  abstract: 'always',
12
+ inheritance: EXTENSION,
10
13
  orientation: 'always',
11
14
  extends: 'inherit',
12
15
  policies: 'inherit',
13
16
  data: 'inherit',
14
17
  endpoints: 'inherit',
15
18
  constraints: 'inherit',
19
+ // A uniqueness tuple bounds every write of the Classes it covers and what a step through them
20
+ // reaches, so every dependent that names the Class relies on it, not only one that extends it.
21
+ unique: 'always',
16
22
  properties: 'members',
17
23
  methods: 'members',
18
24
  description: ANNOTATION,
@@ -46,7 +46,7 @@ export interface Usage {
46
46
  readonly key?: Key;
47
47
  readonly relation: ReferenceSite | 'member' | 'abstract' | 'selection' | 'capability';
48
48
  }
49
- export type Facet = 'class.reference' | 'class.inherit' | 'class.composition' | 'class.properties' | 'class.endpoints' | 'property' | 'property.required' | 'method.existence' | 'method.object' | 'method.inheritance' | 'method.signature' | 'function.existence' | 'function.signature' | 'policy' | 'view' | 'core';
49
+ export type Facet = 'class.reference' | 'class.inherit' | 'class.composition' | 'class.properties' | 'class.endpoints' | 'class.inheritance' | 'property' | 'property.required' | 'method.existence' | 'method.object' | 'method.inheritance' | 'method.signature' | 'function.existence' | 'function.signature' | 'policy' | 'view' | 'core';
50
50
  /** A fact about a compared facet, including unchanged facets. No admission decision is implied. */
51
51
  export interface Observation {
52
52
  readonly id: string;
@@ -81,8 +81,9 @@ export interface Assessment {
81
81
  * meaning evidence never records whether capabilities were declared. A referenced or inherited Class
82
82
  * that loses an effective Property (`class.properties`) raises structure to version 4 and meaning to
83
83
  * version 2, and structure is version 4 too when it records that a dependent Policy lost what it
84
- * relies on (`method.object`, `class.endpoints`). Those facts are recorded only on such a loss, so
85
- * every other comparison, every compatible one included, keeps its bytes.
84
+ * relies on (`method.object`, `class.endpoints`) or that a Class the dependent extends became final
85
+ * (`class.inheritance`). Those facts are recorded only on such a loss, so every other comparison,
86
+ * every compatible one included, keeps its bytes.
86
87
  */
87
88
  export interface ComparisonEvidence {
88
89
  readonly kind: 'structure' | 'meaning';
@@ -12,8 +12,9 @@ export function analyzeStructure(context) {
12
12
  ...result,
13
13
  compatible: result.assessments.every(({ satisfied }) => satisfied),
14
14
  // Each version adds one requirement policy and applies only when its fact is present: 3 a
15
- // declared capability root, 4 a lost effective Property of a reached Class or a failed Policy
16
- // reliance, facts recorded only when they fail. Without one, evidence keeps its version-2 bytes.
15
+ // declared capability root, 4 a lost effective Property of a reached Class, a failed Policy
16
+ // reliance or a refused extension, facts recorded only when they fail. Without one, evidence
17
+ // keeps its version-2 bytes.
17
18
  evidence: evidenceOf({
18
19
  kind: 'structure',
19
20
  version: recordsFailureOnlyFact(result.assessments)
@@ -27,13 +28,14 @@ export function analyzeStructure(context) {
27
28
  }
28
29
  /**
29
30
  * The facts structure records only when they fail: a referenced or inherited Class that lost an
30
- * effective Property, and what a dependent Policy relies on (a Method object, a matched Edge's
31
- * endpoints).
31
+ * effective Property, what a dependent Policy relies on (a Method object, a matched Edge's
32
+ * endpoints), and a Class the dependent extends that became final.
32
33
  */
33
34
  const FAILURE_ONLY_FACETS = new Set([
34
35
  'class.properties',
35
36
  'method.object',
36
37
  'class.endpoints',
38
+ 'class.inheritance',
37
39
  ]);
38
40
  function recordsFailureOnlyFact(assessments) {
39
41
  return assessments.some(({ requirement }) => FAILURE_ONLY_FACETS.has(requirement.kind));
@@ -1,5 +1,6 @@
1
1
  import { Key } from '../../../addressing/index.js';
2
- import { compareAddedProperties, compareLostProperties } from '../comparators/classes.js';
2
+ import { inheritanceOf } from '../../../language/class/inheritance.js';
3
+ import { compareAddedProperties, compareLostProperties, inheritanceMeaning, } from '../comparators/classes.js';
3
4
  import { compareFacet } from '../comparators/index.js';
4
5
  import { fingerprintMeaning } from '../comparators/values.js';
5
6
  import { contextInvalid } from '../context.js';
@@ -73,6 +74,7 @@ export function compareRequirements({ context, report, }) {
73
74
  compareLostProperties({ pair, report });
74
75
  }
75
76
  compareReliance({ context, report });
77
+ compareExtension({ context, report });
76
78
  }
77
79
  /**
78
80
  * What a dependent Policy relies on is recorded only when it fails: a Method it names that is gone
@@ -95,3 +97,29 @@ function compareReliance({ context, report, }) {
95
97
  report.record({ pair, facet, before, after, requirement: { kind: facet } });
96
98
  }
97
99
  }
100
+ /**
101
+ * A Class one of the dependent's own Classes extends must still admit it: the dependent is refused
102
+ * once the compared origin declares that Class `'final'`. The fact is recorded only when it fails,
103
+ * like a Policy reliance, so a compatible comparison keeps its evidence bytes.
104
+ */
105
+ function compareExtension({ context, report, }) {
106
+ for (const reference of context.roots) {
107
+ if (reference.usage.relation !== 'extends')
108
+ continue;
109
+ if (Key.origin(reference.coordinate.key) !== context.source.origin)
110
+ continue;
111
+ const pair = context.pair(reference.coordinate);
112
+ if (pair.source?.kind !== 'class' || pair.target?.kind !== 'class')
113
+ continue;
114
+ if (pair.target.analysis.declaration.inheritance !== 'final')
115
+ continue;
116
+ report.note(reference);
117
+ report.record({
118
+ pair,
119
+ facet: 'class.inheritance',
120
+ before: inheritanceMeaning(inheritanceOf(pair.source.analysis.declaration)),
121
+ after: inheritanceMeaning('final'),
122
+ requirement: { kind: 'class.inheritance' },
123
+ });
124
+ }
125
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Report } from '../report.js';
2
2
  import type { ComparisonContext } from '../scope.js';
3
3
  /**
4
- * Compare inherited contracts and newly imposed obligations on existing Classes.
4
+ * Compare inherited contracts, newly imposed obligations and who may extend existing Classes.
5
5
  * Obligations are reported at their declaring Method: changed signatures retain their comparison;
6
6
  * an unchanged obligation newly inherited by a Class is added and unsatisfied.
7
7
  */
@@ -1,10 +1,11 @@
1
+ import { inheritanceOf } from '../../../language/class/inheritance.js';
1
2
  import { methodInheritance } from '../comparators/callables.js';
2
- import { compareAddedProperties } from '../comparators/classes.js';
3
+ import { compareAddedProperties, inheritanceMeaning } from '../comparators/classes.js';
3
4
  import { compareFacet } from '../comparators/index.js';
4
5
  import { fingerprintMeaning } from '../comparators/values.js';
5
6
  import { classCoordinate, methodCoordinate } from '../coordinate.js';
6
7
  /**
7
- * Compare inherited contracts and newly imposed obligations on existing Classes.
8
+ * Compare inherited contracts, newly imposed obligations and who may extend existing Classes.
8
9
  * Obligations are reported at their declaring Method: changed signatures retain their comparison;
9
10
  * an unchanged obligation newly inherited by a Class is added and unsatisfied.
10
11
  */
@@ -25,6 +26,7 @@ export function compareSurface({ context, report, }) {
25
26
  requirement: { kind: 'class.inherit' },
26
27
  });
27
28
  compareAddedProperties({ context, pair, report });
29
+ compareExtension({ pair, report });
28
30
  for (const [key, method] of source.analysis.requirements)
29
31
  if (!established.has(key))
30
32
  established.set(key, method);
@@ -54,6 +56,27 @@ export function compareSurface({ context, report, }) {
54
56
  });
55
57
  }
56
58
  }
59
+ /**
60
+ * A changed inheritance is observed on its Class, and is major when the Class was `'open'` or
61
+ * becomes `'final'`. Closing an `'open'` Class withdraws what its declaration granted other Domains;
62
+ * acceptance refuses a subclass of a `'final'` Class of any Domain and admits one of another
63
+ * Domain's `'internal'` Class (DV-CLASS-INHERITANCE), so finalizing that Class can refuse a dependent.
64
+ */
65
+ function compareExtension({ pair, report, }) {
66
+ if (pair.source?.kind !== 'class' || pair.target?.kind !== 'class')
67
+ return;
68
+ const before = inheritanceOf(pair.source.analysis.declaration);
69
+ const after = inheritanceOf(pair.target.analysis.declaration);
70
+ if (before === after)
71
+ return;
72
+ report.record({
73
+ pair,
74
+ facet: 'class.inheritance',
75
+ before: inheritanceMeaning(before),
76
+ after: inheritanceMeaning(after),
77
+ requirement: { kind: 'class.inheritance', satisfied: before !== 'open' && after !== 'final' },
78
+ });
79
+ }
57
80
  function changed({ before, after }) {
58
81
  return (before === undefined ||
59
82
  after === undefined ||
@@ -1,4 +1,4 @@
1
- export type SchemaDiagnosticCode = 'DM_CAPABILITY_INVALID' | 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_ADMISSION' | '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_FOREIGN_MINIMUM' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_ENTRYPOINT' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_IMMUTABLE' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | '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_CAPABILITY_INVALID' | 'DM_MEMBER_ROOT_INVALID' | 'DM_DEPENDENCY_CONTEXT_INVALID' | 'DM_DEPENDENCY_NOT_RETAINED' | 'DM_MEANING_INCONSISTENT' | 'DM_MEMBER_ORIGIN_MISMATCH' | 'DV_ACCEPTANCE_CONTEXT' | 'DV_CALLABLE_ADMISSION' | 'DV_CALLABLE_AUTH' | 'DV_CALLABLE_IO' | 'DV_CALLABLE_POLICY_AUTH' | 'DV_CLASS_ABSTRACT' | 'DV_CLASS_ACYCLIC' | 'DV_CLASS_INHERITANCE' | '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_FOREIGN_MINIMUM' | 'DV_EDGE_ROLE' | 'DV_EDGE_UNDIRECTED' | 'DV_ENTRYPOINT' | 'DV_JSON_VALUE' | 'DV_METHOD_ABSTRACT' | 'DV_METHOD_COMPOSITION' | 'DV_PROPERTY_AUTO' | 'DV_PROPERTY_COMPOSITION' | 'DV_PROPERTY_IMMUTABLE' | 'DV_PROPERTY_STATE' | 'DV_REF_RESOLVES' | 'DV_VALUE_SCHEMA' | '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,6 +1,7 @@
1
1
  import { json } from '@astrale-os/kernel-dsl/value';
2
2
  import { compareDefinitionRefs, compareUnicode } from '../../addressing/ordering.js';
3
3
  import { isBinaryInput } from '../../language/callable/model/callable.js';
4
+ import { compareEdgeUniqueTuples } from '../../language/class/unique.js';
4
5
  import { normalizeClassRule, normalizePolicyChecks, normalizePolicyExpression, } from '../../language/policy/normalization/normalize.js';
5
6
  import { admitStateTopology } from '../../language/state/topology.js';
6
7
  import { canonicalizeValueSchema } from '../../language/value/profile/canonicalize.js';
@@ -61,6 +62,7 @@ function canonicalizeClass(value) {
61
62
  ref: value.ref,
62
63
  kind: value.kind,
63
64
  abstract: value.abstract,
65
+ ...(value.inheritance === undefined ? {} : { inheritance: value.inheritance }),
64
66
  extends: [...value.extends].sort(compareDefinitionRefs),
65
67
  properties: mapRecord(value.properties, canonicalizeProperty),
66
68
  };
@@ -80,6 +82,9 @@ function canonicalizeClass(value) {
80
82
  endpoints: canonicalizeDirectedEndpoints(value.endpoints),
81
83
  policies: value.policies,
82
84
  constraints: value.constraints,
85
+ ...(value.unique === undefined
86
+ ? {}
87
+ : { unique: [...value.unique].sort(compareEdgeUniqueTuples) }),
83
88
  }
84
89
  : {
85
90
  ...base,
@@ -109,6 +109,9 @@ function validateParents(environment, owner, ownerKind, parents, diagnostics) {
109
109
  if (target.ref.kind !== 'class' || target.declaration.kind !== ownerKind) {
110
110
  diagnostics.push(diagnostic('DV_CLASS_KIND', pointer, `${formatDefinitionRef(owner)} cannot extend different-kind Class ${formatDefinitionRef(parent)}.`, formatDefinitionRef(parent)));
111
111
  }
112
+ else if (target.declaration.inheritance === 'final') {
113
+ diagnostics.push(diagnostic('DV_CLASS_INHERITANCE', pointer, `${formatDefinitionRef(owner)} cannot extend final Class ${formatDefinitionRef(parent)}.`, formatDefinitionRef(parent)));
114
+ }
112
115
  }
113
116
  }
114
117
  function validateEdgeContract(environment, owner, declaration, diagnostics) {
@@ -98,6 +98,9 @@ const edgeConstraintsSchema = strictObject({
98
98
  noSelf: literal(true).optional(),
99
99
  acyclic: literal(true).optional(),
100
100
  });
101
+ // A tuple names an endpoint, then the slug (`EDGE_UNIQUE_TUPLES`). No tuple is spelled by the
102
+ // 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);
101
104
  const policyPatternSubjectSchema = strictObject({ kind: literal('subject') });
102
105
  const policyPatternObjectSchema = strictObject({ kind: literal('object') });
103
106
  const policyPatternSourceSchema = strictObject({ kind: literal('source') });
@@ -233,10 +236,13 @@ const edgeClassPoliciesSchema = strictObject({
233
236
  read: classPolicySchema.optional(),
234
237
  traverse: classPolicySchema.optional(),
235
238
  });
239
+ // The default `'internal'` is the absence of the field: one spelling per meaning, one revision.
240
+ const inheritanceSchema = enumeration(['open', 'final']);
236
241
  const nodeClassSchema = strictObject({
237
242
  ref: classRefSchema,
238
243
  kind: literal('node'),
239
244
  abstract: boolean(),
245
+ inheritance: inheritanceSchema.optional(),
240
246
  description: descriptionSchema.optional(),
241
247
  extends: uniqueArray(classRefSchema),
242
248
  properties: propertyRecordSchema,
@@ -249,10 +255,12 @@ const edgeClassShape = {
249
255
  ref: classRefSchema,
250
256
  kind: literal('edge'),
251
257
  abstract: boolean(),
258
+ inheritance: inheritanceSchema.optional(),
252
259
  description: descriptionSchema.optional(),
253
260
  extends: uniqueArray(classRefSchema),
254
261
  properties: propertyRecordSchema,
255
262
  constraints: edgeConstraintsSchema,
263
+ unique: edgeUniqueSchema.optional(),
256
264
  };
257
265
  const edgeClassSchema = strictObject({
258
266
  ...edgeClassShape,
@@ -262,14 +270,22 @@ const edgeClassSchema = strictObject({
262
270
  }).superRefine(validateEdgeOrientationShape);
263
271
  const classDeclarationSchema = discriminatedUnion('kind', [nodeClassSchema, edgeClassSchema]);
264
272
  function validateEdgeOrientationShape(value, context) {
265
- if ((value.orientation === 'undirected') === Array.isArray(value.endpoints))
266
- return;
267
- context.addIssue({
268
- code: 'custom',
269
- path: ['endpoints'],
270
- message: `${value.orientation} Edge endpoints have the wrong structural shape.`,
271
- params: { structureCode: 'STRUCTURE_EDGE_ORIENTATION' },
272
- });
273
+ if ((value.orientation === 'undirected') !== Array.isArray(value.endpoints)) {
274
+ context.addIssue({
275
+ code: 'custom',
276
+ path: ['endpoints'],
277
+ message: `${value.orientation} Edge endpoints have the wrong structural shape.`,
278
+ params: { structureCode: 'STRUCTURE_EDGE_ORIENTATION' },
279
+ });
280
+ }
281
+ if (value.orientation === 'undirected' && value.unique !== undefined) {
282
+ context.addIssue({
283
+ code: 'custom',
284
+ path: ['unique'],
285
+ message: 'Only a directed Edge Class declares unique tuples.',
286
+ params: { structureCode: 'STRUCTURE_EDGE_ORIENTATION' },
287
+ });
288
+ }
273
289
  }
274
290
  const functionDefinitionSchema = strictObject({
275
291
  ref: functionRefSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {