@astrale-os/kernel-dsl 0.2.0-beta.14 → 0.2.0-beta.15

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.
@@ -53,6 +53,7 @@ export interface EdgeClassOptions {
53
53
  readonly extends?: readonly EdgeClassReferenceInput[];
54
54
  readonly properties?: PropertyRecordInput;
55
55
  readonly policies?: {
56
+ readonly read?: Lazy<Policy>;
56
57
  readonly traverse?: Lazy<Policy>;
57
58
  };
58
59
  readonly constraints?: {
@@ -114,7 +114,7 @@ function validateEdgeClassOptions(value) {
114
114
  'constraints',
115
115
  ]);
116
116
  if (value.policies !== undefined)
117
- assertKnownKeys(value.policies, ['traverse']);
117
+ assertKnownKeys(value.policies, ['read', 'traverse']);
118
118
  if (value.constraints !== undefined)
119
119
  assertKnownKeys(value.constraints, ['noSelf', 'acyclic']);
120
120
  }
@@ -128,7 +128,7 @@ function validateUndirectedEdgeClassOptions(value) {
128
128
  'constraints',
129
129
  ]);
130
130
  if (value.policies !== undefined)
131
- assertKnownKeys(value.policies, ['traverse']);
131
+ assertKnownKeys(value.policies, ['read', 'traverse']);
132
132
  if (value.constraints !== undefined)
133
133
  assertKnownKeys(value.constraints, ['noSelf']);
134
134
  }
@@ -76,11 +76,16 @@ export class V1DefinitionCompiler {
76
76
  extends: this.parents(input.extends, 'edge', `${pointer}/extends`),
77
77
  properties: facet,
78
78
  ...contract,
79
- policies: input.policies?.traverse === undefined
80
- ? {}
81
- : {
82
- traverse: this.policies.ref(input.policies.traverse, `${pointer}/policies/traverse`),
83
- },
79
+ policies: {
80
+ ...(input.policies?.read === undefined
81
+ ? {}
82
+ : { read: this.policies.ref(input.policies.read, `${pointer}/policies/read`) }),
83
+ ...(input.policies?.traverse === undefined
84
+ ? {}
85
+ : {
86
+ traverse: this.policies.ref(input.policies.traverse, `${pointer}/policies/traverse`),
87
+ }),
88
+ },
84
89
  };
85
90
  }
86
91
  endpointBase(value, pointer) {
@@ -166,7 +166,7 @@ function compiledClass(input, values, path) {
166
166
  if (value.data !== undefined)
167
167
  dataDefinition(value.data, `${path}/data`);
168
168
  boundedString(value.icon, 1, 8000, 'Class icon', `${path}/icon`);
169
- classPolicies(value.policies, true, `${path}/policies`);
169
+ classPolicies(value.policies, `${path}/policies`);
170
170
  const methodsPath = `${path}/methods`;
171
171
  const methods = array(value.methods, 'Class methods', methodsPath);
172
172
  for (const [index, method] of methods.entries())
@@ -174,7 +174,7 @@ function compiledClass(input, values, path) {
174
174
  unique(methods.map((method, index) => string(object(method, 'Compiled Method', `${methodsPath}/${index}`).name, 'Method name', `${methodsPath}/${index}/name`)), 'Class Methods', methodsPath);
175
175
  }
176
176
  else {
177
- classPolicies(value.policies, false, `${path}/policies`);
177
+ classPolicies(value.policies, `${path}/policies`);
178
178
  edgeContract(value, path);
179
179
  }
180
180
  return value;
@@ -313,9 +313,9 @@ function propertyAuto(input, path) {
313
313
  if (value.set !== 'instant')
314
314
  invalid('Property auto setter', undefined, `${path}/set`);
315
315
  }
316
- function classPolicies(input, node, path) {
316
+ function classPolicies(input, path) {
317
317
  const value = object(input, 'Class Policies', path);
318
- exact(value, [], 'Class Policies', path, node ? ['read', 'traverse'] : ['traverse']);
318
+ exact(value, [], 'Class Policies', path, ['read', 'traverse']);
319
319
  if (value.read !== undefined)
320
320
  definitionRef(value.read, 'policy', 'Class read Policy', `${path}/read`);
321
321
  if (value.traverse !== undefined)
@@ -248,7 +248,29 @@ function edgeRefines(context, child, parent) {
248
248
  return false;
249
249
  const refine = (a, b) => endpointRefines(context, child.endpoints[a], parent.endpoints[b]) &&
250
250
  countRefines(child.endpoints[a].incident, parent.endpoints[b].incident);
251
- return (refine(0, 0) && refine(1, 1)) || (refine(0, 1) && refine(1, 0));
251
+ const direct = refine(0, 0) && refine(1, 1);
252
+ if (direct)
253
+ return true;
254
+ const reverse = refine(0, 1) && refine(1, 0);
255
+ return reverse && !hasEffectiveEdgePolicy(context, parent, new Set());
256
+ }
257
+ function hasEffectiveEdgePolicy(context, declaration, visited) {
258
+ if (declaration.policies.read !== undefined || declaration.policies.traverse !== undefined) {
259
+ return true;
260
+ }
261
+ for (const parentRef of declaration.extends) {
262
+ const key = Key.of(parentRef);
263
+ if (visited.has(key))
264
+ continue;
265
+ visited.add(key);
266
+ const parent = context.classes.get(key)?.value;
267
+ if (parent !== undefined &&
268
+ isCompiledEdge(parent) &&
269
+ hasEffectiveEdgePolicy(context, parent, visited)) {
270
+ return true;
271
+ }
272
+ }
273
+ return false;
252
274
  }
253
275
  function endpointRefines(context, child, parent) {
254
276
  return child.accepts.every((accepted) => classSatisfiesAny(context, accepted, parent.accepts));
@@ -502,16 +524,15 @@ function validatePolicyParity(context, entry, entryIndex, policies) {
502
524
  }
503
525
  }
504
526
  for (const [classIndex, definition] of entry.definitions.classes.entries()) {
505
- if (definition.kind === 'node' && definition.policies.read !== undefined) {
506
- if (!typing.policyAccepts(definition.policies.read.name, { kind: 'exact', ref: definition.ref })) {
507
- fail(`/closure/${entryIndex}/definitions/classes/${classIndex}/policies/read`, 'Read Policy is incompatible with its protected Node Class.');
508
- }
509
- }
510
- const traverse = definition.policies.traverse;
511
- if (traverse !== undefined) {
527
+ for (const operation of ['read', 'traverse']) {
528
+ const policy = definition.policies[operation];
529
+ if (policy === undefined)
530
+ continue;
531
+ const path = `/closure/${entryIndex}/definitions/classes/${classIndex}/policies/${operation}`;
532
+ const label = operation === 'read' ? 'Read' : 'Traverse';
512
533
  if (definition.kind === 'node') {
513
- if (!typing.policyAccepts(traverse.name, { kind: 'exact', ref: definition.ref })) {
514
- fail(`/closure/${entryIndex}/definitions/classes/${classIndex}/policies/traverse`, 'Traverse Policy is incompatible with its protected Node Class.');
534
+ if (!typing.policyAccepts(policy.name, { kind: 'exact', ref: definition.ref })) {
535
+ fail(path, `${label} Policy is incompatible with its protected Node Class.`);
515
536
  }
516
537
  }
517
538
  else if (isCompiledEdge(definition)) {
@@ -520,8 +541,8 @@ function validatePolicyParity(context, entry, entryIndex, policies) {
520
541
  : definition.endpoints;
521
542
  const sources = typing.concreteEndpointClasses(endpoints[0].accepts);
522
543
  const targets = typing.concreteEndpointClasses(endpoints[1].accepts);
523
- if (sources.some((source) => targets.some((target) => !typing.policyAcceptsEdge(traverse.name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })))) {
524
- fail(`/closure/${entryIndex}/definitions/classes/${classIndex}/policies/traverse`, 'Traverse Policy is incompatible with an accepted Edge endpoint pair.');
544
+ if (sources.some((source) => targets.some((target) => !typing.policyAcceptsEdge(policy.name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })))) {
545
+ fail(path, `${label} Policy is incompatible with an accepted Edge endpoint pair.`);
525
546
  }
526
547
  }
527
548
  }
@@ -25,12 +25,10 @@ export function createClasses(definitions, validation) {
25
25
  }
26
26
  const shell = {};
27
27
  const policies = Object.freeze({
28
- read: Object.freeze(definition.kind === 'node'
29
- ? uniquePolicyRefs([
30
- ...parents.flatMap((parent) => parent.kind === 'node' ? [...parent.policies.read] : []),
31
- ...(definition.policies.read === undefined ? [] : [definition.policies.read]),
32
- ])
33
- : []),
28
+ read: Object.freeze(uniquePolicyRefs([
29
+ ...parents.flatMap((parent) => [...parent.policies.read]),
30
+ ...(definition.policies.read === undefined ? [] : [definition.policies.read]),
31
+ ])),
34
32
  traverse: Object.freeze(uniquePolicyRefs([
35
33
  ...parents.flatMap((parent) => [...parent.policies.traverse]),
36
34
  ...(definition.policies.traverse === undefined ? [] : [definition.policies.traverse]),
@@ -9,6 +9,7 @@ export interface NodeClassPolicies {
9
9
  readonly traverse?: PolicyRef;
10
10
  }
11
11
  export interface EdgeClassPolicies {
12
+ readonly read?: PolicyRef;
12
13
  readonly traverse?: PolicyRef;
13
14
  }
14
15
  export interface NodeClassDefinition extends DefinitionBase<ClassRef>, PropertyFacet, DataFacet {
@@ -23,8 +23,9 @@ export interface ResolvedData {
23
23
  readonly owner: ResolvedClass<'node'>;
24
24
  readonly definition: DataDefinition;
25
25
  }
26
- export interface ResolvedClassPolicies<Kind extends ClassKind = ClassKind> {
27
- readonly read: Kind extends 'node' ? readonly PolicyRef[] : readonly [];
26
+ export interface ResolvedClassPolicies {
27
+ /** Effective read Policies for this Node or Edge Class. */
28
+ readonly read: readonly PolicyRef[];
28
29
  readonly traverse: readonly PolicyRef[];
29
30
  }
30
31
  export type ResolvedPropertyCollection<ByKey extends Readonly<Record<PropertyKey, ResolvedProperty<unknown>>> = Readonly<Record<PropertyKey, ResolvedProperty<unknown>>>, ByName extends Readonly<Record<string, ResolvedProperty<unknown>>> = Readonly<Record<never, never>>> = Readonly<ByName> & Iterable<ByKey[keyof ByKey]> & {
@@ -45,7 +46,7 @@ export interface ResolvedClass<Kind extends ClassKind = ClassKind, Properties ex
45
46
  readonly definition: Definition;
46
47
  readonly extends: Parents & readonly ResolvedClass<Kind>[];
47
48
  /** Effective inherited Policy conjunction, deduplicated by exact Policy ref. */
48
- readonly policies: ResolvedClassPolicies<Kind>;
49
+ readonly policies: ResolvedClassPolicies;
49
50
  readonly data: Kind extends 'node' ? ResolvedData | undefined : undefined;
50
51
  readonly properties: Properties;
51
52
  readonly methods: Kind extends 'node' ? InstanceMethods : ResolvedMethodCollection<Record<never, never>>;
@@ -40,17 +40,15 @@ export function collectDomainDefinitionRefs(schema) {
40
40
  export function collectClassRefs(declaration, add) {
41
41
  declaration.extends.forEach(add);
42
42
  Object.values(declaration.properties).forEach((property) => collectValueDefinitionRefs(property.schema, add));
43
+ if (declaration.policies.read !== undefined)
44
+ add(declaration.policies.read);
45
+ if (declaration.policies.traverse !== undefined)
46
+ add(declaration.policies.traverse);
43
47
  if (declaration.kind === 'edge') {
44
48
  collectEndpointRefs(declaration, add);
45
- if (declaration.policies.traverse !== undefined)
46
- add(declaration.policies.traverse);
47
49
  return;
48
50
  }
49
51
  Object.values(declaration.methods).forEach((method) => collectCallableRefs(method, add));
50
- if (declaration.policies.read !== undefined)
51
- add(declaration.policies.read);
52
- if (declaration.policies.traverse !== undefined)
53
- add(declaration.policies.traverse);
54
52
  }
55
53
  function collectEndpointRefs(declaration, add) {
56
54
  const endpoints = declaration.orientation === 'directed'
@@ -130,7 +130,30 @@ function edgeContractRefines(environment, child, parent) {
130
130
  return false;
131
131
  const refines = (childIndex, parentIndex) => endpointRefines(environment, child.endpoints[childIndex], parent.endpoints[parentIndex]) &&
132
132
  countRefines(child.endpoints[childIndex].incident, parent.endpoints[parentIndex].incident);
133
- return (refines(0, 0) && refines(1, 1)) || (refines(0, 1) && refines(1, 0));
133
+ const direct = refines(0, 0) && refines(1, 1);
134
+ if (direct)
135
+ return true;
136
+ const reverse = refines(0, 1) && refines(1, 0);
137
+ return reverse && !hasEffectiveEdgePolicy(environment, parent, new Set());
138
+ }
139
+ function hasEffectiveEdgePolicy(environment, declaration, visited) {
140
+ if (declaration.policies.read !== undefined || declaration.policies.traverse !== undefined) {
141
+ return true;
142
+ }
143
+ for (const parentRef of declaration.extends) {
144
+ const key = definitionRefIndex(parentRef);
145
+ if (visited.has(key))
146
+ continue;
147
+ visited.add(key);
148
+ const parent = environment.resolveAny(parentRef);
149
+ if (parent !== undefined &&
150
+ parent.ref.kind === 'class' &&
151
+ parent.declaration.kind === 'edge' &&
152
+ hasEffectiveEdgePolicy(environment, parent.declaration, visited)) {
153
+ return true;
154
+ }
155
+ }
156
+ return false;
134
157
  }
135
158
  function endpointRefines(environment, child, parent) {
136
159
  return child.accepts.every((accepted) => classSatisfies(environment, accepted, parent.accepts));
@@ -203,6 +203,7 @@ const nodeClassPoliciesSchema = z.strictObject({
203
203
  traverse: policyRefSchema.optional(),
204
204
  });
205
205
  const edgeClassPoliciesSchema = z.strictObject({
206
+ read: policyRefSchema.optional(),
206
207
  traverse: policyRefSchema.optional(),
207
208
  });
208
209
  const nodeClassSchema = z.strictObject({
@@ -12,29 +12,17 @@ const PROJECTION_FUNCTION = nodeClassRef('Function');
12
12
  const PROJECTION_POLICY = nodeClassRef('Policy');
13
13
  const PROJECTION_VIEW = nodeClassRef('View');
14
14
  export function validateReadUses(context, typing) {
15
- for (const [name, declaration] of Object.entries(context.environment.candidate.classes)) {
16
- if (declaration.kind !== 'node' || declaration.policies.read === undefined) {
17
- continue;
18
- }
19
- const pointer = `/classes/${escapePointer(name)}/policies/read`;
20
- const ref = declaration.policies.read;
21
- if (!context.resolvePolicy(ref, pointer))
22
- continue;
23
- const object = {
24
- kind: 'exact',
25
- ref: { origin: context.environment.candidate.origin, kind: 'class', name },
26
- };
27
- if (!typing.policyAccepts(ref.name, object)) {
28
- context.add('PL_USE', pointer, `${formatDefinitionRef(ref)} is not valid for every normalized branch of protected Class ${name}.`, formatDefinitionRef(ref));
29
- }
30
- }
15
+ validateClassPolicyUses(context, typing, 'read');
31
16
  }
32
17
  export function validateTraverseUses(context, typing) {
18
+ validateClassPolicyUses(context, typing, 'traverse');
19
+ }
20
+ function validateClassPolicyUses(context, typing, operation) {
33
21
  for (const [name, declaration] of Object.entries(context.environment.candidate.classes)) {
34
- const ref = declaration.policies.traverse;
22
+ const ref = declaration.policies[operation];
35
23
  if (ref === undefined)
36
24
  continue;
37
- const pointer = `/classes/${escapePointer(name)}/policies/traverse`;
25
+ const pointer = `/classes/${escapePointer(name)}/policies/${operation}`;
38
26
  if (!context.resolvePolicy(ref, pointer))
39
27
  continue;
40
28
  if (declaration.kind === 'node') {
@@ -42,9 +30,9 @@ export function validateTraverseUses(context, typing) {
42
30
  kind: 'exact',
43
31
  ref: { origin: context.environment.candidate.origin, kind: 'class', name },
44
32
  };
45
- if (!typing.policyAccepts(ref.name, object)) {
46
- context.add('PL_USE', pointer, `${formatDefinitionRef(ref)} is not valid for every normalized branch of traversed Node Class ${name}.`, formatDefinitionRef(ref));
47
- }
33
+ if (typing.policyAccepts(ref.name, object))
34
+ continue;
35
+ context.add('PL_USE', pointer, `${formatDefinitionRef(ref)} is not valid for every normalized branch of ${operation} Node Class ${name}.`, formatDefinitionRef(ref));
48
36
  continue;
49
37
  }
50
38
  const endpoints = declaration.orientation === 'directed'
@@ -52,19 +40,9 @@ export function validateTraverseUses(context, typing) {
52
40
  : declaration.endpoints;
53
41
  const sources = typing.concreteEndpointClasses(endpoints[0].accepts);
54
42
  const targets = typing.concreteEndpointClasses(endpoints[1].accepts);
55
- let accepted = true;
56
- for (const source of sources) {
57
- for (const target of targets) {
58
- if (!typing.policyAcceptsEdge(ref.name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })) {
59
- accepted = false;
60
- break;
61
- }
62
- }
63
- if (!accepted)
64
- break;
65
- }
43
+ const accepted = sources.every((source) => targets.every((target) => typing.policyAcceptsEdge(ref.name, { kind: 'exact', ref: source }, { kind: 'exact', ref: target })));
66
44
  if (!accepted) {
67
- context.add('PL_USE', pointer, `${formatDefinitionRef(ref)} is not valid for every concrete source/target pair of traversed Edge Class ${name}.`, formatDefinitionRef(ref));
45
+ context.add('PL_USE', pointer, `${formatDefinitionRef(ref)} is not valid for every concrete source/target pair of ${operation} Edge Class ${name}.`, formatDefinitionRef(ref));
68
46
  }
69
47
  }
70
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-dsl",
3
- "version": "0.2.0-beta.14",
3
+ "version": "0.2.0-beta.15",
4
4
  "description": "Astrale Kernel DSL - Schema authoring, inference, and IR serialization",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {