@lorion-org/composition-graph 1.0.0-beta.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/index.cjs +442 -0
- package/dist/index.d.cts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +402 -0
- package/examples/deployment-composition.js +43 -0
- package/examples/deployment-composition.ts +52 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the 'Software'), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# @lorion-org/composition-graph
|
|
2
|
+
|
|
3
|
+
Framework-free descriptor catalogs, relation graphs, and composition selection logic.
|
|
4
|
+
|
|
5
|
+
This package models flat named descriptors, their declared relations, optional base descriptors, and deterministic composition flows.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
pnpm add @lorion-org/composition-graph
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it is
|
|
14
|
+
|
|
15
|
+
- a typed descriptor model for flat descriptors
|
|
16
|
+
- a graph builder for declared relations
|
|
17
|
+
- a catalog for querying profiles and relation paths
|
|
18
|
+
- a selection layer for resolving selected and base descriptors into a final set
|
|
19
|
+
|
|
20
|
+
## What it is not
|
|
21
|
+
|
|
22
|
+
- not a package manager
|
|
23
|
+
- not a filesystem discovery tool
|
|
24
|
+
- not a framework adapter
|
|
25
|
+
- not a runtime-config loader
|
|
26
|
+
|
|
27
|
+
## Basic example
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createDescriptorCatalog } from '@lorion-org/composition-graph';
|
|
31
|
+
|
|
32
|
+
const catalog = createDescriptorCatalog({
|
|
33
|
+
relationDescriptors: [
|
|
34
|
+
{
|
|
35
|
+
id: 'integrations',
|
|
36
|
+
field: 'integrations',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
descriptors: [
|
|
40
|
+
{
|
|
41
|
+
id: 'billing',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
dependencies: { storage: '*' },
|
|
44
|
+
integrations: { analytics: '*' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'storage',
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'analytics',
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'web-shell',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
dependencies: { router: '*' },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'router',
|
|
61
|
+
version: '1.0.0',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const selection = catalog.resolveSelection({
|
|
67
|
+
selected: ['billing'],
|
|
68
|
+
baseDescriptors: ['web-shell'],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(selection.getResolved());
|
|
72
|
+
console.log(selection.getProvenance());
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Example: explicit base descriptors
|
|
76
|
+
|
|
77
|
+
Use `baseDescriptors` for descriptors that always participate in the
|
|
78
|
+
resolution for a given runtime, deployment, or application shell.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { createDescriptorCatalog } from '@lorion-org/composition-graph';
|
|
82
|
+
|
|
83
|
+
const catalog = createDescriptorCatalog({
|
|
84
|
+
descriptors: [
|
|
85
|
+
{
|
|
86
|
+
id: 'billing',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
dependencies: { storage: '*' },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'storage',
|
|
92
|
+
version: '1.0.0',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'web-shell',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
dependencies: { router: '*' },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'router',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const selection = catalog.resolveSelection({
|
|
107
|
+
selected: ['billing'],
|
|
108
|
+
baseDescriptors: ['web-shell'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
selection.getResolved();
|
|
112
|
+
// => ['billing', 'router', 'storage', 'web-shell']
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Example: explain why something is present
|
|
116
|
+
|
|
117
|
+
The catalog and selection APIs expose path explanation helpers for diagnostics,
|
|
118
|
+
admin UIs, and composition debugging.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { createDescriptorCatalog } from '@lorion-org/composition-graph';
|
|
122
|
+
|
|
123
|
+
const catalog = createDescriptorCatalog({
|
|
124
|
+
descriptors: [
|
|
125
|
+
{
|
|
126
|
+
id: 'billing',
|
|
127
|
+
version: '1.0.0',
|
|
128
|
+
dependencies: { storage: '*' },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'storage',
|
|
132
|
+
version: '1.0.0',
|
|
133
|
+
dependencies: { queue: '*' },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'queue',
|
|
137
|
+
version: '1.0.0',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
catalog.explain({
|
|
143
|
+
from: 'billing',
|
|
144
|
+
to: 'queue',
|
|
145
|
+
relationIds: ['dependencies'],
|
|
146
|
+
});
|
|
147
|
+
// => [
|
|
148
|
+
// { from: 'billing', to: 'storage', relation: 'dependencies' },
|
|
149
|
+
// { from: 'storage', to: 'queue', relation: 'dependencies' },
|
|
150
|
+
// ]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Example: combine with descriptor discovery
|
|
154
|
+
|
|
155
|
+
`@lorion-org/composition-graph` expects flat descriptors. If your authoring format
|
|
156
|
+
allows nested descriptor documents, flatten them before building the catalog.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { createDescriptorCatalog } from '@lorion-org/composition-graph';
|
|
160
|
+
import { discoverDescriptors } from '@lorion-org/descriptor-discovery';
|
|
161
|
+
|
|
162
|
+
const discovered = discoverDescriptors({
|
|
163
|
+
roots: ['./descriptors'],
|
|
164
|
+
descriptorFileName: 'descriptor.json',
|
|
165
|
+
idField: 'name',
|
|
166
|
+
nestedField: 'bundles',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const catalog = createDescriptorCatalog({
|
|
170
|
+
descriptors: discovered.map((entry) => entry.descriptor),
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Relations
|
|
175
|
+
|
|
176
|
+
- `dependencies` is the only built-in relation
|
|
177
|
+
- every additional relation must be registered via `relationDescriptors`
|
|
178
|
+
- unconfigured descriptor fields are ignored by the graph
|
|
179
|
+
- string parsing for user-facing selection input belongs in a higher adapter layer
|
|
180
|
+
- nested descriptor authoring belongs in a discovery or normalization layer, not in this package
|
|
181
|
+
|
|
182
|
+
`relationDescriptors` are intentionally small:
|
|
183
|
+
|
|
184
|
+
- `id` identifies the relation in graph queries and policies
|
|
185
|
+
- `field` optionally maps the relation to a descriptor field name
|
|
186
|
+
|
|
187
|
+
The core graph does not prescribe directional interpretation, hinting, or
|
|
188
|
+
weighting. Those concerns belong in higher layers.
|
|
189
|
+
|
|
190
|
+
Dependency-specific projections are also intentionally outside the core. If a
|
|
191
|
+
consumer needs a "what pulled this in?" view, derive that from
|
|
192
|
+
`getProvenance()` in its own adapter or UI layer.
|
|
193
|
+
|
|
194
|
+
## Local commands
|
|
195
|
+
|
|
196
|
+
```shell
|
|
197
|
+
cd packages/composition-graph
|
|
198
|
+
pnpm build
|
|
199
|
+
pnpm test
|
|
200
|
+
pnpm coverage
|
|
201
|
+
pnpm typecheck
|
|
202
|
+
pnpm package:check
|
|
203
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
assertKnownDescriptorIds: () => assertKnownDescriptorIds,
|
|
24
|
+
buildDescriptorGraph: () => buildDescriptorGraph,
|
|
25
|
+
buildDescriptorMap: () => buildDescriptorMap,
|
|
26
|
+
createCompositionSelection: () => createCompositionSelection,
|
|
27
|
+
createDescriptorCatalog: () => createDescriptorCatalog,
|
|
28
|
+
defaultCompositionPolicy: () => defaultCompositionPolicy,
|
|
29
|
+
defaultRelationDescriptors: () => defaultRelationDescriptors,
|
|
30
|
+
explainPath: () => explainPath,
|
|
31
|
+
explainPathsBatch: () => explainPathsBatch,
|
|
32
|
+
getCompositionProvenance: () => getCompositionProvenance,
|
|
33
|
+
getDependents: () => getDependents,
|
|
34
|
+
getIncomingRelationMap: () => getIncomingRelationMap,
|
|
35
|
+
getTransitiveTargets: () => getTransitiveTargets,
|
|
36
|
+
parseDescriptorIds: () => parseDescriptorIds
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/descriptorMap.ts
|
|
41
|
+
function buildDescriptorMap(descriptors) {
|
|
42
|
+
const descriptorMap = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const descriptor of descriptors) {
|
|
44
|
+
descriptorMap.set(descriptor.id, descriptor);
|
|
45
|
+
}
|
|
46
|
+
return descriptorMap;
|
|
47
|
+
}
|
|
48
|
+
function parseDescriptorIds(input) {
|
|
49
|
+
if (!input) return [];
|
|
50
|
+
const items = Array.isArray(input) ? input : input.split(/[,\s]+/).map((item) => item.trim());
|
|
51
|
+
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean))).sort();
|
|
52
|
+
}
|
|
53
|
+
function assertKnownDescriptorIds(descriptorMap, ids, label) {
|
|
54
|
+
const missing = [...new Set(ids)].filter((id) => id && !descriptorMap.has(id)).sort();
|
|
55
|
+
if (!missing.length) return;
|
|
56
|
+
throw new Error(`Unknown ${label}: ${missing.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/descriptorGraph.ts
|
|
60
|
+
var createEmptyAdjacency = (descriptorMap) => {
|
|
61
|
+
return new Map(
|
|
62
|
+
Array.from(descriptorMap.keys()).sort().map((id) => [id, []])
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
var sortEdges = (edges) => {
|
|
66
|
+
return [...edges].sort(
|
|
67
|
+
(left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to) || left.relation.localeCompare(right.relation)
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
var defaultRelationDescriptors = [
|
|
71
|
+
{
|
|
72
|
+
id: "dependencies",
|
|
73
|
+
field: "dependencies"
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
function isVersionConstraintMap(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function getRelationTargets(descriptor, relationDescriptor) {
|
|
80
|
+
const field = relationDescriptor.field ?? relationDescriptor.id;
|
|
81
|
+
const relationValue = descriptor[field];
|
|
82
|
+
if (!isVersionConstraintMap(relationValue)) return [];
|
|
83
|
+
return Object.keys(relationValue).sort();
|
|
84
|
+
}
|
|
85
|
+
function createOriginType(path) {
|
|
86
|
+
return path.length ? "resolved" : "selected";
|
|
87
|
+
}
|
|
88
|
+
function compareOrigins(left, right) {
|
|
89
|
+
const leftPath = JSON.stringify(left.path);
|
|
90
|
+
const rightPath = JSON.stringify(right.path);
|
|
91
|
+
return left.originType.localeCompare(right.originType) || leftPath.localeCompare(rightPath);
|
|
92
|
+
}
|
|
93
|
+
function buildDescriptorGraph(input) {
|
|
94
|
+
const relationDescriptors = input.relationDescriptors ?? defaultRelationDescriptors;
|
|
95
|
+
const descriptorRegistry = new Map(
|
|
96
|
+
[...relationDescriptors].sort((left, right) => left.id.localeCompare(right.id)).map((descriptor) => [descriptor.id, descriptor])
|
|
97
|
+
);
|
|
98
|
+
const edges = [];
|
|
99
|
+
const outgoing = createEmptyAdjacency(input.descriptorMap);
|
|
100
|
+
const incoming = createEmptyAdjacency(input.descriptorMap);
|
|
101
|
+
for (const descriptor of Array.from(input.descriptorMap.values()).sort(
|
|
102
|
+
(left, right) => left.id.localeCompare(right.id)
|
|
103
|
+
)) {
|
|
104
|
+
for (const relationDescriptor of descriptorRegistry.values()) {
|
|
105
|
+
for (const target of getRelationTargets(descriptor, relationDescriptor)) {
|
|
106
|
+
if (!input.descriptorMap.has(target)) continue;
|
|
107
|
+
const edge = {
|
|
108
|
+
from: descriptor.id,
|
|
109
|
+
to: target,
|
|
110
|
+
relation: relationDescriptor.id,
|
|
111
|
+
source: "descriptor"
|
|
112
|
+
};
|
|
113
|
+
edges.push(edge);
|
|
114
|
+
outgoing.get(descriptor.id)?.push(edge);
|
|
115
|
+
incoming.get(target)?.push(edge);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [id, nodeEdges] of outgoing) outgoing.set(id, sortEdges(nodeEdges));
|
|
120
|
+
for (const [id, nodeEdges] of incoming) incoming.set(id, sortEdges(nodeEdges));
|
|
121
|
+
return {
|
|
122
|
+
descriptorMap: input.descriptorMap,
|
|
123
|
+
edges: sortEdges(edges),
|
|
124
|
+
outgoing,
|
|
125
|
+
incoming,
|
|
126
|
+
relationDescriptors: descriptorRegistry
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function getTransitiveTargets(input) {
|
|
130
|
+
const allowedRelations = new Set(input.relationIds);
|
|
131
|
+
const visited = /* @__PURE__ */ new Set();
|
|
132
|
+
const stack = [...input.start].sort().reverse();
|
|
133
|
+
while (stack.length) {
|
|
134
|
+
const current = stack.pop();
|
|
135
|
+
if (!current || visited.has(current)) continue;
|
|
136
|
+
visited.add(current);
|
|
137
|
+
for (const edge of input.graph.outgoing.get(current) ?? []) {
|
|
138
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.to)) continue;
|
|
139
|
+
stack.push(edge.to);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Array.from(visited).filter((id) => input.graph.descriptorMap.has(id)).sort();
|
|
143
|
+
}
|
|
144
|
+
function getIncomingRelationMap(input) {
|
|
145
|
+
const entries = Array.from(input.graph.incoming.entries()).map(([target, edges]) => {
|
|
146
|
+
const sources = edges.filter((edge) => edge.relation === input.relationId).map((edge) => edge.from).sort();
|
|
147
|
+
return [target, sources];
|
|
148
|
+
}).filter(([, sources]) => sources.length > 0).sort(([left], [right]) => left.localeCompare(right));
|
|
149
|
+
return new Map(entries);
|
|
150
|
+
}
|
|
151
|
+
function getDependents(input) {
|
|
152
|
+
if (!input.graph.descriptorMap.has(input.target)) return [];
|
|
153
|
+
const allowedRelations = new Set(input.relationIds);
|
|
154
|
+
const visited = /* @__PURE__ */ new Set();
|
|
155
|
+
const queue = [input.target];
|
|
156
|
+
while (queue.length) {
|
|
157
|
+
const current = queue.shift();
|
|
158
|
+
if (!current || visited.has(current)) continue;
|
|
159
|
+
visited.add(current);
|
|
160
|
+
if (input.transitive === false && current !== input.target) continue;
|
|
161
|
+
for (const edge of input.graph.incoming.get(current) ?? []) {
|
|
162
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.from)) continue;
|
|
163
|
+
queue.push(edge.from);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
visited.delete(input.target);
|
|
167
|
+
return Array.from(visited).sort();
|
|
168
|
+
}
|
|
169
|
+
function explainPath(input) {
|
|
170
|
+
if (!input.graph.descriptorMap.has(input.from) || !input.graph.descriptorMap.has(input.to))
|
|
171
|
+
return [];
|
|
172
|
+
if (input.from === input.to) return [];
|
|
173
|
+
const allowedRelations = new Set(input.relationIds);
|
|
174
|
+
const visited = /* @__PURE__ */ new Set([input.from]);
|
|
175
|
+
const queue = [
|
|
176
|
+
{ node: input.from, path: [] }
|
|
177
|
+
];
|
|
178
|
+
while (queue.length) {
|
|
179
|
+
const current = queue.shift();
|
|
180
|
+
if (!current) continue;
|
|
181
|
+
for (const edge of input.graph.outgoing.get(current.node) ?? []) {
|
|
182
|
+
if (!allowedRelations.has(edge.relation) || visited.has(edge.to)) continue;
|
|
183
|
+
const nextPath = [
|
|
184
|
+
...current.path,
|
|
185
|
+
{
|
|
186
|
+
from: edge.from,
|
|
187
|
+
to: edge.to,
|
|
188
|
+
relation: edge.relation
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
if (edge.to === input.to) return nextPath;
|
|
192
|
+
visited.add(edge.to);
|
|
193
|
+
queue.push({
|
|
194
|
+
node: edge.to,
|
|
195
|
+
path: nextPath
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
function explainPathsBatch(input) {
|
|
202
|
+
return [...input.pairs].sort((left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to)).map(({ from, to }) => ({
|
|
203
|
+
from,
|
|
204
|
+
to,
|
|
205
|
+
path: explainPath({
|
|
206
|
+
graph: input.graph,
|
|
207
|
+
from,
|
|
208
|
+
to,
|
|
209
|
+
relationIds: input.relationIds
|
|
210
|
+
})
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
function getCompositionProvenance(input) {
|
|
214
|
+
const selected = [...new Set(input.selected)].filter((descriptorId) => input.graph.descriptorMap.has(descriptorId)).sort();
|
|
215
|
+
const baseDescriptors = [...new Set(input.baseDescriptors ?? [])].filter((descriptorId) => input.graph.descriptorMap.has(descriptorId)).sort();
|
|
216
|
+
const descriptorIds = [...new Set(input.descriptorIds)].filter((descriptorId) => input.graph.descriptorMap.has(descriptorId)).sort();
|
|
217
|
+
return descriptorIds.map((descriptorId) => {
|
|
218
|
+
const origins = [];
|
|
219
|
+
if (selected.includes(descriptorId)) {
|
|
220
|
+
origins.push({
|
|
221
|
+
originType: "selected",
|
|
222
|
+
path: []
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (baseDescriptors.includes(descriptorId)) {
|
|
226
|
+
origins.push({
|
|
227
|
+
originType: "base",
|
|
228
|
+
path: []
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const source of [...selected, ...baseDescriptors]) {
|
|
232
|
+
if (source === descriptorId) continue;
|
|
233
|
+
const path = explainPath({
|
|
234
|
+
graph: input.graph,
|
|
235
|
+
from: source,
|
|
236
|
+
to: descriptorId,
|
|
237
|
+
relationIds: input.relationIds
|
|
238
|
+
});
|
|
239
|
+
if (!path.length) continue;
|
|
240
|
+
origins.push({
|
|
241
|
+
originType: createOriginType(path),
|
|
242
|
+
path
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
descriptorId,
|
|
247
|
+
origins: origins.sort(compareOrigins)
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/compositionSelection.ts
|
|
253
|
+
var defaultCompositionPolicy = {
|
|
254
|
+
resolutionRelationIds: ["dependencies"],
|
|
255
|
+
provenanceRelationIds: ["dependencies"],
|
|
256
|
+
inspectionRelationIds: ["dependencies"]
|
|
257
|
+
};
|
|
258
|
+
function resolvePolicy(policy) {
|
|
259
|
+
return {
|
|
260
|
+
...defaultCompositionPolicy,
|
|
261
|
+
...policy,
|
|
262
|
+
resolutionRelationIds: policy?.resolutionRelationIds ?? defaultCompositionPolicy.resolutionRelationIds,
|
|
263
|
+
provenanceRelationIds: policy?.provenanceRelationIds ?? defaultCompositionPolicy.provenanceRelationIds,
|
|
264
|
+
inspectionRelationIds: policy?.inspectionRelationIds ?? defaultCompositionPolicy.inspectionRelationIds
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function createCompositionSelection(input) {
|
|
268
|
+
const catalog = input.catalog;
|
|
269
|
+
const descriptorMap = catalog.getDescriptorMap();
|
|
270
|
+
const selected = [...new Set(input.selected ?? [])].filter(Boolean).sort();
|
|
271
|
+
const baseDescriptors = [...new Set(input.baseDescriptors ?? [])].filter(Boolean).sort();
|
|
272
|
+
assertKnownDescriptorIds(descriptorMap, selected, "selected descriptors");
|
|
273
|
+
assertKnownDescriptorIds(descriptorMap, baseDescriptors, "base descriptors");
|
|
274
|
+
const policy = resolvePolicy(input.policy);
|
|
275
|
+
const resolved = catalog.getTransitiveTargets({
|
|
276
|
+
start: [...selected, ...baseDescriptors],
|
|
277
|
+
relationIds: policy.resolutionRelationIds
|
|
278
|
+
});
|
|
279
|
+
let resolvedDescriptorsCache;
|
|
280
|
+
let provenanceCache;
|
|
281
|
+
const getResolvedDescriptors = () => {
|
|
282
|
+
if (!resolvedDescriptorsCache) {
|
|
283
|
+
resolvedDescriptorsCache = resolved.map((descriptorId) => descriptorMap.get(descriptorId)).filter((descriptor) => Boolean(descriptor));
|
|
284
|
+
}
|
|
285
|
+
return resolvedDescriptorsCache;
|
|
286
|
+
};
|
|
287
|
+
const getProvenance = () => {
|
|
288
|
+
if (!provenanceCache) {
|
|
289
|
+
provenanceCache = getCompositionProvenance({
|
|
290
|
+
graph: catalog.getGraph(),
|
|
291
|
+
descriptorIds: resolved,
|
|
292
|
+
selected,
|
|
293
|
+
baseDescriptors,
|
|
294
|
+
relationIds: policy.provenanceRelationIds
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return provenanceCache;
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
getCatalog: () => catalog,
|
|
301
|
+
getGraph: () => catalog.getGraph(),
|
|
302
|
+
getSelected: () => [...selected],
|
|
303
|
+
getBaseDescriptors: () => [...baseDescriptors],
|
|
304
|
+
getResolved: () => [...resolved],
|
|
305
|
+
getResolvedDescriptors,
|
|
306
|
+
getProvenance,
|
|
307
|
+
getDependentsFor: (target, dependentsInput = {}) => {
|
|
308
|
+
return catalog.getDependents({
|
|
309
|
+
target,
|
|
310
|
+
relationIds: dependentsInput.relationIds ?? policy.resolutionRelationIds,
|
|
311
|
+
...dependentsInput.transitive !== void 0 ? { transitive: dependentsInput.transitive } : {}
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
explain: (explainInput) => {
|
|
315
|
+
return catalog.explain({
|
|
316
|
+
from: explainInput.from,
|
|
317
|
+
to: explainInput.to,
|
|
318
|
+
relationIds: explainInput.relationIds ?? policy.inspectionRelationIds
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
explainPathsBatch: (batchInput) => {
|
|
322
|
+
return catalog.explainPathsBatch({
|
|
323
|
+
pairs: batchInput.pairs,
|
|
324
|
+
relationIds: batchInput.relationIds ?? policy.inspectionRelationIds
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/descriptorCatalog.ts
|
|
331
|
+
function createRelationRecord(relationIds, values) {
|
|
332
|
+
return Object.fromEntries(
|
|
333
|
+
relationIds.map((relationId) => {
|
|
334
|
+
const entry = values.find(([relation]) => relation === relationId);
|
|
335
|
+
return [relationId, entry?.[1] ?? []];
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
function resolveRelationDescriptors(relationDescriptors) {
|
|
340
|
+
const registry = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const relationDescriptor of defaultRelationDescriptors) {
|
|
342
|
+
registry.set(relationDescriptor.id, relationDescriptor);
|
|
343
|
+
}
|
|
344
|
+
for (const relationDescriptor of relationDescriptors ?? []) {
|
|
345
|
+
registry.set(relationDescriptor.id, relationDescriptor);
|
|
346
|
+
}
|
|
347
|
+
return Array.from(registry.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
348
|
+
}
|
|
349
|
+
function buildDescriptorProfile(descriptor, relationIds, graph) {
|
|
350
|
+
const outgoing = createRelationRecord(
|
|
351
|
+
relationIds,
|
|
352
|
+
relationIds.map((relationId) => {
|
|
353
|
+
const targets = (graph.outgoing.get(descriptor.id) ?? []).filter((edge) => edge.relation === relationId).map((edge) => edge.to).sort();
|
|
354
|
+
return [relationId, targets];
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
const incoming = createRelationRecord(
|
|
358
|
+
relationIds,
|
|
359
|
+
relationIds.map((relationId) => {
|
|
360
|
+
const sources = (graph.incoming.get(descriptor.id) ?? []).filter((edge) => edge.relation === relationId).map((edge) => edge.from).sort();
|
|
361
|
+
return [relationId, sources];
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
return {
|
|
365
|
+
id: descriptor.id,
|
|
366
|
+
disabled: descriptor.disabled === true,
|
|
367
|
+
capabilities: [...descriptor.capabilities ?? []].sort(),
|
|
368
|
+
...descriptor.location ? { location: descriptor.location } : {},
|
|
369
|
+
...typeof descriptor.providesFor === "string" ? { providesFor: descriptor.providesFor } : {},
|
|
370
|
+
outgoing,
|
|
371
|
+
incoming
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function createDescriptorCatalog(input) {
|
|
375
|
+
const descriptorMap = input.descriptorMap ?? buildDescriptorMap(input.descriptors ?? []);
|
|
376
|
+
const relationDescriptors = resolveRelationDescriptors(input.relationDescriptors);
|
|
377
|
+
const graph = buildDescriptorGraph({
|
|
378
|
+
descriptorMap,
|
|
379
|
+
relationDescriptors
|
|
380
|
+
});
|
|
381
|
+
const catalog = {
|
|
382
|
+
getDescriptorMap: () => descriptorMap,
|
|
383
|
+
getAllDescriptors: () => Array.from(descriptorMap.values()),
|
|
384
|
+
getDescriptor: (id) => descriptorMap.get(id),
|
|
385
|
+
getGraph: () => graph,
|
|
386
|
+
getRelationDescriptors: () => Array.from(graph.relationDescriptors.values()),
|
|
387
|
+
getProfiles: (profileInput = {}) => {
|
|
388
|
+
const ids = [...new Set(profileInput.ids ?? [])].filter(Boolean).sort();
|
|
389
|
+
const descriptors = ids.length ? ids.map((id) => descriptorMap.get(id)).filter((descriptor) => Boolean(descriptor)) : Array.from(descriptorMap.values()).filter(
|
|
390
|
+
(descriptor) => profileInput.includeDisabled === true || descriptor.disabled !== true
|
|
391
|
+
).sort((left, right) => left.id.localeCompare(right.id));
|
|
392
|
+
return descriptors.map(
|
|
393
|
+
(descriptor) => buildDescriptorProfile(descriptor, Array.from(graph.relationDescriptors.keys()), graph)
|
|
394
|
+
);
|
|
395
|
+
},
|
|
396
|
+
getIncomingRelationMap: (relationId) => getIncomingRelationMap({ graph, relationId }),
|
|
397
|
+
getTransitiveTargets: (transitiveInput) => getTransitiveTargets({
|
|
398
|
+
graph,
|
|
399
|
+
start: transitiveInput.start,
|
|
400
|
+
relationIds: transitiveInput.relationIds
|
|
401
|
+
}),
|
|
402
|
+
getDependents: (dependentsInput) => getDependents({
|
|
403
|
+
graph,
|
|
404
|
+
target: dependentsInput.target,
|
|
405
|
+
relationIds: dependentsInput.relationIds,
|
|
406
|
+
...dependentsInput.transitive !== void 0 ? { transitive: dependentsInput.transitive } : {}
|
|
407
|
+
}),
|
|
408
|
+
explain: (explainInput) => explainPath({
|
|
409
|
+
graph,
|
|
410
|
+
from: explainInput.from,
|
|
411
|
+
to: explainInput.to,
|
|
412
|
+
relationIds: explainInput.relationIds
|
|
413
|
+
}),
|
|
414
|
+
explainPathsBatch: (batchInput) => explainPathsBatch({
|
|
415
|
+
graph,
|
|
416
|
+
pairs: batchInput.pairs,
|
|
417
|
+
relationIds: batchInput.relationIds
|
|
418
|
+
}),
|
|
419
|
+
resolveSelection: (selectionInput) => createCompositionSelection({
|
|
420
|
+
catalog,
|
|
421
|
+
...selectionInput
|
|
422
|
+
})
|
|
423
|
+
};
|
|
424
|
+
return catalog;
|
|
425
|
+
}
|
|
426
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
427
|
+
0 && (module.exports = {
|
|
428
|
+
assertKnownDescriptorIds,
|
|
429
|
+
buildDescriptorGraph,
|
|
430
|
+
buildDescriptorMap,
|
|
431
|
+
createCompositionSelection,
|
|
432
|
+
createDescriptorCatalog,
|
|
433
|
+
defaultCompositionPolicy,
|
|
434
|
+
defaultRelationDescriptors,
|
|
435
|
+
explainPath,
|
|
436
|
+
explainPathsBatch,
|
|
437
|
+
getCompositionProvenance,
|
|
438
|
+
getDependents,
|
|
439
|
+
getIncomingRelationMap,
|
|
440
|
+
getTransitiveTargets,
|
|
441
|
+
parseDescriptorIds
|
|
442
|
+
});
|