@astrale-os/sdk 0.6.0-beta.41 → 0.6.0-beta.43

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 (177) hide show
  1. package/README.md +22 -2
  2. package/dist/application/action/context.d.ts +49 -18
  3. package/dist/application/action/index.d.ts +1 -1
  4. package/dist/application/error/error.d.ts +16 -1
  5. package/dist/application/error/error.js +39 -5
  6. package/dist/application/error/index.d.ts +1 -1
  7. package/dist/application/error/index.js +1 -1
  8. package/dist/application/mutation/authoring/builder.d.ts +81 -33
  9. package/dist/application/mutation/authoring/builder.js +100 -30
  10. package/dist/application/mutation/authoring/guard.d.ts +37 -0
  11. package/dist/application/mutation/authoring/guard.js +150 -0
  12. package/dist/application/mutation/authoring/index.d.ts +5 -2
  13. package/dist/application/mutation/authoring/index.js +2 -1
  14. package/dist/application/mutation/authoring/names.d.ts +19 -0
  15. package/dist/application/mutation/authoring/names.js +83 -0
  16. package/dist/application/mutation/authoring/transition.d.ts +3 -2
  17. package/dist/application/mutation/define.d.ts +24 -10
  18. package/dist/application/mutation/define.js +49 -53
  19. package/dist/application/mutation/execution/execute.d.ts +2 -2
  20. package/dist/application/mutation/execution/execute.js +78 -7
  21. package/dist/application/mutation/execution/executor.d.ts +4 -1
  22. package/dist/application/mutation/execution/executor.js +6 -1
  23. package/dist/application/mutation/failure.d.ts +11 -1
  24. package/dist/application/mutation/failure.js +18 -3
  25. package/dist/application/mutation/index.d.ts +8 -4
  26. package/dist/application/mutation/index.js +5 -2
  27. package/dist/application/mutation/legacy/v6/builder.d.ts +84 -0
  28. package/dist/application/mutation/legacy/v6/define.d.ts +18 -0
  29. package/dist/application/mutation/legacy/v6/define.js +56 -0
  30. package/dist/application/mutation/legacy/v6/mutation.d.ts +28 -0
  31. package/dist/application/mutation/legacy/v6/mutation.js +1 -0
  32. package/dist/application/mutation/{authoring → legacy/v6}/precondition.d.ts +4 -0
  33. package/dist/application/mutation/mutation.d.ts +30 -19
  34. package/dist/application/query/answer/answer.d.ts +117 -0
  35. package/dist/application/query/answer/answer.js +1 -0
  36. package/dist/application/query/answer/conformance.d.ts +4 -0
  37. package/dist/application/query/answer/conformance.js +27 -0
  38. package/dist/application/query/answer/facts.d.ts +17 -0
  39. package/dist/application/query/answer/facts.js +57 -0
  40. package/dist/application/query/composite/builder.d.ts +1 -1
  41. package/dist/application/query/composite/builder.js +1 -1
  42. package/dist/application/query/composite/plan.d.ts +1 -1
  43. package/dist/application/query/composite/plan.js +1 -1
  44. package/dist/application/query/composite/query.d.ts +1 -2
  45. package/dist/application/query/define.d.ts +2 -33
  46. package/dist/application/query/define.js +3 -130
  47. package/dist/application/query/definition.d.ts +12 -0
  48. package/dist/application/query/definition.js +47 -0
  49. package/dist/application/query/execution/bounds.d.ts +22 -0
  50. package/dist/application/query/execution/bounds.js +88 -0
  51. package/dist/application/query/execution/crossings.d.ts +25 -0
  52. package/dist/application/query/execution/crossings.js +192 -0
  53. package/dist/application/query/execution/error.d.ts +68 -0
  54. package/dist/application/query/execution/error.js +50 -0
  55. package/dist/application/query/execution/execute.d.ts +2 -7
  56. package/dist/application/query/execution/execute.js +3 -51
  57. package/dist/application/query/execution/recipe.d.ts +85 -0
  58. package/dist/application/query/execution/recipe.js +324 -0
  59. package/dist/application/query/execution/refusal.d.ts +37 -0
  60. package/dist/application/query/execution/refusal.js +80 -0
  61. package/dist/application/query/execution/rows.d.ts +42 -0
  62. package/dist/application/query/execution/rows.js +308 -0
  63. package/dist/application/query/execution/split.d.ts +24 -0
  64. package/dist/application/query/execution/split.js +190 -0
  65. package/dist/application/query/handles/build.d.ts +10 -0
  66. package/dist/application/query/handles/build.js +53 -0
  67. package/dist/application/query/handles/conditions.d.ts +94 -0
  68. package/dist/application/query/handles/conditions.js +1 -0
  69. package/dist/application/query/handles/domain.d.ts +157 -0
  70. package/dist/application/query/handles/domain.js +1 -0
  71. package/dist/application/query/handles/handles.d.ts +268 -0
  72. package/dist/application/query/handles/handles.js +1 -0
  73. package/dist/application/query/handles/model.d.ts +96 -0
  74. package/dist/application/query/handles/model.js +246 -0
  75. package/dist/application/query/handles/plan.d.ts +99 -0
  76. package/dist/application/query/handles/plan.js +37 -0
  77. package/dist/application/query/handles/reads.d.ts +106 -0
  78. package/dist/application/query/handles/reads.js +839 -0
  79. package/dist/application/query/handles/relations.d.ts +100 -0
  80. package/dist/application/query/handles/relations.js +1 -0
  81. package/dist/application/query/handles/rows.d.ts +94 -0
  82. package/dist/application/query/handles/rows.js +4 -0
  83. package/dist/application/query/handles/terms.d.ts +103 -0
  84. package/dist/application/query/handles/terms.js +282 -0
  85. package/dist/application/query/index.d.ts +10 -7
  86. package/dist/application/query/index.js +9 -4
  87. package/dist/application/query/language/index.d.ts +2 -0
  88. package/dist/application/query/language/index.js +2 -0
  89. package/dist/application/query/language/query.d.ts +323 -0
  90. package/dist/application/query/language/query.js +2 -0
  91. package/dist/application/query/{collection → legacy/v6/collection}/identity.d.ts +2 -2
  92. package/dist/application/query/{collection → legacy/v6/collection}/metadata.d.ts +1 -1
  93. package/dist/application/query/{collection → legacy/v6/collection}/union.d.ts +3 -3
  94. package/dist/application/query/{collection → legacy/v6/collection}/union.js +1 -1
  95. package/dist/application/query/{conformance.d.ts → legacy/v6/conformance.d.ts} +3 -3
  96. package/dist/application/query/{conformance.js → legacy/v6/conformance.js} +1 -1
  97. package/dist/application/query/legacy/v6/define.d.ts +37 -0
  98. package/dist/application/query/legacy/v6/define.js +90 -0
  99. package/dist/application/query/legacy/v6/execution/execute.d.ts +16 -0
  100. package/dist/application/query/legacy/v6/execution/execute.js +50 -0
  101. package/dist/application/query/legacy/v6/index.d.ts +6 -0
  102. package/dist/application/query/legacy/v6/index.js +3 -0
  103. package/dist/application/query/legacy/v6/kernel.d.ts +6 -0
  104. package/dist/application/query/legacy/v6/kernel.js +5 -0
  105. package/dist/application/query/{single → legacy/v6/single}/pagination.js +1 -1
  106. package/dist/application/query/{single → legacy/v6/single}/query.d.ts +2 -14
  107. package/dist/application/query/legacy/v6/single/query.js +1 -0
  108. package/dist/application/query/query.d.ts +16 -1
  109. package/dist/application/query/recipe/define.d.ts +44 -0
  110. package/dist/application/query/recipe/define.js +95 -0
  111. package/dist/application/query/recipe/recipe.d.ts +134 -0
  112. package/dist/application/query/recipe/recipe.js +1 -0
  113. package/dist/application/workflow/durable.d.ts +22 -8
  114. package/dist/deployment/build/build.d.ts +8 -1
  115. package/dist/deployment/build/material.js +3 -0
  116. package/dist/execution/invocation/capabilities/bind.d.ts +6 -4
  117. package/dist/execution/invocation/capabilities/bind.js +39 -3
  118. package/dist/execution/invocation/dispatch/action.js +4 -2
  119. package/dist/execution/invocation/dispatch/context.js +1 -1
  120. package/dist/execution/invocation/dispatch/workflow.js +4 -2
  121. package/dist/execution/invocation/result/index.d.ts +1 -0
  122. package/dist/execution/invocation/result/index.js +1 -0
  123. package/dist/execution/invocation/result/recipe.d.ts +6 -0
  124. package/dist/execution/invocation/result/recipe.js +17 -0
  125. package/dist/execution/service/create.js +1 -0
  126. package/dist/execution/workflows/failure.d.ts +23 -6
  127. package/dist/execution/workflows/failure.js +78 -18
  128. package/dist/execution/workflows/receiver.d.ts +3 -0
  129. package/dist/execution/workflows/receiver.js +1 -0
  130. package/dist/execution/workflows/run.js +13 -9
  131. package/dist/execution/workflows/steps.d.ts +3 -0
  132. package/dist/execution/workflows/steps.js +2 -2
  133. package/dist/platform/schema/kernel-compiled.d.ts +2 -0
  134. package/dist/platform/schema/kernel-compiled.js +2 -0
  135. package/dist/testing/index.d.ts +2 -0
  136. package/dist/testing/index.js +1 -0
  137. package/dist/testing/recipe.d.ts +17 -0
  138. package/dist/testing/recipe.js +34 -0
  139. package/dist/tooling/cli/orchestrate.js +6 -2
  140. package/dist/tooling/linter/adapters/typescript/index.d.ts +2 -1
  141. package/dist/tooling/linter/adapters/typescript/index.js +2 -1
  142. package/dist/tooling/linter/adapters/typescript/query-observation.js +37 -12
  143. package/dist/tooling/linter/adapters/typescript/recipe-definitions.d.ts +34 -0
  144. package/dist/tooling/linter/adapters/typescript/recipe-definitions.js +88 -0
  145. package/dist/tooling/linter/adapters/typescript/type-analysis.d.ts +2 -0
  146. package/dist/tooling/linter/adapters/typescript/type-analysis.js +10 -0
  147. package/dist/tooling/linter/implementations/source/global.js +15 -9
  148. package/dist/tooling/linter/implementations/source/mutations.js +128 -19
  149. package/dist/tooling/linter/implementations/source/policy-edges.d.ts +2 -0
  150. package/dist/tooling/linter/implementations/source/policy-edges.js +113 -0
  151. package/dist/tooling/linter/implementations/source/recipes.d.ts +2 -0
  152. package/dist/tooling/linter/implementations/source/recipes.js +425 -0
  153. package/dist/tooling/linter/implementations/source/shared.d.ts +7 -1
  154. package/dist/tooling/linter/implementations/source/shared.js +31 -10
  155. package/dist/tooling/linter/implementations/source/verifiers.js +4 -0
  156. package/dist/tooling/linter/policy/generated.js +2 -2
  157. package/dist/tooling/linter/requirements/registry.js +4 -0
  158. package/docs/durable-workflows.md +48 -20
  159. package/docs/handler-dependencies.md +76 -4
  160. package/docs/legacy/query-v6.md +62 -0
  161. package/docs/queries.md +442 -0
  162. package/package.json +11 -8
  163. /package/dist/application/{query/single/query.js → mutation/legacy/v6/builder.js} +0 -0
  164. /package/dist/application/mutation/{authoring → legacy/v6}/precondition.js +0 -0
  165. /package/dist/application/query/{collection → legacy/v6/collection}/identity.js +0 -0
  166. /package/dist/application/query/{collection → legacy/v6/collection}/limits.d.ts +0 -0
  167. /package/dist/application/query/{collection → legacy/v6/collection}/limits.js +0 -0
  168. /package/dist/application/query/{collection → legacy/v6/collection}/metadata.js +0 -0
  169. /package/dist/application/query/{execution → legacy/v6/execution}/pages.d.ts +0 -0
  170. /package/dist/application/query/{execution → legacy/v6/execution}/pages.js +0 -0
  171. /package/dist/application/query/{single → legacy/v6/single}/index.d.ts +0 -0
  172. /package/dist/application/query/{single → legacy/v6/single}/index.js +0 -0
  173. /package/dist/application/query/{single → legacy/v6/single}/node.d.ts +0 -0
  174. /package/dist/application/query/{single → legacy/v6/single}/node.js +0 -0
  175. /package/dist/application/query/{single → legacy/v6/single}/pagination.d.ts +0 -0
  176. /package/dist/application/query/{single → legacy/v6/single}/selection.d.ts +0 -0
  177. /package/dist/application/query/{single → legacy/v6/single}/selection.js +0 -0
@@ -14,6 +14,7 @@ export const mutationRules = [
14
14
  const targets = [];
15
15
  for (const file of production(project, 'mutations')) {
16
16
  const builderScopes = mutationBuilderScopes(file);
17
+ const handles = handleParameters(file);
17
18
  visit(file.source, (node) => {
18
19
  if (!ts.isCallExpression(node))
19
20
  return;
@@ -25,11 +26,11 @@ export const mutationRules = [
25
26
  return;
26
27
  const input = staticObject(node.arguments[0]);
27
28
  const selectedClass = input && assignedValue(input, 'class');
28
- const target = selectedClass && dependencyClassTarget(selectedClass);
29
+ const target = selectedClass && dependencyClassTarget(selectedClass, handles);
29
30
  if (selectedClass !== undefined && target !== undefined) {
30
31
  targets.push({ file, node: selectedClass, ...target, operation: classOperation });
31
32
  }
32
- else if (selectedClass && expressionSelectsDependency(selectedClass)) {
33
+ else if (selectedClass && expressionSelectsDependency(selectedClass, handles)) {
33
34
  evidence.push(ambiguity(file, selectedClass, 'Mutation plan selects a foreign Class dynamically, so its Domain requirement cannot be proven.'));
34
35
  }
35
36
  });
@@ -97,6 +98,7 @@ export const mutationRules = [
97
98
  const machineProperties = machinePropertiesByClass(project);
98
99
  for (const file of production(project, 'mutations')) {
99
100
  const builderScopes = mutationBuilderScopes(file);
101
+ const handles = handleParameters(file);
100
102
  visit(file.source, (node) => {
101
103
  if (!ts.isCallExpression(node) ||
102
104
  mutationBuilderMember(node, builderScopes) !== 'createNode') {
@@ -109,7 +111,7 @@ export const mutationRules = [
109
111
  }
110
112
  return;
111
113
  }
112
- const selectedClass = propertiesForClass(input, machineProperties);
114
+ const selectedClass = propertiesForClass(input, machineProperties, handles);
113
115
  if (selectedClass.kind === 'ambiguous') {
114
116
  evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'Mutation createNode Class has no statically resolvable Schema owner.'));
115
117
  return;
@@ -170,6 +172,7 @@ export const mutationRules = [
170
172
  const machineProperties = machinePropertiesByClass(project);
171
173
  for (const file of production(project, 'mutations')) {
172
174
  const builderScopes = mutationBuilderScopes(file);
175
+ const handles = handleParameters(file);
173
176
  visit(file.source, (node) => {
174
177
  if (!ts.isCallExpression(node))
175
178
  return;
@@ -184,7 +187,7 @@ export const mutationRules = [
184
187
  evidence.push(ambiguity(file, node, 'Mutation updateNode input is dynamic and may bypass a StateMachine transition.'));
185
188
  return;
186
189
  }
187
- const selectedClass = propertiesForClass(input, machineProperties);
190
+ const selectedClass = propertiesForClass(input, machineProperties, handles);
188
191
  if (selectedClass.kind === 'ambiguous') {
189
192
  evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'Mutation updateNode Class has no statically resolvable Schema owner.'));
190
193
  return;
@@ -265,7 +268,7 @@ export const mutationRules = [
265
268
  evidence.push(violation(file, input, `StateMachine transition has no ${required} intent.`));
266
269
  }
267
270
  }
268
- const selectedClass = propertiesForClass(input, machineProperties);
271
+ const selectedClass = propertiesForClass(input, machineProperties, handles);
269
272
  if (selectedClass.kind === 'ambiguous') {
270
273
  evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'StateMachine transition Class has no statically resolvable Schema owner.'));
271
274
  }
@@ -305,8 +308,8 @@ export const mutationRules = [
305
308
  evidence.push(violation(file, object, 'Mutation definition has no build callback.'));
306
309
  continue;
307
310
  }
308
- if (build.parameters.length < 2) {
309
- evidence.push(violation(file, build, 'Mutation build has no callback-scoped builder as its second parameter.'));
311
+ if (builderParameter(definition, build) === undefined) {
312
+ evidence.push(violation(file, build, `Mutation build has no callback-scoped builder as its ${definition.id === undefined ? 'second' : 'third'} parameter.`));
310
313
  }
311
314
  if (returnedValues(build) > 0) {
312
315
  evidence.push(violation(file, build, 'Mutation build returns a value.'));
@@ -350,7 +353,7 @@ export const mutationRules = [
350
353
  const build = object && callback(object, 'build');
351
354
  if (!build)
352
355
  continue;
353
- const builderName = build.parameters[1]?.name;
356
+ const builderName = builderParameter(definition, build)?.name;
354
357
  if (!builderName || !ts.isIdentifier(builderName)) {
355
358
  evidence.push(ambiguity(file, build, 'Mutation build builder parameter is not an identifier.'));
356
359
  continue;
@@ -405,7 +408,7 @@ export const mutationRules = [
405
408
  const build = object && callback(object, 'build');
406
409
  if (!build)
407
410
  continue;
408
- const builderName = build.parameters[1]?.name;
411
+ const builderName = builderParameter(definition, build)?.name;
409
412
  visitOwnBody(build, (node) => {
410
413
  if (!ts.isCallExpression(node))
411
414
  return;
@@ -516,7 +519,7 @@ function mutationBuilderScopes(file) {
516
519
  if (definition.origin !== 'resolved')
517
520
  continue;
518
521
  const build = definition.object && callback(definition.object, 'build');
519
- const builder = build?.parameters[1]?.name;
522
+ const builder = build && builderParameter(definition, build)?.name;
520
523
  if (build && builder && ts.isIdentifier(builder))
521
524
  add(build, builder);
522
525
  }
@@ -537,7 +540,7 @@ function mutationBuilderScopes(file) {
537
540
  continue;
538
541
  }
539
542
  for (const [imported, local] of sourceImport.bindings) {
540
- if (imported === 'RichMutationBuilder')
543
+ if (imported === 'RichMutationBuilder' || imported === 'RecipeMutationBuilder')
541
544
  richBuilderTypes.add(local);
542
545
  }
543
546
  }
@@ -659,7 +662,14 @@ function classOperationForBuilder(operation) {
659
662
  return undefined;
660
663
  }
661
664
  }
662
- function dependencyClassTarget(expression) {
665
+ function dependencyClassTarget(expression, handles) {
666
+ // A recipe names a dependency's Class by its handle, `d.<alias>.<Class>`.
667
+ const path = handlePath(expression, handles);
668
+ if (path !== undefined) {
669
+ return path.length === 2 && !OWN_HANDLES.has(path[0])
670
+ ? { dependency: path[0], className: path[1] }
671
+ : undefined;
672
+ }
663
673
  const chain = staticPropertyChain(expression);
664
674
  if (!chain)
665
675
  return undefined;
@@ -670,10 +680,105 @@ function dependencyClassTarget(expression) {
670
680
  }
671
681
  return { dependency: suffix[dependency + 1], className: suffix[dependency + 3] };
672
682
  }
673
- function expressionSelectsDependency(expression) {
683
+ function expressionSelectsDependency(expression, handles) {
684
+ const root = firstIdentifier(unwrap(expression));
685
+ // A handle the source does not name statically, `d[alias]`, may be a dependency's.
686
+ if (root !== undefined && handlePath(root, handles) !== undefined)
687
+ return handlePath(expression, handles) === undefined;
674
688
  const chain = staticPropertyChain(expression);
675
689
  return chain?.includes('dependencies') ?? false;
676
690
  }
691
+ /** The handles of `d` that are not dependency aliases (S1). */
692
+ const OWN_HANDLES = new Set(['kernel', 'core', 'origin', 'document']);
693
+ function handleParameters(file) {
694
+ const parameters = new Set();
695
+ for (const definition of definitionObjects(file, 'defineMutation')) {
696
+ if (definition.origin !== 'resolved' || definition.id === undefined)
697
+ continue;
698
+ const build = definition.object && callback(definition.object, 'build');
699
+ const parameter = build?.parameters[1];
700
+ if (parameter !== undefined)
701
+ parameters.add(parameter);
702
+ }
703
+ const handleTypes = new Set();
704
+ for (const sourceImport of file.imports) {
705
+ if (sourceImport.specifier !== '@astrale-os/sdk' &&
706
+ sourceImport.specifier !== '@astrale-os/sdk/query')
707
+ continue;
708
+ for (const [imported, local] of sourceImport.bindings)
709
+ if (imported === 'Handles')
710
+ handleTypes.add(local);
711
+ }
712
+ if (handleTypes.size > 0)
713
+ visit(file.source, (node) => {
714
+ if (ts.isParameter(node) &&
715
+ node.type !== undefined &&
716
+ ts.isTypeReferenceNode(node.type) &&
717
+ ts.isIdentifier(node.type.typeName) &&
718
+ handleTypes.has(node.type.typeName.text))
719
+ parameters.add(node);
720
+ });
721
+ return parameters;
722
+ }
723
+ /**
724
+ * The handle a recipe's expression names, as its path from `d`: `d.Item` and a destructured
725
+ * `{ Item }` are `['Item']`, `d.directory.Person` is `['directory', 'Person']`. Undefined for an
726
+ * expression that does not start from the handles, or not statically.
727
+ */
728
+ function handlePath(expression, handles, seen = new Set()) {
729
+ const value = unwrap(expression);
730
+ if (handles.size === 0)
731
+ return undefined;
732
+ if (ts.isIdentifier(value) && !seen.has(value.text)) {
733
+ const initializer = visibleConstInitializer(value);
734
+ if (initializer !== undefined)
735
+ return handlePath(initializer, handles, new Set([...seen, value.text]));
736
+ }
737
+ const chain = propertyChain(value);
738
+ const root = firstIdentifier(value);
739
+ const binding = root && identifierDeclaration(root);
740
+ const prefix = binding && bindingPath(binding, handles, seen);
741
+ return chain && prefix && [...prefix, ...chain.slice(1)];
742
+ }
743
+ /**
744
+ * The path of a binding from the handles: `{ directory: { Person } }` binds `Person` at
745
+ * `['directory', 'Person']`, in a handles parameter or in a constant the handles initialize,
746
+ * `const { directory } = d`.
747
+ */
748
+ function bindingPath(binding, handles, seen) {
749
+ const path = [];
750
+ let current = binding;
751
+ while (ts.isBindingElement(current.parent)) {
752
+ const element = current.parent;
753
+ const name = element.propertyName ?? element.name;
754
+ if (!ts.isObjectBindingPattern(element.parent) ||
755
+ element.dotDotDotToken ||
756
+ !(ts.isIdentifier(name) || ts.isStringLiteral(name)))
757
+ return undefined;
758
+ path.unshift(name.text);
759
+ current = element.parent;
760
+ }
761
+ const declaration = current.parent;
762
+ if (declaration !== undefined &&
763
+ ts.isVariableDeclaration(declaration) &&
764
+ declaration.name === current &&
765
+ declaration.initializer !== undefined &&
766
+ (declaration.parent.flags & ts.NodeFlags.Const) !== 0 &&
767
+ !seen.has(binding.text)) {
768
+ const prefix = handlePath(declaration.initializer, handles, new Set([...seen, binding.text]));
769
+ return prefix && [...prefix, ...path];
770
+ }
771
+ return declaration !== undefined &&
772
+ ts.isParameter(declaration) &&
773
+ declaration.name === current &&
774
+ handles.has(declaration)
775
+ ? path
776
+ : undefined;
777
+ }
778
+ /** The builder a Mutation's `build` receives: second in the closure form, third in a recipe. */
779
+ function builderParameter(definition, build) {
780
+ return build.parameters[definition.id === undefined ? 1 : 2];
781
+ }
677
782
  function kernelProvisionCall(call) {
678
783
  const chain = propertyChain(call.expression);
679
784
  if (chain?.length === 4 &&
@@ -1180,9 +1285,9 @@ function containingVariableName(node) {
1180
1285
  }
1181
1286
  return undefined;
1182
1287
  }
1183
- function propertiesForClass(input, properties) {
1288
+ function propertiesForClass(input, properties, handles) {
1184
1289
  const selectedClass = assignedValue(input, 'class');
1185
- const selection = selectedClass && selectedClassNames(selectedClass);
1290
+ const selection = selectedClass && selectedClassNames(selectedClass, handles);
1186
1291
  if (selection === undefined || !closedClassSelection(selection, properties)) {
1187
1292
  return { kind: 'ambiguous' };
1188
1293
  }
@@ -1205,13 +1310,17 @@ function propertiesForClass(input, properties) {
1205
1310
  }
1206
1311
  return hasMachineProperty ? { kind: 'resolved', properties: resolved } : { kind: 'absent' };
1207
1312
  }
1208
- function selectedClassNames(expression, seen = new Set()) {
1313
+ function selectedClassNames(expression, handles, seen = new Set()) {
1209
1314
  const value = unwrap(expression);
1210
1315
  if (ts.isConditionalExpression(value)) {
1211
- const whenTrue = selectedClassNames(value.whenTrue, seen);
1212
- const whenFalse = selectedClassNames(value.whenFalse, seen);
1316
+ const whenTrue = selectedClassNames(value.whenTrue, handles, seen);
1317
+ const whenFalse = selectedClassNames(value.whenFalse, handles, seen);
1213
1318
  return whenTrue && whenFalse ? new Set([...whenTrue, ...whenFalse]) : undefined;
1214
1319
  }
1320
+ // A recipe names its Class by its handle: the last segment of its path from `d`.
1321
+ const path = handlePath(value, handles);
1322
+ if (path !== undefined)
1323
+ return path.length > 0 ? new Set([path.at(-1)]) : undefined;
1215
1324
  if (ts.isIdentifier(value)) {
1216
1325
  if (seen.has(value.text))
1217
1326
  return undefined;
@@ -1219,7 +1328,7 @@ function selectedClassNames(expression, seen = new Set()) {
1219
1328
  if (initializer !== undefined) {
1220
1329
  const next = new Set(seen);
1221
1330
  next.add(value.text);
1222
- return selectedClassNames(initializer, next);
1331
+ return selectedClassNames(initializer, handles, next);
1223
1332
  }
1224
1333
  }
1225
1334
  const chain = staticPropertyChain(value);
@@ -0,0 +1,2 @@
1
+ import type { SourceRuleVerifier } from './verifier.js';
2
+ export declare const policyEdgeRules: readonly SourceRuleVerifier[];
@@ -0,0 +1,113 @@
1
+ import ts from 'typescript';
2
+ import { declarationOf, unwrap, visit, } from '../../adapters/typescript/index.js';
3
+ import { production, propertyExpression, violation } from './shared.js';
4
+ export const policyEdgeRules = [
5
+ {
6
+ id: 'SCH-EDGE-KEY-ORDER',
7
+ ruleRevision: '7377f52e7387d618840c7a6d520b317c7eda63348c84b86cfc1e389fe5db10b5',
8
+ evaluate(project) {
9
+ return production(project, 'schema').flatMap((file) => edgeOrderEvidence(project, file));
10
+ },
11
+ },
12
+ ];
13
+ /**
14
+ * Each Policy `edge(Class, { [role]: term })` whose ends list another role before the source's. A
15
+ * Class whose source role is not statically visible, or ends that are not plain keys, prove nothing.
16
+ */
17
+ function edgeOrderEvidence(project, file) {
18
+ const evidence = [];
19
+ visit(file.source, (node) => {
20
+ if (!ts.isCallExpression(node) || !isPolicyEdge(node.expression))
21
+ return;
22
+ const [edgeClass, ends] = node.arguments;
23
+ const object = ends && unwrap(ends);
24
+ if (!edgeClass || !object || !ts.isObjectLiteralExpression(object))
25
+ return;
26
+ const roles = endRoles(object);
27
+ const source = roles && sourceRole(project, file, edgeClass);
28
+ if (!roles || source === undefined || roles[0] === source || !roles.includes(source))
29
+ return;
30
+ evidence.push(violation(file, object, `Policy edge(${edgeClass.getText(file.source)}, { ... }) lists ${roles[0]} first; write the source role ${source} first.`));
31
+ });
32
+ return evidence;
33
+ }
34
+ /** `edge` of a Policy context: destructured from a callback's parameter, or read off it. */
35
+ function isPolicyEdge(expression) {
36
+ const callee = unwrap(expression);
37
+ if (ts.isPropertyAccessExpression(callee))
38
+ return (callee.name.text === 'edge' &&
39
+ ts.isIdentifier(callee.expression) &&
40
+ bindingOf(callee.expression) === 'parameter');
41
+ return ts.isIdentifier(callee) && bindingOf(callee) === 'edge';
42
+ }
43
+ /** How the nearest enclosing callback binds a name: as a parameter, or as its `edge` member. */
44
+ function bindingOf(identifier) {
45
+ for (let current = identifier.parent; current; current = current.parent) {
46
+ if (!ts.isFunctionLike(current))
47
+ continue;
48
+ for (const { name } of current.parameters) {
49
+ if (ts.isIdentifier(name) && name.text === identifier.text)
50
+ return 'parameter';
51
+ if (!ts.isObjectBindingPattern(name))
52
+ continue;
53
+ for (const element of name.elements) {
54
+ if (!ts.isIdentifier(element.name) || element.name.text !== identifier.text)
55
+ continue;
56
+ const key = element.propertyName ?? element.name;
57
+ return ts.isIdentifier(key) && key.text === 'edge' ? 'edge' : undefined;
58
+ }
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ /** The keys of the ends, in the order written; `undefined` when one is not a plain key. */
64
+ function endRoles(object) {
65
+ const roles = [];
66
+ for (const property of object.properties) {
67
+ if (ts.isShorthandPropertyAssignment(property))
68
+ roles.push(property.name.text);
69
+ else if (ts.isPropertyAssignment(property) &&
70
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)))
71
+ roles.push(property.name.text);
72
+ else
73
+ return undefined;
74
+ }
75
+ return roles;
76
+ }
77
+ /**
78
+ * The source role of an Edge Class, read from its declaration: the `as` of a directed Class's
79
+ * source, or of an undirected Class's first endpoint, which lowers to the source.
80
+ */
81
+ function sourceRole(project, file, value) {
82
+ let reference = unwrap(value);
83
+ if (ts.isArrowFunction(reference) && !ts.isBlock(reference.body))
84
+ reference = unwrap(reference.body);
85
+ if (!ts.isIdentifier(reference))
86
+ return undefined;
87
+ const declaration = declarationOf(project, file, reference);
88
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer)
89
+ return undefined;
90
+ const call = unwrap(declaration.initializer);
91
+ if (!ts.isCallExpression(call))
92
+ return undefined;
93
+ const callee = unwrap(call.expression);
94
+ const input = call.arguments[0] && unwrap(call.arguments[0]);
95
+ if (!ts.isPropertyAccessExpression(callee) ||
96
+ !ts.isIdentifier(callee.expression) ||
97
+ callee.expression.text !== 'edgeClass' ||
98
+ !input ||
99
+ !ts.isObjectLiteralExpression(input))
100
+ return undefined;
101
+ if (callee.name.text === 'directed')
102
+ return roleOf(propertyExpression(input, 'source'));
103
+ if (callee.name.text !== 'undirected')
104
+ return undefined;
105
+ const endpoints = propertyExpression(input, 'endpoints');
106
+ return endpoints && ts.isArrayLiteralExpression(endpoints)
107
+ ? roleOf(endpoints.elements[0])
108
+ : undefined;
109
+ }
110
+ function roleOf(end) {
111
+ const role = end && ts.isObjectLiteralExpression(end) ? propertyExpression(end, 'as') : undefined;
112
+ return role && ts.isStringLiteralLike(role) ? role.text : undefined;
113
+ }
@@ -0,0 +1,2 @@
1
+ import type { SourceRuleVerifier } from './verifier.js';
2
+ export declare const recipeRules: readonly SourceRuleVerifier[];