@lorion-org/composition-graph 1.0.0-beta.1 → 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 +44 -3
- 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 +1 -0
- package/examples/deployment-composition.ts +1 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Descriptor, DescriptorId, DescriptorMap } from './types';
|
|
2
|
+
|
|
3
|
+
export type DescriptorSelectionSeedInput = {
|
|
4
|
+
argv?: string[];
|
|
5
|
+
cliKeys?: string[];
|
|
6
|
+
defaultValue?: DescriptorId[] | string;
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
envKeys?: string[];
|
|
9
|
+
key?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function buildDescriptorMap(descriptors: Iterable<Descriptor>): DescriptorMap {
|
|
13
|
+
const descriptorMap: DescriptorMap = new Map();
|
|
14
|
+
|
|
15
|
+
for (const descriptor of descriptors) {
|
|
16
|
+
descriptorMap.set(descriptor.id, descriptor);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return descriptorMap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseDescriptorIds(input?: unknown): DescriptorId[] {
|
|
23
|
+
if (typeof input === 'string') {
|
|
24
|
+
return Array.from(
|
|
25
|
+
new Set(
|
|
26
|
+
input
|
|
27
|
+
.split(/[,\s]+/)
|
|
28
|
+
.map((item) => item.trim())
|
|
29
|
+
.filter(Boolean),
|
|
30
|
+
),
|
|
31
|
+
).sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!Array.isArray(input)) return [];
|
|
35
|
+
|
|
36
|
+
const items = input.flatMap((item) => (typeof item === 'string' ? parseDescriptorIds(item) : []));
|
|
37
|
+
|
|
38
|
+
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean))).sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readCliValue(argv: string[], key: string): string | undefined {
|
|
42
|
+
const arg = argv.find((entry) => entry === key || entry.startsWith(`${key}=`));
|
|
43
|
+
|
|
44
|
+
if (!arg) return undefined;
|
|
45
|
+
if (arg === key) {
|
|
46
|
+
const value = argv[argv.indexOf(arg) + 1];
|
|
47
|
+
return value?.startsWith('-') ? undefined : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return arg.slice(`${key}=`.length);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function firstNonEmptyValue(
|
|
54
|
+
values: Array<string | string[] | undefined>,
|
|
55
|
+
): string | string[] | undefined {
|
|
56
|
+
return values.find((value) => {
|
|
57
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
58
|
+
return Array.isArray(value) && value.some((entry) => typeof entry === 'string' && entry.trim());
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pluralizeSelectionKeySegment(value: string): string {
|
|
63
|
+
if (value.endsWith('s')) return value;
|
|
64
|
+
if (value.endsWith('y')) return `${value.slice(0, -1)}ies`;
|
|
65
|
+
return `${value}s`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSelectionKey(value: string): string {
|
|
69
|
+
const segments = value
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
72
|
+
.replace(/[_\s]+/g, '-')
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.split('-')
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
|
|
77
|
+
if (!segments.length) return '';
|
|
78
|
+
|
|
79
|
+
const lastSegment = segments[segments.length - 1] as string;
|
|
80
|
+
segments[segments.length - 1] = pluralizeSelectionKeySegment(lastSegment);
|
|
81
|
+
|
|
82
|
+
return segments.join('-');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toEnvKey(value: string): string {
|
|
86
|
+
return value.replaceAll('-', '_').toUpperCase();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveCliKeys(input: DescriptorSelectionSeedInput): string[] {
|
|
90
|
+
if (input.cliKeys) return input.cliKeys;
|
|
91
|
+
|
|
92
|
+
const key = input.key ? normalizeSelectionKey(input.key) : '';
|
|
93
|
+
return key ? [`--${key}`] : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveEnvKeys(input: DescriptorSelectionSeedInput): string[] {
|
|
97
|
+
if (input.envKeys) return input.envKeys;
|
|
98
|
+
|
|
99
|
+
const key = input.key ? normalizeSelectionKey(input.key) : '';
|
|
100
|
+
return key ? [`npm_config_${key.replaceAll('-', '_')}`, `LORION_${toEnvKey(key)}`] : [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveDescriptorSelectionSeed(
|
|
104
|
+
input: DescriptorSelectionSeedInput = {},
|
|
105
|
+
): DescriptorId[] {
|
|
106
|
+
return parseDescriptorIds(
|
|
107
|
+
firstNonEmptyValue([
|
|
108
|
+
...resolveCliKeys(input).map((key) => readCliValue(input.argv ?? [], key)),
|
|
109
|
+
...resolveEnvKeys(input).map((key) => input.env?.[key]),
|
|
110
|
+
input.defaultValue,
|
|
111
|
+
]),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function assertKnownDescriptorIds(
|
|
116
|
+
descriptorMap: DescriptorMap,
|
|
117
|
+
ids: DescriptorId[],
|
|
118
|
+
label: string,
|
|
119
|
+
): void {
|
|
120
|
+
const missing: DescriptorId[] = [...new Set(ids)]
|
|
121
|
+
.filter((id) => id && !descriptorMap.has(id))
|
|
122
|
+
.sort();
|
|
123
|
+
|
|
124
|
+
if (!missing.length) return;
|
|
125
|
+
|
|
126
|
+
throw new Error(`Unknown ${label}: ${missing.join(', ')}`);
|
|
127
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export {
|
|
2
|
+
assertKnownDescriptorIds,
|
|
3
|
+
buildDescriptorMap,
|
|
4
|
+
parseDescriptorIds,
|
|
5
|
+
resolveDescriptorSelectionSeed,
|
|
6
|
+
} from './descriptorMap';
|
|
7
|
+
export {
|
|
8
|
+
buildDescriptorGraph,
|
|
9
|
+
defaultRelationDescriptors,
|
|
10
|
+
explainPath,
|
|
11
|
+
explainPathsBatch,
|
|
12
|
+
getCompositionProvenance,
|
|
13
|
+
getDependents,
|
|
14
|
+
getIncomingRelationMap,
|
|
15
|
+
getTransitiveTargets,
|
|
16
|
+
} from './descriptorGraph';
|
|
17
|
+
export { createDescriptorCatalog } from './descriptorCatalog';
|
|
18
|
+
export { createCompositionSelection, defaultCompositionPolicy } from './compositionSelection';
|
|
19
|
+
export type {
|
|
20
|
+
CompositionOriginType,
|
|
21
|
+
CompositionPolicy,
|
|
22
|
+
CompositionProvenance,
|
|
23
|
+
CompositionProvenanceOrigin,
|
|
24
|
+
CompositionSelection,
|
|
25
|
+
Descriptor,
|
|
26
|
+
DescriptorCatalog,
|
|
27
|
+
DescriptorEdge,
|
|
28
|
+
DescriptorGraph,
|
|
29
|
+
DescriptorId,
|
|
30
|
+
DescriptorIds,
|
|
31
|
+
DescriptorMap,
|
|
32
|
+
DescriptorProfile,
|
|
33
|
+
RelationDescriptor,
|
|
34
|
+
RelationId,
|
|
35
|
+
ResolutionStep,
|
|
36
|
+
VersionConstraintMap,
|
|
37
|
+
} from './types';
|
|
38
|
+
export type { DescriptorSelectionSeedInput } from './descriptorMap';
|