@lorion-org/composition-graph 1.0.0-beta.0 → 1.0.0-beta.2
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/README.md +101 -39
- package/dist/index.cjs +71 -12
- package/dist/index.d.cts +16 -4
- package/dist/index.d.ts +16 -4
- package/dist/index.js +69 -11
- package/examples/deployment-composition.js +22 -15
- package/examples/deployment-composition.ts +29 -21
- package/package.json +8 -2
- package/src/compositionSelection.ts +109 -0
- package/src/descriptorCatalog.ts +163 -0
- package/src/descriptorGraph.ts +327 -0
- package/src/descriptorMap.ts +127 -0
- package/src/index.ts +38 -0
- package/src/types.ts +133 -0
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
import { createDescriptorCatalog } from '@lorion-org/composition-graph';
|
|
2
2
|
|
|
3
3
|
const catalog = createDescriptorCatalog({
|
|
4
|
-
|
|
4
|
+
descriptors: [
|
|
5
5
|
{
|
|
6
|
-
id: '
|
|
7
|
-
|
|
6
|
+
id: 'web',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
dependencies: {
|
|
9
|
+
checkout: '^1.0.0',
|
|
10
|
+
payments: '^1.0.0',
|
|
11
|
+
'payment-provider-invoice': '^1.0.0',
|
|
12
|
+
'payment-provider-stripe': '^1.0.0',
|
|
13
|
+
shops: '^1.0.0',
|
|
14
|
+
},
|
|
8
15
|
},
|
|
9
|
-
],
|
|
10
|
-
descriptors: [
|
|
11
16
|
{
|
|
12
|
-
id: '
|
|
17
|
+
id: 'checkout',
|
|
13
18
|
version: '1.0.0',
|
|
14
|
-
dependencies: {
|
|
15
|
-
integrations: { analytics: '*' },
|
|
19
|
+
dependencies: { payments: '^1.0.0' },
|
|
16
20
|
},
|
|
17
21
|
{
|
|
18
|
-
id: '
|
|
22
|
+
id: 'payments',
|
|
19
23
|
version: '1.0.0',
|
|
20
24
|
},
|
|
21
25
|
{
|
|
22
|
-
id: '
|
|
26
|
+
id: 'shops',
|
|
23
27
|
version: '1.0.0',
|
|
24
28
|
},
|
|
25
29
|
{
|
|
26
|
-
id: '
|
|
30
|
+
id: 'payment-provider-invoice',
|
|
27
31
|
version: '1.0.0',
|
|
28
|
-
|
|
32
|
+
providesFor: 'payment-checkout',
|
|
33
|
+
dependencies: { payments: '^1.0.0' },
|
|
29
34
|
},
|
|
30
35
|
{
|
|
31
|
-
id: '
|
|
36
|
+
id: 'payment-provider-stripe',
|
|
32
37
|
version: '1.0.0',
|
|
38
|
+
defaultFor: 'payment-checkout',
|
|
39
|
+
providesFor: 'payment-checkout',
|
|
40
|
+
dependencies: { payments: '^1.0.0' },
|
|
33
41
|
},
|
|
34
42
|
],
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
const selection = catalog.resolveSelection({
|
|
38
|
-
selected: ['
|
|
39
|
-
baseDescriptors: ['ui-shell'],
|
|
46
|
+
selected: ['web'],
|
|
40
47
|
});
|
|
41
48
|
|
|
42
49
|
console.log(selection.getResolved());
|
|
43
|
-
// ['
|
|
50
|
+
// ['checkout', 'payment-provider-invoice', 'payment-provider-stripe', 'payments', 'shops', 'web']
|
|
44
51
|
|
|
45
52
|
console.log(selection.getProvenance());
|
|
46
53
|
// [
|
|
47
|
-
// { descriptorId: '
|
|
48
|
-
// { descriptorId: '
|
|
49
|
-
// { descriptorId: '
|
|
50
|
-
// { descriptorId: '
|
|
51
|
-
// { descriptorId: '
|
|
54
|
+
// { descriptorId: 'checkout', origins: ['resolved'] },
|
|
55
|
+
// { descriptorId: 'payment-provider-invoice', origins: ['resolved'] },
|
|
56
|
+
// { descriptorId: 'payment-provider-stripe', origins: ['resolved'] },
|
|
57
|
+
// { descriptorId: 'payments', origins: ['resolved'] },
|
|
58
|
+
// { descriptorId: 'shops', origins: ['resolved'] },
|
|
59
|
+
// { descriptorId: 'web', origins: ['selected'] }
|
|
52
60
|
// ]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lorion-org/composition-graph",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Framework-free descriptor catalogs, relation graphs, and composition selection logic.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"exports": {
|
|
24
24
|
".": {
|
|
25
|
+
"lorion-source": {
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"default": "./src/index.ts"
|
|
28
|
+
},
|
|
25
29
|
"import": {
|
|
26
30
|
"types": "./dist/index.d.ts",
|
|
27
31
|
"default": "./dist/index.js"
|
|
@@ -35,7 +39,9 @@
|
|
|
35
39
|
"files": [
|
|
36
40
|
"dist",
|
|
37
41
|
"examples",
|
|
38
|
-
"LICENSE"
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"src/**/*.ts",
|
|
44
|
+
"!src/**/*.spec.ts"
|
|
39
45
|
],
|
|
40
46
|
"keywords": [
|
|
41
47
|
"composition",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getCompositionProvenance } from './descriptorGraph';
|
|
2
|
+
import { assertKnownDescriptorIds } from './descriptorMap';
|
|
3
|
+
import type {
|
|
4
|
+
CompositionPolicy,
|
|
5
|
+
CompositionProvenance,
|
|
6
|
+
CompositionSelection,
|
|
7
|
+
Descriptor,
|
|
8
|
+
DescriptorCatalog,
|
|
9
|
+
DescriptorId,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
export const defaultCompositionPolicy: CompositionPolicy = {
|
|
13
|
+
resolutionRelationIds: ['dependencies'],
|
|
14
|
+
provenanceRelationIds: ['dependencies'],
|
|
15
|
+
inspectionRelationIds: ['dependencies'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function resolvePolicy(policy?: Partial<CompositionPolicy>): CompositionPolicy {
|
|
19
|
+
return {
|
|
20
|
+
...defaultCompositionPolicy,
|
|
21
|
+
...policy,
|
|
22
|
+
resolutionRelationIds:
|
|
23
|
+
policy?.resolutionRelationIds ?? defaultCompositionPolicy.resolutionRelationIds,
|
|
24
|
+
provenanceRelationIds:
|
|
25
|
+
policy?.provenanceRelationIds ?? defaultCompositionPolicy.provenanceRelationIds,
|
|
26
|
+
inspectionRelationIds:
|
|
27
|
+
policy?.inspectionRelationIds ?? defaultCompositionPolicy.inspectionRelationIds,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createCompositionSelection(input: {
|
|
32
|
+
catalog: DescriptorCatalog;
|
|
33
|
+
selected?: DescriptorId[];
|
|
34
|
+
baseDescriptors?: DescriptorId[];
|
|
35
|
+
policy?: Partial<CompositionPolicy>;
|
|
36
|
+
}): CompositionSelection {
|
|
37
|
+
const catalog = input.catalog;
|
|
38
|
+
const descriptorMap = catalog.getDescriptorMap();
|
|
39
|
+
const selected = [...new Set(input.selected ?? [])].filter(Boolean).sort();
|
|
40
|
+
const baseDescriptors = [...new Set(input.baseDescriptors ?? [])].filter(Boolean).sort();
|
|
41
|
+
|
|
42
|
+
assertKnownDescriptorIds(descriptorMap, selected, 'selected descriptors');
|
|
43
|
+
assertKnownDescriptorIds(descriptorMap, baseDescriptors, 'base descriptors');
|
|
44
|
+
|
|
45
|
+
const policy = resolvePolicy(input.policy);
|
|
46
|
+
const resolved = catalog.getTransitiveTargets({
|
|
47
|
+
start: [...selected, ...baseDescriptors],
|
|
48
|
+
relationIds: policy.resolutionRelationIds,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let resolvedDescriptorsCache: Descriptor[] | undefined;
|
|
52
|
+
let provenanceCache: CompositionProvenance[] | undefined;
|
|
53
|
+
|
|
54
|
+
const getResolvedDescriptors = (): Descriptor[] => {
|
|
55
|
+
if (!resolvedDescriptorsCache) {
|
|
56
|
+
resolvedDescriptorsCache = resolved
|
|
57
|
+
.map((descriptorId) => descriptorMap.get(descriptorId))
|
|
58
|
+
.filter((descriptor): descriptor is Descriptor => Boolean(descriptor));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolvedDescriptorsCache;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const getProvenance = (): CompositionProvenance[] => {
|
|
65
|
+
if (!provenanceCache) {
|
|
66
|
+
provenanceCache = getCompositionProvenance({
|
|
67
|
+
graph: catalog.getGraph(),
|
|
68
|
+
descriptorIds: resolved,
|
|
69
|
+
selected,
|
|
70
|
+
baseDescriptors,
|
|
71
|
+
relationIds: policy.provenanceRelationIds,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return provenanceCache;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
getCatalog: () => catalog,
|
|
80
|
+
getGraph: () => catalog.getGraph(),
|
|
81
|
+
getSelected: () => [...selected],
|
|
82
|
+
getBaseDescriptors: () => [...baseDescriptors],
|
|
83
|
+
getResolved: () => [...resolved],
|
|
84
|
+
getResolvedDescriptors,
|
|
85
|
+
getProvenance,
|
|
86
|
+
getDependentsFor: (target, dependentsInput = {}) => {
|
|
87
|
+
return catalog.getDependents({
|
|
88
|
+
target,
|
|
89
|
+
relationIds: dependentsInput.relationIds ?? policy.resolutionRelationIds,
|
|
90
|
+
...(dependentsInput.transitive !== undefined
|
|
91
|
+
? { transitive: dependentsInput.transitive }
|
|
92
|
+
: {}),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
explain: (explainInput) => {
|
|
96
|
+
return catalog.explain({
|
|
97
|
+
from: explainInput.from,
|
|
98
|
+
to: explainInput.to,
|
|
99
|
+
relationIds: explainInput.relationIds ?? policy.inspectionRelationIds,
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
explainPathsBatch: (batchInput) => {
|
|
103
|
+
return catalog.explainPathsBatch({
|
|
104
|
+
pairs: batchInput.pairs,
|
|
105
|
+
relationIds: batchInput.relationIds ?? policy.inspectionRelationIds,
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDescriptorGraph,
|
|
3
|
+
defaultRelationDescriptors,
|
|
4
|
+
explainPath,
|
|
5
|
+
explainPathsBatch,
|
|
6
|
+
getDependents,
|
|
7
|
+
getIncomingRelationMap,
|
|
8
|
+
getTransitiveTargets,
|
|
9
|
+
} from './descriptorGraph';
|
|
10
|
+
import { buildDescriptorMap } from './descriptorMap';
|
|
11
|
+
import { createCompositionSelection } from './compositionSelection';
|
|
12
|
+
import type {
|
|
13
|
+
Descriptor,
|
|
14
|
+
DescriptorCatalog,
|
|
15
|
+
DescriptorGraph,
|
|
16
|
+
DescriptorMap,
|
|
17
|
+
DescriptorProfile,
|
|
18
|
+
RelationDescriptor,
|
|
19
|
+
RelationId,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
function createRelationRecord(
|
|
23
|
+
relationIds: RelationId[],
|
|
24
|
+
values: Array<[RelationId, string[]]>,
|
|
25
|
+
): Record<RelationId, string[]> {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
relationIds.map((relationId) => {
|
|
28
|
+
const entry = values.find(([relation]) => relation === relationId);
|
|
29
|
+
|
|
30
|
+
return [relationId, entry?.[1] ?? []];
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveRelationDescriptors(
|
|
36
|
+
relationDescriptors?: RelationDescriptor[],
|
|
37
|
+
): RelationDescriptor[] {
|
|
38
|
+
const registry = new Map<string, RelationDescriptor>();
|
|
39
|
+
|
|
40
|
+
for (const relationDescriptor of defaultRelationDescriptors) {
|
|
41
|
+
registry.set(relationDescriptor.id, relationDescriptor);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const relationDescriptor of relationDescriptors ?? []) {
|
|
45
|
+
registry.set(relationDescriptor.id, relationDescriptor);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Array.from(registry.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildDescriptorProfile(
|
|
52
|
+
descriptor: Descriptor,
|
|
53
|
+
relationIds: RelationId[],
|
|
54
|
+
graph: DescriptorGraph,
|
|
55
|
+
): DescriptorProfile {
|
|
56
|
+
const outgoing = createRelationRecord(
|
|
57
|
+
relationIds,
|
|
58
|
+
relationIds.map((relationId) => {
|
|
59
|
+
const targets = (graph.outgoing.get(descriptor.id) ?? [])
|
|
60
|
+
.filter((edge) => edge.relation === relationId)
|
|
61
|
+
.map((edge) => edge.to)
|
|
62
|
+
.sort();
|
|
63
|
+
|
|
64
|
+
return [relationId, targets];
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
const incoming = createRelationRecord(
|
|
68
|
+
relationIds,
|
|
69
|
+
relationIds.map((relationId) => {
|
|
70
|
+
const sources = (graph.incoming.get(descriptor.id) ?? [])
|
|
71
|
+
.filter((edge) => edge.relation === relationId)
|
|
72
|
+
.map((edge) => edge.from)
|
|
73
|
+
.sort();
|
|
74
|
+
|
|
75
|
+
return [relationId, sources];
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
id: descriptor.id,
|
|
81
|
+
disabled: descriptor.disabled === true,
|
|
82
|
+
capabilities: [...(descriptor.capabilities ?? [])].sort(),
|
|
83
|
+
...(descriptor.location ? { location: descriptor.location } : {}),
|
|
84
|
+
...(typeof descriptor.providesFor === 'string' || Array.isArray(descriptor.providesFor)
|
|
85
|
+
? { providesFor: descriptor.providesFor }
|
|
86
|
+
: {}),
|
|
87
|
+
outgoing,
|
|
88
|
+
incoming,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createDescriptorCatalog(input: {
|
|
93
|
+
descriptorMap?: DescriptorMap;
|
|
94
|
+
descriptors?: Descriptor[];
|
|
95
|
+
relationDescriptors?: RelationDescriptor[];
|
|
96
|
+
}): DescriptorCatalog {
|
|
97
|
+
const descriptorMap = input.descriptorMap ?? buildDescriptorMap(input.descriptors ?? []);
|
|
98
|
+
const relationDescriptors = resolveRelationDescriptors(input.relationDescriptors);
|
|
99
|
+
const graph = buildDescriptorGraph({
|
|
100
|
+
descriptorMap,
|
|
101
|
+
relationDescriptors,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const catalog: DescriptorCatalog = {
|
|
105
|
+
getDescriptorMap: () => descriptorMap,
|
|
106
|
+
getAllDescriptors: () => Array.from(descriptorMap.values()),
|
|
107
|
+
getDescriptor: (id) => descriptorMap.get(id),
|
|
108
|
+
getGraph: () => graph,
|
|
109
|
+
getRelationDescriptors: () => Array.from(graph.relationDescriptors.values()),
|
|
110
|
+
getProfiles: (profileInput = {}) => {
|
|
111
|
+
const ids = [...new Set(profileInput.ids ?? [])].filter(Boolean).sort();
|
|
112
|
+
const descriptors = ids.length
|
|
113
|
+
? ids
|
|
114
|
+
.map((id) => descriptorMap.get(id))
|
|
115
|
+
.filter((descriptor): descriptor is Descriptor => Boolean(descriptor))
|
|
116
|
+
: Array.from(descriptorMap.values())
|
|
117
|
+
.filter(
|
|
118
|
+
(descriptor) => profileInput.includeDisabled === true || descriptor.disabled !== true,
|
|
119
|
+
)
|
|
120
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
121
|
+
|
|
122
|
+
return descriptors.map((descriptor) =>
|
|
123
|
+
buildDescriptorProfile(descriptor, Array.from(graph.relationDescriptors.keys()), graph),
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
getIncomingRelationMap: (relationId) => getIncomingRelationMap({ graph, relationId }),
|
|
127
|
+
getTransitiveTargets: (transitiveInput) =>
|
|
128
|
+
getTransitiveTargets({
|
|
129
|
+
graph,
|
|
130
|
+
start: transitiveInput.start,
|
|
131
|
+
relationIds: transitiveInput.relationIds,
|
|
132
|
+
}),
|
|
133
|
+
getDependents: (dependentsInput) =>
|
|
134
|
+
getDependents({
|
|
135
|
+
graph,
|
|
136
|
+
target: dependentsInput.target,
|
|
137
|
+
relationIds: dependentsInput.relationIds,
|
|
138
|
+
...(dependentsInput.transitive !== undefined
|
|
139
|
+
? { transitive: dependentsInput.transitive }
|
|
140
|
+
: {}),
|
|
141
|
+
}),
|
|
142
|
+
explain: (explainInput) =>
|
|
143
|
+
explainPath({
|
|
144
|
+
graph,
|
|
145
|
+
from: explainInput.from,
|
|
146
|
+
to: explainInput.to,
|
|
147
|
+
relationIds: explainInput.relationIds,
|
|
148
|
+
}),
|
|
149
|
+
explainPathsBatch: (batchInput) =>
|
|
150
|
+
explainPathsBatch({
|
|
151
|
+
graph,
|
|
152
|
+
pairs: batchInput.pairs,
|
|
153
|
+
relationIds: batchInput.relationIds,
|
|
154
|
+
}),
|
|
155
|
+
resolveSelection: (selectionInput) =>
|
|
156
|
+
createCompositionSelection({
|
|
157
|
+
catalog,
|
|
158
|
+
...selectionInput,
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return catalog;
|
|
163
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompositionOriginType,
|
|
3
|
+
CompositionProvenance,
|
|
4
|
+
CompositionProvenanceOrigin,
|
|
5
|
+
Descriptor,
|
|
6
|
+
DescriptorEdge,
|
|
7
|
+
DescriptorGraph,
|
|
8
|
+
DescriptorId,
|
|
9
|
+
DescriptorIds,
|
|
10
|
+
DescriptorMap,
|
|
11
|
+
RelationDescriptor,
|
|
12
|
+
RelationId,
|
|
13
|
+
ResolutionStep,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const createEmptyAdjacency = (
|
|
17
|
+
descriptorMap: DescriptorMap,
|
|
18
|
+
): Map<DescriptorId, DescriptorEdge[]> => {
|
|
19
|
+
return new Map(
|
|
20
|
+
Array.from(descriptorMap.keys())
|
|
21
|
+
.sort()
|
|
22
|
+
.map((id) => [id, []]),
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sortEdges = (edges: DescriptorEdge[]): DescriptorEdge[] => {
|
|
27
|
+
return [...edges].sort(
|
|
28
|
+
(left, right) =>
|
|
29
|
+
left.from.localeCompare(right.from) ||
|
|
30
|
+
left.to.localeCompare(right.to) ||
|
|
31
|
+
left.relation.localeCompare(right.relation),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const defaultRelationDescriptors: RelationDescriptor[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'dependencies',
|
|
38
|
+
field: 'dependencies',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRelationTargets(
|
|
47
|
+
descriptor: Descriptor,
|
|
48
|
+
relationDescriptor: RelationDescriptor,
|
|
49
|
+
): DescriptorId[] {
|
|
50
|
+
const field = relationDescriptor.field ?? relationDescriptor.id;
|
|
51
|
+
const relationValue = descriptor[field];
|
|
52
|
+
|
|
53
|
+
const targets = Array.isArray(relationValue)
|
|
54
|
+
? relationValue
|
|
55
|
+
: typeof relationValue === 'string'
|
|
56
|
+
? [relationValue]
|
|
57
|
+
: isRecord(relationValue)
|
|
58
|
+
? relationDescriptor.targetMode === 'values'
|
|
59
|
+
? Object.values(relationValue)
|
|
60
|
+
: Object.keys(relationValue)
|
|
61
|
+
: [];
|
|
62
|
+
|
|
63
|
+
return targets
|
|
64
|
+
.filter((target): target is string => typeof target === 'string' && target.length > 0)
|
|
65
|
+
.sort();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createOriginType(path: ResolutionStep[]): CompositionOriginType {
|
|
69
|
+
return path.length ? 'resolved' : 'selected';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareOrigins(
|
|
73
|
+
left: CompositionProvenanceOrigin,
|
|
74
|
+
right: CompositionProvenanceOrigin,
|
|
75
|
+
): number {
|
|
76
|
+
const leftPath = JSON.stringify(left.path);
|
|
77
|
+
const rightPath = JSON.stringify(right.path);
|
|
78
|
+
|
|
79
|
+
return left.originType.localeCompare(right.originType) || leftPath.localeCompare(rightPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildDescriptorGraph(input: {
|
|
83
|
+
descriptorMap: DescriptorMap;
|
|
84
|
+
relationDescriptors?: RelationDescriptor[];
|
|
85
|
+
}): DescriptorGraph {
|
|
86
|
+
const relationDescriptors = input.relationDescriptors ?? defaultRelationDescriptors;
|
|
87
|
+
const descriptorRegistry = new Map<RelationId, RelationDescriptor>(
|
|
88
|
+
[...relationDescriptors]
|
|
89
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
90
|
+
.map((descriptor) => [descriptor.id, descriptor]),
|
|
91
|
+
);
|
|
92
|
+
const edges: DescriptorEdge[] = [];
|
|
93
|
+
const outgoing = createEmptyAdjacency(input.descriptorMap);
|
|
94
|
+
const incoming = createEmptyAdjacency(input.descriptorMap);
|
|
95
|
+
|
|
96
|
+
for (const descriptor of Array.from(input.descriptorMap.values()).sort((left, right) =>
|
|
97
|
+
left.id.localeCompare(right.id),
|
|
98
|
+
)) {
|
|
99
|
+
for (const relationDescriptor of descriptorRegistry.values()) {
|
|
100
|
+
for (const target of getRelationTargets(descriptor, relationDescriptor)) {
|
|
101
|
+
if (!input.descriptorMap.has(target)) continue;
|
|
102
|
+
|
|
103
|
+
const isIncoming = relationDescriptor.direction === 'incoming';
|
|
104
|
+
const edge: DescriptorEdge = {
|
|
105
|
+
from: isIncoming ? target : descriptor.id,
|
|
106
|
+
to: isIncoming ? descriptor.id : target,
|
|
107
|
+
relation: relationDescriptor.id,
|
|
108
|
+
source: 'descriptor',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
edges.push(edge);
|
|
112
|
+
outgoing.get(edge.from)?.push(edge);
|
|
113
|
+
incoming.get(edge.to)?.push(edge);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const [id, nodeEdges] of outgoing) outgoing.set(id, sortEdges(nodeEdges));
|
|
119
|
+
for (const [id, nodeEdges] of incoming) incoming.set(id, sortEdges(nodeEdges));
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
descriptorMap: input.descriptorMap,
|
|
123
|
+
edges: sortEdges(edges),
|
|
124
|
+
outgoing,
|
|
125
|
+
incoming,
|
|
126
|
+
relationDescriptors: descriptorRegistry,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getTransitiveTargets(input: {
|
|
131
|
+
graph: DescriptorGraph;
|
|
132
|
+
start: DescriptorId[];
|
|
133
|
+
relationIds: RelationId[];
|
|
134
|
+
}): DescriptorIds {
|
|
135
|
+
const allowedRelations = new Set(input.relationIds);
|
|
136
|
+
const visited = new Set<DescriptorId>();
|
|
137
|
+
const stack = [...input.start].sort().reverse();
|
|
138
|
+
|
|
139
|
+
while (stack.length) {
|
|
140
|
+
const current = stack.pop();
|
|
141
|
+
if (!current || visited.has(current)) continue;
|
|
142
|
+
|
|
143
|
+
visited.add(current);
|
|
144
|
+
|
|
145
|
+
for (const edge of input.graph.outgoing.get(current) ?? []) {
|
|
146
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.to)) continue;
|
|
147
|
+
stack.push(edge.to);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Array.from(visited)
|
|
152
|
+
.filter((id) => input.graph.descriptorMap.has(id))
|
|
153
|
+
.sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getIncomingRelationMap(input: {
|
|
157
|
+
graph: DescriptorGraph;
|
|
158
|
+
relationId: RelationId;
|
|
159
|
+
}): Map<DescriptorId, DescriptorId[]> {
|
|
160
|
+
const entries = Array.from(input.graph.incoming.entries())
|
|
161
|
+
.map(([target, edges]) => {
|
|
162
|
+
const sources = edges
|
|
163
|
+
.filter((edge) => edge.relation === input.relationId)
|
|
164
|
+
.map((edge) => edge.from)
|
|
165
|
+
.sort();
|
|
166
|
+
|
|
167
|
+
return [target, sources] as const;
|
|
168
|
+
})
|
|
169
|
+
.filter(([, sources]) => sources.length > 0)
|
|
170
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
171
|
+
|
|
172
|
+
return new Map(entries);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getDependents(input: {
|
|
176
|
+
graph: DescriptorGraph;
|
|
177
|
+
target: DescriptorId;
|
|
178
|
+
relationIds: RelationId[];
|
|
179
|
+
transitive?: boolean;
|
|
180
|
+
}): DescriptorIds {
|
|
181
|
+
if (!input.graph.descriptorMap.has(input.target)) return [];
|
|
182
|
+
|
|
183
|
+
const allowedRelations = new Set(input.relationIds);
|
|
184
|
+
const visited = new Set<DescriptorId>();
|
|
185
|
+
const queue: DescriptorId[] = [input.target];
|
|
186
|
+
|
|
187
|
+
while (queue.length) {
|
|
188
|
+
const current = queue.shift();
|
|
189
|
+
if (!current || visited.has(current)) continue;
|
|
190
|
+
|
|
191
|
+
visited.add(current);
|
|
192
|
+
|
|
193
|
+
if (input.transitive === false && current !== input.target) continue;
|
|
194
|
+
|
|
195
|
+
for (const edge of input.graph.incoming.get(current) ?? []) {
|
|
196
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.from)) continue;
|
|
197
|
+
queue.push(edge.from);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
visited.delete(input.target);
|
|
202
|
+
|
|
203
|
+
return Array.from(visited).sort();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function explainPath(input: {
|
|
207
|
+
graph: DescriptorGraph;
|
|
208
|
+
from: DescriptorId;
|
|
209
|
+
to: DescriptorId;
|
|
210
|
+
relationIds: RelationId[];
|
|
211
|
+
}): ResolutionStep[] {
|
|
212
|
+
if (!input.graph.descriptorMap.has(input.from) || !input.graph.descriptorMap.has(input.to))
|
|
213
|
+
return [];
|
|
214
|
+
if (input.from === input.to) return [];
|
|
215
|
+
|
|
216
|
+
const allowedRelations = new Set(input.relationIds);
|
|
217
|
+
const visited = new Set<DescriptorId>([input.from]);
|
|
218
|
+
const queue: Array<{ node: DescriptorId; path: ResolutionStep[] }> = [
|
|
219
|
+
{ node: input.from, path: [] },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
while (queue.length) {
|
|
223
|
+
const current = queue.shift();
|
|
224
|
+
if (!current) continue;
|
|
225
|
+
|
|
226
|
+
for (const edge of input.graph.outgoing.get(current.node) ?? []) {
|
|
227
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.to)) continue;
|
|
228
|
+
|
|
229
|
+
const nextPath: ResolutionStep[] = [
|
|
230
|
+
...current.path,
|
|
231
|
+
{
|
|
232
|
+
from: edge.from,
|
|
233
|
+
to: edge.to,
|
|
234
|
+
relation: edge.relation,
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
if (edge.to === input.to) return nextPath;
|
|
239
|
+
|
|
240
|
+
visited.add(edge.to);
|
|
241
|
+
queue.push({
|
|
242
|
+
node: edge.to,
|
|
243
|
+
path: nextPath,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function explainPathsBatch(input: {
|
|
252
|
+
graph: DescriptorGraph;
|
|
253
|
+
pairs: Array<{ from: DescriptorId; to: DescriptorId }>;
|
|
254
|
+
relationIds: RelationId[];
|
|
255
|
+
}): Array<{ from: DescriptorId; to: DescriptorId; path: ResolutionStep[] }> {
|
|
256
|
+
return [...input.pairs]
|
|
257
|
+
.sort((left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to))
|
|
258
|
+
.map(({ from, to }) => ({
|
|
259
|
+
from,
|
|
260
|
+
to,
|
|
261
|
+
path: explainPath({
|
|
262
|
+
graph: input.graph,
|
|
263
|
+
from,
|
|
264
|
+
to,
|
|
265
|
+
relationIds: input.relationIds,
|
|
266
|
+
}),
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getCompositionProvenance(input: {
|
|
271
|
+
graph: DescriptorGraph;
|
|
272
|
+
descriptorIds: DescriptorId[];
|
|
273
|
+
selected: DescriptorId[];
|
|
274
|
+
baseDescriptors?: DescriptorId[];
|
|
275
|
+
relationIds: RelationId[];
|
|
276
|
+
}): CompositionProvenance[] {
|
|
277
|
+
const selected = [...new Set(input.selected)]
|
|
278
|
+
.filter((descriptorId) => input.graph.descriptorMap.has(descriptorId))
|
|
279
|
+
.sort();
|
|
280
|
+
const baseDescriptors = [...new Set(input.baseDescriptors ?? [])]
|
|
281
|
+
.filter((descriptorId) => input.graph.descriptorMap.has(descriptorId))
|
|
282
|
+
.sort();
|
|
283
|
+
const descriptorIds = [...new Set(input.descriptorIds)]
|
|
284
|
+
.filter((descriptorId) => input.graph.descriptorMap.has(descriptorId))
|
|
285
|
+
.sort();
|
|
286
|
+
|
|
287
|
+
return descriptorIds.map((descriptorId): CompositionProvenance => {
|
|
288
|
+
const origins: CompositionProvenanceOrigin[] = [];
|
|
289
|
+
|
|
290
|
+
if (selected.includes(descriptorId)) {
|
|
291
|
+
origins.push({
|
|
292
|
+
originType: 'selected',
|
|
293
|
+
path: [],
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (baseDescriptors.includes(descriptorId)) {
|
|
298
|
+
origins.push({
|
|
299
|
+
originType: 'base',
|
|
300
|
+
path: [],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const source of [...selected, ...baseDescriptors]) {
|
|
305
|
+
if (source === descriptorId) continue;
|
|
306
|
+
|
|
307
|
+
const path = explainPath({
|
|
308
|
+
graph: input.graph,
|
|
309
|
+
from: source,
|
|
310
|
+
to: descriptorId,
|
|
311
|
+
relationIds: input.relationIds,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!path.length) continue;
|
|
315
|
+
|
|
316
|
+
origins.push({
|
|
317
|
+
originType: createOriginType(path),
|
|
318
|
+
path,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
descriptorId,
|
|
324
|
+
origins: origins.sort(compareOrigins),
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
}
|