@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.
- package/dist/v1/builder/class/authoring.d.ts +11 -1
- package/dist/v1/builder/class/authoring.js +11 -1
- package/dist/v1/builder/lowering/class.js +7 -0
- package/dist/v1/compiled/admission/semantics/class.js +2 -0
- package/dist/v1/compiled/admission/wire/class.js +29 -2
- package/dist/v1/index.d.ts +1 -1
- package/dist/v1/language/class/inheritance.d.ts +9 -5
- package/dist/v1/language/class/inheritance.js +4 -0
- package/dist/v1/language/class/model/class.d.ts +9 -0
- package/dist/v1/language/class/model/edge.d.ts +13 -0
- package/dist/v1/language/class/unique.d.ts +24 -0
- package/dist/v1/language/class/unique.js +46 -0
- package/dist/v1/language/index.d.ts +3 -0
- package/dist/v1/language/index.js +2 -0
- package/dist/v1/language/value/profile/canonicalize.js +6 -5
- package/dist/v1/schema/compatibility/comparators/classes.d.ts +3 -0
- package/dist/v1/schema/compatibility/comparators/classes.js +4 -0
- package/dist/v1/schema/compatibility/comparators/index.d.ts +1 -1
- package/dist/v1/schema/compatibility/fields.d.ts +9 -1
- package/dist/v1/schema/compatibility/fields.js +6 -0
- package/dist/v1/schema/compatibility/model.d.ts +4 -3
- package/dist/v1/schema/compatibility/structure/index.js +6 -4
- package/dist/v1/schema/compatibility/structure/requirements.js +29 -1
- package/dist/v1/schema/compatibility/versioning/surface.d.ts +1 -1
- package/dist/v1/schema/compatibility/versioning/surface.js +25 -2
- package/dist/v1/schema/diagnostic.d.ts +1 -1
- package/dist/v1/schema/normalization/domain.js +5 -0
- package/dist/v1/schema/validation/class.js +3 -0
- package/dist/v1/schema/validation/domain.structure.js +24 -8
- 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'
|
|
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' ||
|
|
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);
|
package/dist/v1/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
4
|
-
export interface
|
|
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<
|
|
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:
|
|
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
|
|
12
|
+
return canonicalizeSchema(schema, true);
|
|
12
13
|
}
|
|
13
|
-
function canonicalizeSchema(schema, root
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
85
|
-
*
|
|
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
|
|
16
|
-
// reliance, facts recorded only when they fail. Without one, evidence
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
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')
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|