@astrale-os/kernel-dsl 1.0.0-beta.6 → 1.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/v1/builder/class/authoring.d.ts +29 -4
  2. package/dist/v1/builder/class/authoring.js +15 -5
  3. package/dist/v1/builder/index.d.ts +1 -1
  4. package/dist/v1/builder/lowering/class.js +11 -1
  5. package/dist/v1/builder/lowering/property-keys.d.ts +27 -0
  6. package/dist/v1/builder/lowering/property-keys.js +108 -0
  7. package/dist/v1/builder/lowering/schema/compile.js +8 -3
  8. package/dist/v1/builder/lowering/schema/lifetime.d.ts +11 -0
  9. package/dist/v1/builder/lowering/schema/lifetime.js +55 -0
  10. package/dist/v1/builder/schema/diagnostic.d.ts +1 -1
  11. package/dist/v1/compiled/admission/semantics/cardinality.d.ts +1 -1
  12. package/dist/v1/compiled/admission/semantics/cardinality.js +16 -1
  13. package/dist/v1/compiled/admission/semantics/class.js +23 -0
  14. package/dist/v1/compiled/admission/semantics/core.js +16 -0
  15. package/dist/v1/compiled/admission/wire/class.js +32 -7
  16. package/dist/v1/language/class/lifetime.d.ts +48 -11
  17. package/dist/v1/language/class/lifetime.js +86 -20
  18. package/dist/v1/language/class/model/class.d.ts +16 -1
  19. package/dist/v1/language/class/model/edge.d.ts +22 -4
  20. package/dist/v1/language/class/unique.d.ts +134 -0
  21. package/dist/v1/language/class/unique.js +257 -0
  22. package/dist/v1/language/index.d.ts +4 -2
  23. package/dist/v1/language/index.js +2 -1
  24. package/dist/v1/schema/diagnostic.d.ts +1 -1
  25. package/dist/v1/schema/normalization/domain.js +15 -1
  26. package/dist/v1/schema/validation/class.js +70 -1
  27. package/dist/v1/schema/validation/core.js +50 -0
  28. package/dist/v1/schema/validation/domain.structure.js +19 -5
  29. package/package.json +1 -1
  30. package/dist/v1/builder/lowering/core-properties.d.ts +0 -10
  31. package/dist/v1/builder/lowering/core-properties.js +0 -76
@@ -1,17 +1,54 @@
1
1
  import type { ClassRef } from '../../addressing/index.js';
2
- import type { EdgeContract } from './model/edge.js';
2
+ import type { ClassDefinition, EdgeClassDefinition } from './model/class.js';
3
+ import type { EdgeContract, EdgeCount, EdgeEndpointBase, EdgeLifetime } from './model/edge.js';
4
+ export type { EdgeLifetime } from './model/edge.js';
5
+ /** The Classes lifetimes read: those of an Edge Class's Domain and of its dependency closure. */
6
+ export interface LifetimeScope {
7
+ /**
8
+ * Whether a Node of another Domain may be of the Class: it is `'open'`, or a Class of its own
9
+ * Domain that extends it is.
10
+ */
11
+ readonly extendable: (ref: ClassRef) => boolean;
12
+ }
13
+ /** The lifetime scope of `classes`. */
14
+ export declare function lifetimeScope(classes: Iterable<ClassDefinition>): LifetimeScope;
15
+ /** One endpoint of an Edge contract and how many Edges of the Class each Node in it has. */
16
+ export interface ContractEndpoint {
17
+ readonly endpoint: EdgeEndpointBase;
18
+ readonly count: EdgeCount;
19
+ }
20
+ /** The endpoints of an Edge contract in order: source then target, or the two incident ones. */
21
+ export declare function contractEndpoints(contract: EdgeContract): readonly [ContractEndpoint, ContractEndpoint];
3
22
  /**
4
- * What deleting the Node at one endpoint does to an Edge: `restrict` requires the transition to
5
- * delete the Edge explicitly; `detach` deletes the Edge with the Node.
23
+ * Whether an endpoint of an Edge Class of `origin` can hold a Node of another Domain: it accepts a
24
+ * Class `origin` does not own, or one another Domain may extend.
6
25
  */
7
- export type EdgeLifetime = 'restrict' | 'detach';
26
+ export declare function reachesAnotherDomain(origin: ClassRef['origin'], endpoint: EdgeEndpointBase, scope: LifetimeScope): boolean;
8
27
  /**
9
- * The lifetime of each endpoint of an Edge Class, in endpoint order (source then target, or the two
10
- * incident endpoints), derived from the contract its Domain `origin` declares.
28
+ * The lifetime of each endpoint of `edge`, in endpoint order: the one it declares, else the derived
29
+ * one.
11
30
  *
12
- * An Edge Class binds only the Nodes of its own Domain and of the Domains that depend on it. An
13
- * endpoint that accepts a Class `origin` does not own can therefore reach a Node it does not bind,
14
- * and such a foreign endpoint detaches, unless an endpoint declares a minimum count that
15
- * detaching would break. Every other endpoint restricts.
31
+ * An Edge Class binds only the Nodes of its own Domain and of the Domains that depend on it, and a
32
+ * Domain blocks only the deletion of its own Nodes. An endpoint that can hold a Node of another
33
+ * Domain therefore detaches, unless an endpoint of the Class declares a minimum count that
34
+ * detaching could break (`requiredEndpoint`). Every other endpoint restricts.
16
35
  */
17
- export declare function edgeLifetimes(origin: ClassRef['origin'], contract: EdgeContract): readonly [EdgeLifetime, EdgeLifetime];
36
+ export declare function edgeLifetimes(edge: EdgeClassDefinition, scope: LifetimeScope): readonly [EdgeLifetime, EdgeLifetime];
37
+ /** One breach of the lifetime laws at one endpoint of an Edge Class: its index and why. */
38
+ export interface LifetimeBreach {
39
+ readonly index: 0 | 1;
40
+ readonly reason: string;
41
+ }
42
+ /**
43
+ * The breaches of the lifetime laws (LANGUAGE-EDGE-LIFETIME) by the lifetimes `edge` declares: an
44
+ * endpoint that can hold a Node of another Domain never restricts, and none detaches in a Class
45
+ * with an endpoint that requires an Edge (`requiredEndpoint`). A derived lifetime breaks neither.
46
+ */
47
+ export declare function lifetimeBreaches(edge: EdgeClassDefinition, scope: LifetimeScope): readonly LifetimeBreach[];
48
+ /**
49
+ * The first endpoint of an Edge Class that requires an Edge, if one does, with its index. Detaching
50
+ * there could leave a Node below its count, so an endpoint of such a Class never detaches.
51
+ */
52
+ export declare function requiredEndpoint(edge: EdgeContract): (ContractEndpoint & {
53
+ readonly index: 0 | 1;
54
+ }) | undefined;
@@ -1,26 +1,92 @@
1
- /**
2
- * The lifetime of each endpoint of an Edge Class, in endpoint order (source then target, or the two
3
- * incident endpoints), derived from the contract its Domain `origin` declares.
4
- *
5
- * An Edge Class binds only the Nodes of its own Domain and of the Domains that depend on it. An
6
- * endpoint that accepts a Class `origin` does not own can therefore reach a Node it does not bind,
7
- * and such a foreign endpoint detaches, unless an endpoint declares a minimum count that
8
- * detaching would break. Every other endpoint restricts.
9
- */
10
- export function edgeLifetimes(origin, contract) {
11
- const endpoints = contract.orientation === 'directed'
1
+ import { definitionRefIndex } from '../../addressing/ordering.js';
2
+ /** The lifetime scope of `classes`. */
3
+ export function lifetimeScope(classes) {
4
+ const declarations = new Map();
5
+ for (const declaration of classes)
6
+ declarations.set(definitionRefIndex(declaration.ref), declaration);
7
+ const extendable = new Set();
8
+ const open = (ref) => {
9
+ const key = definitionRefIndex(ref);
10
+ if (extendable.has(key))
11
+ return;
12
+ extendable.add(key);
13
+ for (const parent of declarations.get(key)?.extends ?? [])
14
+ if (parent.origin === ref.origin)
15
+ open(parent);
16
+ };
17
+ for (const declaration of declarations.values())
18
+ if (declaration.kind === 'node' && declaration.inheritance === 'open')
19
+ open(declaration.ref);
20
+ return Object.freeze({
21
+ extendable: (ref) => extendable.has(definitionRefIndex(ref)),
22
+ });
23
+ }
24
+ /** The endpoints of an Edge contract in order: source then target, or the two incident ones. */
25
+ export function contractEndpoints(contract) {
26
+ return contract.orientation === 'directed'
12
27
  ? [
13
- [contract.endpoints.source.accepts, contract.endpoints.source.outgoing],
14
- [contract.endpoints.target.accepts, contract.endpoints.target.incoming],
28
+ { endpoint: contract.endpoints.source, count: contract.endpoints.source.outgoing },
29
+ { endpoint: contract.endpoints.target, count: contract.endpoints.target.incoming },
15
30
  ]
16
31
  : [
17
- [contract.endpoints[0].accepts, contract.endpoints[0].incident],
18
- [contract.endpoints[1].accepts, contract.endpoints[1].incident],
32
+ { endpoint: contract.endpoints[0], count: contract.endpoints[0].incident },
33
+ { endpoint: contract.endpoints[1], count: contract.endpoints[1].incident },
19
34
  ];
20
- const required = endpoints.some(([, count]) => minimum(count) > 0);
21
- const lifetime = ([accepts]) => !required && accepts.some((ref) => ref.origin !== origin) ? 'detach' : 'restrict';
22
- return Object.freeze([lifetime(endpoints[0]), lifetime(endpoints[1])]);
23
35
  }
24
- function minimum(count) {
25
- return count === '1' || count === '1..*' ? 1 : 0;
36
+ /**
37
+ * Whether an endpoint of an Edge Class of `origin` can hold a Node of another Domain: it accepts a
38
+ * Class `origin` does not own, or one another Domain may extend.
39
+ */
40
+ export function reachesAnotherDomain(origin, endpoint, scope) {
41
+ return endpoint.accepts.some((ref) => ref.origin !== origin || scope.extendable(ref));
42
+ }
43
+ /**
44
+ * The lifetime of each endpoint of `edge`, in endpoint order: the one it declares, else the derived
45
+ * one.
46
+ *
47
+ * An Edge Class binds only the Nodes of its own Domain and of the Domains that depend on it, and a
48
+ * Domain blocks only the deletion of its own Nodes. An endpoint that can hold a Node of another
49
+ * Domain therefore detaches, unless an endpoint of the Class declares a minimum count that
50
+ * detaching could break (`requiredEndpoint`). Every other endpoint restricts.
51
+ */
52
+ export function edgeLifetimes(edge, scope) {
53
+ const required = requiredEndpoint(edge) !== undefined;
54
+ const [head, tail] = contractEndpoints(edge).map(({ endpoint }) => endpoint.onDelete ??
55
+ (!required && reachesAnotherDomain(edge.ref.origin, endpoint, scope) ? 'detach' : 'restrict'));
56
+ return Object.freeze([head, tail]);
57
+ }
58
+ /**
59
+ * The breaches of the lifetime laws (LANGUAGE-EDGE-LIFETIME) by the lifetimes `edge` declares: an
60
+ * endpoint that can hold a Node of another Domain never restricts, and none detaches in a Class
61
+ * with an endpoint that requires an Edge (`requiredEndpoint`). A derived lifetime breaks neither.
62
+ */
63
+ export function lifetimeBreaches(edge, scope) {
64
+ const required = requiredEndpoint(edge)?.count;
65
+ return Object.freeze(contractEndpoints(edge).flatMap(({ endpoint }, index) => {
66
+ const at = index;
67
+ if (endpoint.onDelete === 'restrict' &&
68
+ reachesAnotherDomain(edge.ref.origin, endpoint, scope))
69
+ return [
70
+ {
71
+ index: at,
72
+ reason: 'can hold Nodes of another Domain and cannot restrict their deletion.',
73
+ },
74
+ ];
75
+ if (endpoint.onDelete === 'detach' && required !== undefined)
76
+ return [
77
+ {
78
+ index: at,
79
+ reason: `cannot detach: an endpoint of the Class requires ${required} Edges.`,
80
+ },
81
+ ];
82
+ return [];
83
+ }));
84
+ }
85
+ /**
86
+ * The first endpoint of an Edge Class that requires an Edge, if one does, with its index. Detaching
87
+ * there could leave a Node below its count, so an endpoint of such a Class never detaches.
88
+ */
89
+ export function requiredEndpoint(edge) {
90
+ const index = contractEndpoints(edge).findIndex(({ count }) => count === '1' || count === '1..*');
91
+ return index === -1 ? undefined : { ...contractEndpoints(edge)[index], index: index };
26
92
  }
@@ -1,4 +1,4 @@
1
- import type { ClassRef, DefinitionBase } from '../../../addressing/index.js';
1
+ import type { ClassRef, DefinitionBase, PropertyKey } from '../../../addressing/index.js';
2
2
  import type { MethodRecord } from '../../callable/model/callable.js';
3
3
  import type { ClassPolicy, NodeClassPolicy } from '../../policy/model/policy.js';
4
4
  import type { DataFacet } from './data.js';
@@ -20,6 +20,16 @@ export interface EdgeClassPolicies {
20
20
  readonly read?: ClassPolicy;
21
21
  readonly traverse?: ClassPolicy;
22
22
  }
23
+ /**
24
+ * The Properties one key of a Node Class names, own or inherited, which no two Nodes of the Classes
25
+ * it covers share while each of them is present: distinct, by key in code-point order.
26
+ */
27
+ export type NodeUniqueTuple = readonly [PropertyKey, ...PropertyKey[]];
28
+ /**
29
+ * The form of a Node key tuple: its fields are Properties, whatever their keys and number. A
30
+ * backend announces it to enforce Node keys, so a tuple is never ignored where it is not.
31
+ */
32
+ export type NodeUniqueForm = readonly ['property'];
23
33
  export interface NodeClassDefinition extends DefinitionBase<ClassRef>, PropertyFacet, DataFacet {
24
34
  readonly kind: 'node';
25
35
  readonly abstract: boolean;
@@ -30,6 +40,11 @@ export interface NodeClassDefinition extends DefinitionBase<ClassRef>, PropertyF
30
40
  readonly methods: MethodRecord;
31
41
  readonly icon?: string;
32
42
  readonly policies: NodeClassPolicies;
43
+ /**
44
+ * The tuples no two Nodes of this Class and of its active concrete descendants share while every
45
+ * Property is present; absent, none. A descendant is covered without declaring them again.
46
+ */
47
+ readonly unique?: readonly NodeUniqueTuple[];
33
48
  }
34
49
  export type EdgeClassDefinition = DefinitionBase<ClassRef> & PropertyFacet & EdgeContract & {
35
50
  readonly kind: 'edge';
@@ -1,9 +1,16 @@
1
- import type { ClassRef } from '../../../addressing/index.js';
1
+ import type { ClassRef, PropertyKey } from '../../../addressing/index.js';
2
2
  export type EdgeCount = '0..*' | '1..*' | '0..1' | '1';
3
3
  export type EdgeOrientation = 'directed' | 'undirected';
4
+ /**
5
+ * What deleting the Node at one endpoint does to an Edge: `restrict` requires the transition to
6
+ * delete the Edge explicitly; `detach` deletes the Edge with the Node.
7
+ */
8
+ export type EdgeLifetime = 'restrict' | 'detach';
4
9
  export interface EdgeEndpointBase {
5
10
  readonly role: string;
6
11
  readonly accepts: readonly ClassRef[];
12
+ /** Declared only when authored; otherwise derived (`edgeLifetimes`). */
13
+ readonly onDelete?: EdgeLifetime;
7
14
  }
8
15
  export interface DirectedSourceEndpoint extends EdgeEndpointBase {
9
16
  readonly outgoing: EdgeCount;
@@ -22,11 +29,22 @@ export interface UndirectedEdgeConstraints {
22
29
  readonly noSelf?: true;
23
30
  readonly acyclic?: never;
24
31
  }
32
+ /** A field a key tuple names: an endpoint of the Edge, its slug, or one of its Properties. */
33
+ export type EdgeUniqueField = 'source' | 'target' | 'slug' | {
34
+ readonly property: PropertyKey;
35
+ };
36
+ /**
37
+ * The fields one key of a directed Edge Class names, which no two Edges of the Classes it covers
38
+ * share while each of them is present: distinct, in canonical order (`compareEdgeUniqueFields`).
39
+ */
40
+ export type EdgeUniqueTuple = readonly [EdgeUniqueField, ...EdgeUniqueField[]];
41
+ /** The kind of a key field: an endpoint, the slug, or a Property whatever its key. */
42
+ export type EdgeUniqueFieldKind = 'source' | 'target' | 'slug' | 'property';
25
43
  /**
26
- * The fields one uniqueness tuple of a directed Edge Class names, which no two Edges of the Classes
27
- * it covers share: its source endpoint and its slug.
44
+ * The form of a key tuple: the kinds of its fields, each once, in canonical order. A backend
45
+ * announces the forms it enforces, so a tuple of a form it does not announce is never ignored.
28
46
  */
29
- export type EdgeUniqueTuple = readonly ['source', 'slug'];
47
+ export type EdgeUniqueForm = readonly [EdgeUniqueFieldKind, ...EdgeUniqueFieldKind[]];
30
48
  export interface DirectedEdgeContract {
31
49
  readonly orientation: 'directed';
32
50
  readonly endpoints: {
@@ -0,0 +1,134 @@
1
+ import type { Value as PortableValue } from '@astrale-os/kernel-dsl/value';
2
+ import type { ClassRef } from '../../addressing/index.js';
3
+ import type { OrderingKind } from '../value/ordering.js';
4
+ import type { ClassAncestry } from './inheritance.js';
5
+ import type { NodeUniqueForm, NodeUniqueTuple } from './model/class.js';
6
+ import type { EdgeUniqueField, EdgeUniqueForm, EdgeUniqueTuple } from './model/edge.js';
7
+ import { ClassKey } from '../../addressing/class-key.js';
8
+ /**
9
+ * Every form a key tuple may take, in canonical order: each nonempty set of field kinds. A Property
10
+ * kind stands for one Property or more, whatever their keys.
11
+ */
12
+ export declare const EDGE_UNIQUE_FORMS: readonly EdgeUniqueForm[];
13
+ /** The one form a Node key tuple takes: its fields are Properties, whatever their keys. */
14
+ export declare const NODE_UNIQUE_FORMS: readonly NodeUniqueForm[];
15
+ /**
16
+ * The forms of key tuples a backend enforces, by the kind of the Classes they constrain: a Node key
17
+ * reads Node Properties and an Edge key the fields of an Edge, so announcing one says nothing of
18
+ * the other.
19
+ */
20
+ export interface UniqueForms {
21
+ readonly edge: readonly EdgeUniqueForm[];
22
+ readonly node: readonly NodeUniqueForm[];
23
+ }
24
+ /** Every form of every kind, which a backend enforcing any key tuple announces. */
25
+ export declare const UNIQUE_FORMS: UniqueForms;
26
+ /** No form of either kind: a backend that announces none enforces no key tuple. */
27
+ export declare const NO_UNIQUE_FORMS: UniqueForms;
28
+ /** The fields of an Edge a key tuple reads; the slug is absent from an Edge without one. */
29
+ export interface EdgeUniqueFacts {
30
+ readonly source: string;
31
+ readonly target: string;
32
+ readonly slug?: string;
33
+ /** Its Property values by key; a Property it lacks is absent. */
34
+ readonly props: Readonly<Record<string, PortableValue>>;
35
+ }
36
+ /** The Properties of a Node a key tuple reads by key; a Property it lacks is absent. */
37
+ export interface NodeUniqueFacts {
38
+ readonly props: Readonly<Record<string, PortableValue>>;
39
+ }
40
+ /** The canonical order of fields: source, target, slug, then Properties by key. */
41
+ export declare function compareEdgeUniqueFields(left: EdgeUniqueField, right: EdgeUniqueField): number;
42
+ /** The canonical order of tuples: field by field, a tuple before every tuple it prefixes. */
43
+ export declare const compareEdgeUniqueTuples: (left: EdgeUniqueTuple, right: EdgeUniqueTuple) => number;
44
+ /**
45
+ * The tuple `value` spells, detached, or undefined when it spells none: a nonempty list of fields,
46
+ * distinct and in canonical order, so one set of fields has one spelling.
47
+ */
48
+ export declare function edgeUniqueTuple(value: unknown): EdgeUniqueTuple | undefined;
49
+ /** The canonical order of Node tuples: Property by Property, a tuple before every one it prefixes. */
50
+ export declare const compareNodeUniqueTuples: (left: NodeUniqueTuple, right: NodeUniqueTuple) => number;
51
+ /**
52
+ * The Node tuple `value` spells, detached, or undefined when it spells none: a nonempty list of
53
+ * Property keys, distinct and in code-point order, so one set of Properties has one spelling.
54
+ */
55
+ export declare function nodeUniqueTuple(value: unknown): NodeUniqueTuple | undefined;
56
+ /** The form of `NODE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
57
+ export declare function nodeUniqueForm(value: unknown): NodeUniqueForm | undefined;
58
+ /**
59
+ * Whether a backend announcing `forms` for Nodes enforces the Node tuple `value` spells: every
60
+ * Node tuple has the one Node form. False when `value` spells no tuple.
61
+ */
62
+ export declare function enforcesNodeUniqueTuple(forms: readonly NodeUniqueForm[], value: unknown): boolean;
63
+ /** The form of `tuple`: the kinds of its fields, each once. */
64
+ export declare function edgeUniqueFormOf(tuple: EdgeUniqueTuple): EdgeUniqueForm;
65
+ /** The form of `EDGE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
66
+ export declare function edgeUniqueForm(value: unknown): EdgeUniqueForm | undefined;
67
+ /**
68
+ * Whether a backend announcing `forms` enforces the tuple `value` spells: its form is one of them.
69
+ * False when `value` spells no tuple, so a tuple is never taken as enforced by mistake.
70
+ */
71
+ export declare function enforcesEdgeUniqueTuple(forms: readonly EdgeUniqueForm[], value: unknown): boolean;
72
+ /**
73
+ * Whether a Property may be a key field (D-S1): a single-kind scalar by its ordering facet, a
74
+ * string, number, boolean or instant, that no automatic program writes.
75
+ */
76
+ export declare function keyableProperty(ordering: OrderingKind | undefined, auto: unknown): boolean;
77
+ /**
78
+ * The values of the fields of `tuple` on `edge`, in order, or undefined when one of them is absent,
79
+ * so that an Edge without a slug or without a Property never collides.
80
+ */
81
+ export declare function edgeUniqueValues(tuple: EdgeUniqueTuple, edge: EdgeUniqueFacts): readonly PortableValue[] | undefined;
82
+ /**
83
+ * The value `edge` takes under `tuple`, the canonical JSON of the values of its fields, or
84
+ * undefined when one of them is absent: two distinct Edges of the Classes a tuple covers collide
85
+ * exactly when they take one value. Endpoints compare by NodeId, slugs by exact text and
86
+ * Properties by canonical Value.
87
+ */
88
+ export declare function edgeUniqueValue(tuple: EdgeUniqueTuple, edge: EdgeUniqueFacts): string | undefined;
89
+ /**
90
+ * The values of the Properties of `tuple` on `node`, in order, or undefined when one of them is
91
+ * absent, so that a Node without one of them never collides, like NULL in an SQL UNIQUE.
92
+ */
93
+ export declare function nodeUniqueValues(tuple: NodeUniqueTuple, node: NodeUniqueFacts): readonly PortableValue[] | undefined;
94
+ /**
95
+ * The value `node` takes under `tuple`, the canonical JSON of the values of its Properties, or
96
+ * undefined when one of them is absent: two distinct Nodes of the Classes a tuple covers collide
97
+ * exactly when they take one value. Properties compare by canonical Value, nothing normalized.
98
+ */
99
+ export declare function nodeUniqueValue(tuple: NodeUniqueTuple, node: NodeUniqueFacts): string | undefined;
100
+ /** A Class with the key tuples it declares, an Edge Class or a Node Class. */
101
+ type KeyedClass = ClassAncestry & {
102
+ readonly unique?: readonly (EdgeUniqueTuple | NodeUniqueTuple)[];
103
+ };
104
+ /** A key tuple together with the Class declaring it, whose descendants it covers. */
105
+ export interface DeclaredEdgeUniqueTuple {
106
+ readonly class: ClassKey;
107
+ readonly fields: EdgeUniqueTuple;
108
+ }
109
+ /** A Node key tuple together with the Class declaring it, whose descendants it covers. */
110
+ export interface DeclaredNodeUniqueTuple {
111
+ readonly class: ClassKey;
112
+ readonly fields: NodeUniqueTuple;
113
+ }
114
+ /**
115
+ * The tuples covering `candidate`: those it and each of its ancestors declare, each with the Class
116
+ * declaring it, ancestors found by `resolve`. A descendant never restates them.
117
+ */
118
+ export declare function coveringEdgeUniqueTuples<Declaration extends KeyedClass>(candidate: Declaration, resolve: (ref: ClassRef) => Declaration | undefined): readonly DeclaredEdgeUniqueTuple[];
119
+ /**
120
+ * The Node tuples covering `candidate`: those it and each of its ancestors declare, each with the
121
+ * Class declaring it, ancestors found by `resolve`. A descendant never restates them.
122
+ */
123
+ export declare function coveringNodeUniqueTuples<Declaration extends KeyedClass>(candidate: Declaration, resolve: (ref: ClassRef) => Declaration | undefined): readonly DeclaredNodeUniqueTuple[];
124
+ /**
125
+ * The keys `edge` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
126
+ * tuple, so two Edges collide exactly when they hold one key.
127
+ */
128
+ export declare function edgeUniqueKeys(tuples: readonly DeclaredEdgeUniqueTuple[], edge: EdgeUniqueFacts): readonly string[];
129
+ /**
130
+ * The keys `node` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
131
+ * tuple, so two Nodes collide exactly when they hold one key.
132
+ */
133
+ export declare function nodeUniqueKeys(tuples: readonly DeclaredNodeUniqueTuple[], node: NodeUniqueFacts): readonly string[];
134
+ export {};
@@ -0,0 +1,257 @@
1
+ import { json } from '@astrale-os/kernel-dsl/value';
2
+ import { ClassKey } from '../../addressing/class-key.js';
3
+ import { compareUnicode } from '../../addressing/ordering.js';
4
+ import { PropertyKey } from '../../addressing/property-key.js';
5
+ /** The kinds of key fields, in canonical order: the endpoints, the slug, then the Properties. */
6
+ const KINDS = Object.freeze([
7
+ 'source',
8
+ 'target',
9
+ 'slug',
10
+ 'property',
11
+ ]);
12
+ /** The canonical order of forms: kind by kind, a form before every form it prefixes. */
13
+ const compareForms = compareSequences(compareKinds);
14
+ /**
15
+ * Every form a key tuple may take, in canonical order: each nonempty set of field kinds. A Property
16
+ * kind stands for one Property or more, whatever their keys.
17
+ */
18
+ export const EDGE_UNIQUE_FORMS = Object.freeze(Array.from({ length: (1 << KINDS.length) - 1 }, (_, index) => Object.freeze(KINDS.filter((_, kind) => ((index + 1) & (1 << kind)) !== 0))).sort(compareForms));
19
+ /** The one form a Node key tuple takes: its fields are Properties, whatever their keys. */
20
+ export const NODE_UNIQUE_FORMS = Object.freeze([
21
+ Object.freeze(['property']),
22
+ ]);
23
+ /** Every form of every kind, which a backend enforcing any key tuple announces. */
24
+ export const UNIQUE_FORMS = Object.freeze({
25
+ edge: EDGE_UNIQUE_FORMS,
26
+ node: NODE_UNIQUE_FORMS,
27
+ });
28
+ /** No form of either kind: a backend that announces none enforces no key tuple. */
29
+ export const NO_UNIQUE_FORMS = Object.freeze({
30
+ edge: Object.freeze([]),
31
+ node: Object.freeze([]),
32
+ });
33
+ /** The kind of `field`. */
34
+ function kindOf(field) {
35
+ return typeof field === 'string' ? field : 'property';
36
+ }
37
+ function compareKinds(left, right) {
38
+ return KINDS.indexOf(left) - KINDS.indexOf(right);
39
+ }
40
+ /** The canonical order of fields: source, target, slug, then Properties by key. */
41
+ export function compareEdgeUniqueFields(left, right) {
42
+ return (compareKinds(kindOf(left), kindOf(right)) ||
43
+ (typeof left === 'string' || typeof right === 'string'
44
+ ? 0
45
+ : compareUnicode(left.property, right.property)));
46
+ }
47
+ /** The canonical order of tuples: field by field, a tuple before every tuple it prefixes. */
48
+ export const compareEdgeUniqueTuples = compareSequences(compareEdgeUniqueFields);
49
+ /** The field `value` spells, detached, or undefined when it spells none. */
50
+ function edgeUniqueField(value) {
51
+ if (value === 'source' || value === 'target' || value === 'slug')
52
+ return value;
53
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
54
+ return undefined;
55
+ const keys = Object.keys(value);
56
+ const property = value.property;
57
+ return keys.length === 1 && keys[0] === 'property' && PropertyKey.is(property)
58
+ ? Object.freeze({ property })
59
+ : undefined;
60
+ }
61
+ /**
62
+ * The tuple `value` spells, detached, or undefined when it spells none: a nonempty list of fields,
63
+ * distinct and in canonical order, so one set of fields has one spelling.
64
+ */
65
+ export function edgeUniqueTuple(value) {
66
+ if (!Array.isArray(value) || value.length === 0)
67
+ return undefined;
68
+ const fields = [];
69
+ for (const item of value) {
70
+ const field = edgeUniqueField(item);
71
+ if (field === undefined)
72
+ return undefined;
73
+ if (fields.length > 0 && compareEdgeUniqueFields(fields.at(-1), field) >= 0)
74
+ return undefined;
75
+ fields.push(field);
76
+ }
77
+ return Object.freeze(fields);
78
+ }
79
+ /** The canonical order of Node tuples: Property by Property, a tuple before every one it prefixes. */
80
+ export const compareNodeUniqueTuples = compareSequences(compareUnicode);
81
+ /**
82
+ * The Node tuple `value` spells, detached, or undefined when it spells none: a nonempty list of
83
+ * Property keys, distinct and in code-point order, so one set of Properties has one spelling.
84
+ */
85
+ export function nodeUniqueTuple(value) {
86
+ if (!Array.isArray(value) || value.length === 0)
87
+ return undefined;
88
+ const keys = [];
89
+ for (const key of value) {
90
+ if (!PropertyKey.is(key))
91
+ return undefined;
92
+ if (keys.length > 0 && compareUnicode(keys.at(-1), key) >= 0)
93
+ return undefined;
94
+ keys.push(key);
95
+ }
96
+ return Object.freeze(keys);
97
+ }
98
+ /** The form of `NODE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
99
+ export function nodeUniqueForm(value) {
100
+ return Array.isArray(value) && value.length === 1 && value[0] === 'property'
101
+ ? NODE_UNIQUE_FORMS[0]
102
+ : undefined;
103
+ }
104
+ /**
105
+ * Whether a backend announcing `forms` for Nodes enforces the Node tuple `value` spells: every
106
+ * Node tuple has the one Node form. False when `value` spells no tuple.
107
+ */
108
+ export function enforcesNodeUniqueTuple(forms, value) {
109
+ return forms.length > 0 && nodeUniqueTuple(value) !== undefined;
110
+ }
111
+ /** The form of `tuple`: the kinds of its fields, each once. */
112
+ export function edgeUniqueFormOf(tuple) {
113
+ const kinds = new Set(tuple.map(kindOf));
114
+ return EDGE_UNIQUE_FORMS.find((form) => form.length === kinds.size && form.every((kind) => kinds.has(kind)));
115
+ }
116
+ /** The form of `EDGE_UNIQUE_FORMS` that `value` spells, or undefined when it spells none. */
117
+ export function edgeUniqueForm(value) {
118
+ if (!Array.isArray(value))
119
+ return undefined;
120
+ return EDGE_UNIQUE_FORMS.find((form) => form.length === value.length && form.every((kind, index) => kind === value[index]));
121
+ }
122
+ /**
123
+ * Whether a backend announcing `forms` enforces the tuple `value` spells: its form is one of them.
124
+ * False when `value` spells no tuple, so a tuple is never taken as enforced by mistake.
125
+ */
126
+ export function enforcesEdgeUniqueTuple(forms, value) {
127
+ const tuple = edgeUniqueTuple(value);
128
+ if (tuple === undefined)
129
+ return false;
130
+ const form = edgeUniqueFormOf(tuple);
131
+ return forms.some((candidate) => compareForms(candidate, form) === 0);
132
+ }
133
+ /**
134
+ * Whether a Property may be a key field (D-S1): a single-kind scalar by its ordering facet, a
135
+ * string, number, boolean or instant, that no automatic program writes.
136
+ */
137
+ export function keyableProperty(ordering, auto) {
138
+ return ordering !== undefined && auto === undefined;
139
+ }
140
+ /**
141
+ * The values of the fields of `tuple` on `edge`, in order, or undefined when one of them is absent,
142
+ * so that an Edge without a slug or without a Property never collides.
143
+ */
144
+ export function edgeUniqueValues(tuple, edge) {
145
+ return keyValues(tuple, (field) => typeof field === 'string' ? edge[field] : propertyOf(edge.props, field.property));
146
+ }
147
+ /**
148
+ * The value `edge` takes under `tuple`, the canonical JSON of the values of its fields, or
149
+ * undefined when one of them is absent: two distinct Edges of the Classes a tuple covers collide
150
+ * exactly when they take one value. Endpoints compare by NodeId, slugs by exact text and
151
+ * Properties by canonical Value.
152
+ */
153
+ export function edgeUniqueValue(tuple, edge) {
154
+ return serialized(edgeUniqueValues(tuple, edge));
155
+ }
156
+ /**
157
+ * The values of the Properties of `tuple` on `node`, in order, or undefined when one of them is
158
+ * absent, so that a Node without one of them never collides, like NULL in an SQL UNIQUE.
159
+ */
160
+ export function nodeUniqueValues(tuple, node) {
161
+ return keyValues(tuple, (key) => propertyOf(node.props, key));
162
+ }
163
+ /**
164
+ * The value `node` takes under `tuple`, the canonical JSON of the values of its Properties, or
165
+ * undefined when one of them is absent: two distinct Nodes of the Classes a tuple covers collide
166
+ * exactly when they take one value. Properties compare by canonical Value, nothing normalized.
167
+ */
168
+ export function nodeUniqueValue(tuple, node) {
169
+ return serialized(nodeUniqueValues(tuple, node));
170
+ }
171
+ /**
172
+ * The tuples covering `candidate`: those it and each of its ancestors declare, each with the Class
173
+ * declaring it, ancestors found by `resolve`. A descendant never restates them.
174
+ */
175
+ export function coveringEdgeUniqueTuples(candidate, resolve) {
176
+ return coveringTuples(candidate, resolve);
177
+ }
178
+ /**
179
+ * The Node tuples covering `candidate`: those it and each of its ancestors declare, each with the
180
+ * Class declaring it, ancestors found by `resolve`. A descendant never restates them.
181
+ */
182
+ export function coveringNodeUniqueTuples(candidate, resolve) {
183
+ return coveringTuples(candidate, resolve);
184
+ }
185
+ /**
186
+ * The keys `edge` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
187
+ * tuple, so two Edges collide exactly when they hold one key.
188
+ */
189
+ export function edgeUniqueKeys(tuples, edge) {
190
+ return heldKeys(tuples, (fields) => edgeUniqueValue(fields, edge));
191
+ }
192
+ /**
193
+ * The keys `node` holds: its value under each tuple of `tuples`, tagged by the Class declaring the
194
+ * tuple, so two Nodes collide exactly when they hold one key.
195
+ */
196
+ export function nodeUniqueKeys(tuples, node) {
197
+ return heldKeys(tuples, (fields) => nodeUniqueValue(fields, node));
198
+ }
199
+ /** The values `read` gives the fields of `tuple`, in order, or undefined when one is absent. */
200
+ function keyValues(tuple, read) {
201
+ const values = [];
202
+ for (const field of tuple) {
203
+ const value = read(field);
204
+ if (value === undefined)
205
+ return undefined;
206
+ values.push(value);
207
+ }
208
+ return Object.freeze(values);
209
+ }
210
+ /** The value of the Property `key` in `props`, or undefined when it is absent. */
211
+ function propertyOf(props, key) {
212
+ return Object.hasOwn(props, key) ? props[key] : undefined;
213
+ }
214
+ /** The canonical JSON of a key's values, or undefined when it has none. */
215
+ function serialized(values) {
216
+ return values === undefined ? undefined : json.serializeAdmitted(values);
217
+ }
218
+ /**
219
+ * The tuples `candidate` and its ancestors declare, each with its declarer. Only ancestors of the
220
+ * candidate's kind are visited, so every tuple is of that kind.
221
+ */
222
+ function coveringTuples(candidate, resolve) {
223
+ const tuples = [];
224
+ const seen = new Set();
225
+ const visit = (value) => {
226
+ if (seen.has(value) || value.kind !== candidate.kind)
227
+ return;
228
+ seen.add(value);
229
+ for (const fields of value.unique ?? [])
230
+ tuples.push(Object.freeze({ class: ClassKey.of(value.ref), fields }));
231
+ for (const ref of value.extends) {
232
+ const parent = resolve(ref);
233
+ if (parent !== undefined)
234
+ visit(parent);
235
+ }
236
+ };
237
+ visit(candidate);
238
+ return Object.freeze(tuples);
239
+ }
240
+ /** The keys a fact holds under `tuples`, its value under each tagged by the declaring Class. */
241
+ function heldKeys(tuples, valueOf) {
242
+ return tuples.flatMap(({ class: declaring, fields }) => {
243
+ const value = valueOf(fields);
244
+ return value === undefined ? [] : [JSON.stringify([declaring, fields, value])];
245
+ });
246
+ }
247
+ /** The lexicographic order of sequences by `compare`, a sequence before every one it prefixes. */
248
+ function compareSequences(compare) {
249
+ return (left, right) => {
250
+ for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
251
+ const order = compare(left[index], right[index]);
252
+ if (order !== 0)
253
+ return order;
254
+ }
255
+ return left.length - right.length;
256
+ };
257
+ }