@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
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ const catalog = createDescriptorCatalog({
|
|
|
71
71
|
{
|
|
72
72
|
id: 'payment-provider-stripe',
|
|
73
73
|
version: '1.0.0',
|
|
74
|
+
defaultFor: 'payment-checkout',
|
|
74
75
|
providesFor: 'payment-checkout',
|
|
75
76
|
dependencies: { payments: '^1.0.0' },
|
|
76
77
|
},
|
|
@@ -134,6 +135,33 @@ selection.getResolved();
|
|
|
134
135
|
// => ['checkout', 'payments', 'shops', 'web']
|
|
135
136
|
```
|
|
136
137
|
|
|
138
|
+
## Example: normalize selection seeds
|
|
139
|
+
|
|
140
|
+
Host adapters often accept descriptor selections from CLI flags, package-manager
|
|
141
|
+
config variables, environment variables, or static defaults. Use
|
|
142
|
+
`resolveDescriptorSelectionSeed()` at that boundary and pass the resulting ids to
|
|
143
|
+
the catalog or framework adapter.
|
|
144
|
+
|
|
145
|
+
The graph package does not prescribe a domain word for selection. Pass `key`
|
|
146
|
+
when you want derived CLI and environment names, or pass explicit `cliKeys` and
|
|
147
|
+
`envKeys`. Framework adapters such as `@lorion-org/nuxt` and
|
|
148
|
+
`@lorion-org/react` provide their own `capability` default.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { resolveDescriptorSelectionSeed } from '@lorion-org/composition-graph';
|
|
152
|
+
|
|
153
|
+
const selected = resolveDescriptorSelectionSeed({
|
|
154
|
+
argv: process.argv,
|
|
155
|
+
defaultValue: ['default'],
|
|
156
|
+
env: process.env,
|
|
157
|
+
key: 'capability',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --capabilities=admin,checkout
|
|
161
|
+
// LORION_CAPABILITIES="admin checkout"
|
|
162
|
+
// => ['admin', 'checkout']
|
|
163
|
+
```
|
|
164
|
+
|
|
137
165
|
## Example: explain why something is present
|
|
138
166
|
|
|
139
167
|
The catalog and selection APIs expose path explanation helpers for diagnostics,
|
|
@@ -197,16 +225,29 @@ const catalog = createDescriptorCatalog({
|
|
|
197
225
|
- `dependencies` is the only built-in relation
|
|
198
226
|
- every additional relation must be registered via `relationDescriptors`
|
|
199
227
|
- unconfigured descriptor fields are ignored by the graph
|
|
200
|
-
-
|
|
228
|
+
- selection seed parsing belongs at the host adapter boundary
|
|
201
229
|
- nested descriptor authoring belongs in a discovery or normalization layer, not in this package
|
|
202
230
|
|
|
203
231
|
`relationDescriptors` are intentionally small:
|
|
204
232
|
|
|
205
233
|
- `id` identifies the relation in graph queries and policies
|
|
206
234
|
- `field` optionally maps the relation to a descriptor field name
|
|
235
|
+
- `targetMode: 'values'` reads object values instead of object keys
|
|
236
|
+
- `direction: 'incoming'` builds inverse edges from a target field back to the descriptor
|
|
237
|
+
|
|
238
|
+
The core graph does not prescribe provider policy, hinting, or weighting. Those
|
|
239
|
+
concerns belong in higher layers. A framework adapter can, for example, register
|
|
240
|
+
an inverse `defaultProviders` relation for provider-owned defaults:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const catalog = createDescriptorCatalog({
|
|
244
|
+
descriptors,
|
|
245
|
+
relationDescriptors: [{ id: 'defaultProviders', field: 'defaultFor', direction: 'incoming' }],
|
|
246
|
+
});
|
|
247
|
+
```
|
|
207
248
|
|
|
208
|
-
|
|
209
|
-
|
|
249
|
+
That turns `{ id: 'keycloak', defaultFor: 'auth' }` into a graph edge
|
|
250
|
+
`auth -> keycloak`, as long as both descriptors exist.
|
|
210
251
|
|
|
211
252
|
Dependency-specific projections are also intentionally outside the core. If a
|
|
212
253
|
consumer needs a "what pulled this in?" view, derive that from
|
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
getDependents: () => getDependents,
|
|
34
34
|
getIncomingRelationMap: () => getIncomingRelationMap,
|
|
35
35
|
getTransitiveTargets: () => getTransitiveTargets,
|
|
36
|
-
parseDescriptorIds: () => parseDescriptorIds
|
|
36
|
+
parseDescriptorIds: () => parseDescriptorIds,
|
|
37
|
+
resolveDescriptorSelectionSeed: () => resolveDescriptorSelectionSeed
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
|
|
@@ -46,10 +47,66 @@ function buildDescriptorMap(descriptors) {
|
|
|
46
47
|
return descriptorMap;
|
|
47
48
|
}
|
|
48
49
|
function parseDescriptorIds(input) {
|
|
49
|
-
if (
|
|
50
|
-
|
|
50
|
+
if (typeof input === "string") {
|
|
51
|
+
return Array.from(
|
|
52
|
+
new Set(
|
|
53
|
+
input.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean)
|
|
54
|
+
)
|
|
55
|
+
).sort();
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(input)) return [];
|
|
58
|
+
const items = input.flatMap((item) => typeof item === "string" ? parseDescriptorIds(item) : []);
|
|
51
59
|
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean))).sort();
|
|
52
60
|
}
|
|
61
|
+
function readCliValue(argv, key) {
|
|
62
|
+
const arg = argv.find((entry) => entry === key || entry.startsWith(`${key}=`));
|
|
63
|
+
if (!arg) return void 0;
|
|
64
|
+
if (arg === key) {
|
|
65
|
+
const value = argv[argv.indexOf(arg) + 1];
|
|
66
|
+
return value?.startsWith("-") ? void 0 : value;
|
|
67
|
+
}
|
|
68
|
+
return arg.slice(`${key}=`.length);
|
|
69
|
+
}
|
|
70
|
+
function firstNonEmptyValue(values) {
|
|
71
|
+
return values.find((value) => {
|
|
72
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
73
|
+
return Array.isArray(value) && value.some((entry) => typeof entry === "string" && entry.trim());
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function pluralizeSelectionKeySegment(value) {
|
|
77
|
+
if (value.endsWith("s")) return value;
|
|
78
|
+
if (value.endsWith("y")) return `${value.slice(0, -1)}ies`;
|
|
79
|
+
return `${value}s`;
|
|
80
|
+
}
|
|
81
|
+
function normalizeSelectionKey(value) {
|
|
82
|
+
const segments = value.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase().split("-").filter(Boolean);
|
|
83
|
+
if (!segments.length) return "";
|
|
84
|
+
const lastSegment = segments[segments.length - 1];
|
|
85
|
+
segments[segments.length - 1] = pluralizeSelectionKeySegment(lastSegment);
|
|
86
|
+
return segments.join("-");
|
|
87
|
+
}
|
|
88
|
+
function toEnvKey(value) {
|
|
89
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
90
|
+
}
|
|
91
|
+
function resolveCliKeys(input) {
|
|
92
|
+
if (input.cliKeys) return input.cliKeys;
|
|
93
|
+
const key = input.key ? normalizeSelectionKey(input.key) : "";
|
|
94
|
+
return key ? [`--${key}`] : [];
|
|
95
|
+
}
|
|
96
|
+
function resolveEnvKeys(input) {
|
|
97
|
+
if (input.envKeys) return input.envKeys;
|
|
98
|
+
const key = input.key ? normalizeSelectionKey(input.key) : "";
|
|
99
|
+
return key ? [`npm_config_${key.replaceAll("-", "_")}`, `LORION_${toEnvKey(key)}`] : [];
|
|
100
|
+
}
|
|
101
|
+
function resolveDescriptorSelectionSeed(input = {}) {
|
|
102
|
+
return parseDescriptorIds(
|
|
103
|
+
firstNonEmptyValue([
|
|
104
|
+
...resolveCliKeys(input).map((key) => readCliValue(input.argv ?? [], key)),
|
|
105
|
+
...resolveEnvKeys(input).map((key) => input.env?.[key]),
|
|
106
|
+
input.defaultValue
|
|
107
|
+
])
|
|
108
|
+
);
|
|
109
|
+
}
|
|
53
110
|
function assertKnownDescriptorIds(descriptorMap, ids, label) {
|
|
54
111
|
const missing = [...new Set(ids)].filter((id) => id && !descriptorMap.has(id)).sort();
|
|
55
112
|
if (!missing.length) return;
|
|
@@ -73,14 +130,14 @@ var defaultRelationDescriptors = [
|
|
|
73
130
|
field: "dependencies"
|
|
74
131
|
}
|
|
75
132
|
];
|
|
76
|
-
function
|
|
133
|
+
function isRecord(value) {
|
|
77
134
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
135
|
}
|
|
79
136
|
function getRelationTargets(descriptor, relationDescriptor) {
|
|
80
137
|
const field = relationDescriptor.field ?? relationDescriptor.id;
|
|
81
138
|
const relationValue = descriptor[field];
|
|
82
|
-
|
|
83
|
-
return
|
|
139
|
+
const targets = Array.isArray(relationValue) ? relationValue : typeof relationValue === "string" ? [relationValue] : isRecord(relationValue) ? relationDescriptor.targetMode === "values" ? Object.values(relationValue) : Object.keys(relationValue) : [];
|
|
140
|
+
return targets.filter((target) => typeof target === "string" && target.length > 0).sort();
|
|
84
141
|
}
|
|
85
142
|
function createOriginType(path) {
|
|
86
143
|
return path.length ? "resolved" : "selected";
|
|
@@ -104,15 +161,16 @@ function buildDescriptorGraph(input) {
|
|
|
104
161
|
for (const relationDescriptor of descriptorRegistry.values()) {
|
|
105
162
|
for (const target of getRelationTargets(descriptor, relationDescriptor)) {
|
|
106
163
|
if (!input.descriptorMap.has(target)) continue;
|
|
164
|
+
const isIncoming = relationDescriptor.direction === "incoming";
|
|
107
165
|
const edge = {
|
|
108
|
-
from: descriptor.id,
|
|
109
|
-
to: target,
|
|
166
|
+
from: isIncoming ? target : descriptor.id,
|
|
167
|
+
to: isIncoming ? descriptor.id : target,
|
|
110
168
|
relation: relationDescriptor.id,
|
|
111
169
|
source: "descriptor"
|
|
112
170
|
};
|
|
113
171
|
edges.push(edge);
|
|
114
|
-
outgoing.get(
|
|
115
|
-
incoming.get(
|
|
172
|
+
outgoing.get(edge.from)?.push(edge);
|
|
173
|
+
incoming.get(edge.to)?.push(edge);
|
|
116
174
|
}
|
|
117
175
|
}
|
|
118
176
|
}
|
|
@@ -366,7 +424,7 @@ function buildDescriptorProfile(descriptor, relationIds, graph) {
|
|
|
366
424
|
disabled: descriptor.disabled === true,
|
|
367
425
|
capabilities: [...descriptor.capabilities ?? []].sort(),
|
|
368
426
|
...descriptor.location ? { location: descriptor.location } : {},
|
|
369
|
-
...typeof descriptor.providesFor === "string" ? { providesFor: descriptor.providesFor } : {},
|
|
427
|
+
...typeof descriptor.providesFor === "string" || Array.isArray(descriptor.providesFor) ? { providesFor: descriptor.providesFor } : {},
|
|
370
428
|
outgoing,
|
|
371
429
|
incoming
|
|
372
430
|
};
|
|
@@ -438,5 +496,6 @@ function createDescriptorCatalog(input) {
|
|
|
438
496
|
getDependents,
|
|
439
497
|
getIncomingRelationMap,
|
|
440
498
|
getTransitiveTargets,
|
|
441
|
-
parseDescriptorIds
|
|
499
|
+
parseDescriptorIds,
|
|
500
|
+
resolveDescriptorSelectionSeed
|
|
442
501
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -5,7 +5,8 @@ type RelationId = string;
|
|
|
5
5
|
type Descriptor = {
|
|
6
6
|
id: DescriptorId;
|
|
7
7
|
version: string;
|
|
8
|
-
providesFor?:
|
|
8
|
+
providesFor?: DescriptorId | DescriptorId[];
|
|
9
|
+
defaultFor?: DescriptorId | DescriptorId[];
|
|
9
10
|
capabilities?: string[];
|
|
10
11
|
dependencies?: VersionConstraintMap;
|
|
11
12
|
disabled?: boolean;
|
|
@@ -14,8 +15,10 @@ type Descriptor = {
|
|
|
14
15
|
};
|
|
15
16
|
type DescriptorMap = Map<DescriptorId, Descriptor>;
|
|
16
17
|
type RelationDescriptor = {
|
|
18
|
+
direction?: 'outgoing' | 'incoming';
|
|
17
19
|
id: RelationId;
|
|
18
20
|
field?: string;
|
|
21
|
+
targetMode?: 'keys' | 'values';
|
|
19
22
|
};
|
|
20
23
|
type DescriptorEdge = {
|
|
21
24
|
from: DescriptorId;
|
|
@@ -48,7 +51,7 @@ type DescriptorProfile = {
|
|
|
48
51
|
id: DescriptorId;
|
|
49
52
|
location?: string;
|
|
50
53
|
disabled: boolean;
|
|
51
|
-
providesFor?:
|
|
54
|
+
providesFor?: DescriptorId | DescriptorId[];
|
|
52
55
|
capabilities: string[];
|
|
53
56
|
outgoing: Record<RelationId, string[]>;
|
|
54
57
|
incoming: Record<RelationId, string[]>;
|
|
@@ -130,8 +133,17 @@ type CompositionSelection = {
|
|
|
130
133
|
}>;
|
|
131
134
|
};
|
|
132
135
|
|
|
136
|
+
type DescriptorSelectionSeedInput = {
|
|
137
|
+
argv?: string[];
|
|
138
|
+
cliKeys?: string[];
|
|
139
|
+
defaultValue?: DescriptorId[] | string;
|
|
140
|
+
env?: Record<string, string | undefined>;
|
|
141
|
+
envKeys?: string[];
|
|
142
|
+
key?: string;
|
|
143
|
+
};
|
|
133
144
|
declare function buildDescriptorMap(descriptors: Iterable<Descriptor>): DescriptorMap;
|
|
134
|
-
declare function parseDescriptorIds(input?:
|
|
145
|
+
declare function parseDescriptorIds(input?: unknown): DescriptorId[];
|
|
146
|
+
declare function resolveDescriptorSelectionSeed(input?: DescriptorSelectionSeedInput): DescriptorId[];
|
|
135
147
|
declare function assertKnownDescriptorIds(descriptorMap: DescriptorMap, ids: DescriptorId[], label: string): void;
|
|
136
148
|
|
|
137
149
|
declare const defaultRelationDescriptors: RelationDescriptor[];
|
|
@@ -194,4 +206,4 @@ declare function createCompositionSelection(input: {
|
|
|
194
206
|
policy?: Partial<CompositionPolicy>;
|
|
195
207
|
}): CompositionSelection;
|
|
196
208
|
|
|
197
|
-
export { type CompositionOriginType, type CompositionPolicy, type CompositionProvenance, type CompositionProvenanceOrigin, type CompositionSelection, type Descriptor, type DescriptorCatalog, type DescriptorEdge, type DescriptorGraph, type DescriptorId, type DescriptorIds, type DescriptorMap, type DescriptorProfile, type RelationDescriptor, type RelationId, type ResolutionStep, type VersionConstraintMap, assertKnownDescriptorIds, buildDescriptorGraph, buildDescriptorMap, createCompositionSelection, createDescriptorCatalog, defaultCompositionPolicy, defaultRelationDescriptors, explainPath, explainPathsBatch, getCompositionProvenance, getDependents, getIncomingRelationMap, getTransitiveTargets, parseDescriptorIds };
|
|
209
|
+
export { type CompositionOriginType, type CompositionPolicy, type CompositionProvenance, type CompositionProvenanceOrigin, type CompositionSelection, type Descriptor, type DescriptorCatalog, type DescriptorEdge, type DescriptorGraph, type DescriptorId, type DescriptorIds, type DescriptorMap, type DescriptorProfile, type DescriptorSelectionSeedInput, type RelationDescriptor, type RelationId, type ResolutionStep, type VersionConstraintMap, assertKnownDescriptorIds, buildDescriptorGraph, buildDescriptorMap, createCompositionSelection, createDescriptorCatalog, defaultCompositionPolicy, defaultRelationDescriptors, explainPath, explainPathsBatch, getCompositionProvenance, getDependents, getIncomingRelationMap, getTransitiveTargets, parseDescriptorIds, resolveDescriptorSelectionSeed };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ type RelationId = string;
|
|
|
5
5
|
type Descriptor = {
|
|
6
6
|
id: DescriptorId;
|
|
7
7
|
version: string;
|
|
8
|
-
providesFor?:
|
|
8
|
+
providesFor?: DescriptorId | DescriptorId[];
|
|
9
|
+
defaultFor?: DescriptorId | DescriptorId[];
|
|
9
10
|
capabilities?: string[];
|
|
10
11
|
dependencies?: VersionConstraintMap;
|
|
11
12
|
disabled?: boolean;
|
|
@@ -14,8 +15,10 @@ type Descriptor = {
|
|
|
14
15
|
};
|
|
15
16
|
type DescriptorMap = Map<DescriptorId, Descriptor>;
|
|
16
17
|
type RelationDescriptor = {
|
|
18
|
+
direction?: 'outgoing' | 'incoming';
|
|
17
19
|
id: RelationId;
|
|
18
20
|
field?: string;
|
|
21
|
+
targetMode?: 'keys' | 'values';
|
|
19
22
|
};
|
|
20
23
|
type DescriptorEdge = {
|
|
21
24
|
from: DescriptorId;
|
|
@@ -48,7 +51,7 @@ type DescriptorProfile = {
|
|
|
48
51
|
id: DescriptorId;
|
|
49
52
|
location?: string;
|
|
50
53
|
disabled: boolean;
|
|
51
|
-
providesFor?:
|
|
54
|
+
providesFor?: DescriptorId | DescriptorId[];
|
|
52
55
|
capabilities: string[];
|
|
53
56
|
outgoing: Record<RelationId, string[]>;
|
|
54
57
|
incoming: Record<RelationId, string[]>;
|
|
@@ -130,8 +133,17 @@ type CompositionSelection = {
|
|
|
130
133
|
}>;
|
|
131
134
|
};
|
|
132
135
|
|
|
136
|
+
type DescriptorSelectionSeedInput = {
|
|
137
|
+
argv?: string[];
|
|
138
|
+
cliKeys?: string[];
|
|
139
|
+
defaultValue?: DescriptorId[] | string;
|
|
140
|
+
env?: Record<string, string | undefined>;
|
|
141
|
+
envKeys?: string[];
|
|
142
|
+
key?: string;
|
|
143
|
+
};
|
|
133
144
|
declare function buildDescriptorMap(descriptors: Iterable<Descriptor>): DescriptorMap;
|
|
134
|
-
declare function parseDescriptorIds(input?:
|
|
145
|
+
declare function parseDescriptorIds(input?: unknown): DescriptorId[];
|
|
146
|
+
declare function resolveDescriptorSelectionSeed(input?: DescriptorSelectionSeedInput): DescriptorId[];
|
|
135
147
|
declare function assertKnownDescriptorIds(descriptorMap: DescriptorMap, ids: DescriptorId[], label: string): void;
|
|
136
148
|
|
|
137
149
|
declare const defaultRelationDescriptors: RelationDescriptor[];
|
|
@@ -194,4 +206,4 @@ declare function createCompositionSelection(input: {
|
|
|
194
206
|
policy?: Partial<CompositionPolicy>;
|
|
195
207
|
}): CompositionSelection;
|
|
196
208
|
|
|
197
|
-
export { type CompositionOriginType, type CompositionPolicy, type CompositionProvenance, type CompositionProvenanceOrigin, type CompositionSelection, type Descriptor, type DescriptorCatalog, type DescriptorEdge, type DescriptorGraph, type DescriptorId, type DescriptorIds, type DescriptorMap, type DescriptorProfile, type RelationDescriptor, type RelationId, type ResolutionStep, type VersionConstraintMap, assertKnownDescriptorIds, buildDescriptorGraph, buildDescriptorMap, createCompositionSelection, createDescriptorCatalog, defaultCompositionPolicy, defaultRelationDescriptors, explainPath, explainPathsBatch, getCompositionProvenance, getDependents, getIncomingRelationMap, getTransitiveTargets, parseDescriptorIds };
|
|
209
|
+
export { type CompositionOriginType, type CompositionPolicy, type CompositionProvenance, type CompositionProvenanceOrigin, type CompositionSelection, type Descriptor, type DescriptorCatalog, type DescriptorEdge, type DescriptorGraph, type DescriptorId, type DescriptorIds, type DescriptorMap, type DescriptorProfile, type DescriptorSelectionSeedInput, type RelationDescriptor, type RelationId, type ResolutionStep, type VersionConstraintMap, assertKnownDescriptorIds, buildDescriptorGraph, buildDescriptorMap, createCompositionSelection, createDescriptorCatalog, defaultCompositionPolicy, defaultRelationDescriptors, explainPath, explainPathsBatch, getCompositionProvenance, getDependents, getIncomingRelationMap, getTransitiveTargets, parseDescriptorIds, resolveDescriptorSelectionSeed };
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,66 @@ function buildDescriptorMap(descriptors) {
|
|
|
7
7
|
return descriptorMap;
|
|
8
8
|
}
|
|
9
9
|
function parseDescriptorIds(input) {
|
|
10
|
-
if (
|
|
11
|
-
|
|
10
|
+
if (typeof input === "string") {
|
|
11
|
+
return Array.from(
|
|
12
|
+
new Set(
|
|
13
|
+
input.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean)
|
|
14
|
+
)
|
|
15
|
+
).sort();
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(input)) return [];
|
|
18
|
+
const items = input.flatMap((item) => typeof item === "string" ? parseDescriptorIds(item) : []);
|
|
12
19
|
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean))).sort();
|
|
13
20
|
}
|
|
21
|
+
function readCliValue(argv, key) {
|
|
22
|
+
const arg = argv.find((entry) => entry === key || entry.startsWith(`${key}=`));
|
|
23
|
+
if (!arg) return void 0;
|
|
24
|
+
if (arg === key) {
|
|
25
|
+
const value = argv[argv.indexOf(arg) + 1];
|
|
26
|
+
return value?.startsWith("-") ? void 0 : value;
|
|
27
|
+
}
|
|
28
|
+
return arg.slice(`${key}=`.length);
|
|
29
|
+
}
|
|
30
|
+
function firstNonEmptyValue(values) {
|
|
31
|
+
return values.find((value) => {
|
|
32
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
33
|
+
return Array.isArray(value) && value.some((entry) => typeof entry === "string" && entry.trim());
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function pluralizeSelectionKeySegment(value) {
|
|
37
|
+
if (value.endsWith("s")) return value;
|
|
38
|
+
if (value.endsWith("y")) return `${value.slice(0, -1)}ies`;
|
|
39
|
+
return `${value}s`;
|
|
40
|
+
}
|
|
41
|
+
function normalizeSelectionKey(value) {
|
|
42
|
+
const segments = value.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase().split("-").filter(Boolean);
|
|
43
|
+
if (!segments.length) return "";
|
|
44
|
+
const lastSegment = segments[segments.length - 1];
|
|
45
|
+
segments[segments.length - 1] = pluralizeSelectionKeySegment(lastSegment);
|
|
46
|
+
return segments.join("-");
|
|
47
|
+
}
|
|
48
|
+
function toEnvKey(value) {
|
|
49
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
function resolveCliKeys(input) {
|
|
52
|
+
if (input.cliKeys) return input.cliKeys;
|
|
53
|
+
const key = input.key ? normalizeSelectionKey(input.key) : "";
|
|
54
|
+
return key ? [`--${key}`] : [];
|
|
55
|
+
}
|
|
56
|
+
function resolveEnvKeys(input) {
|
|
57
|
+
if (input.envKeys) return input.envKeys;
|
|
58
|
+
const key = input.key ? normalizeSelectionKey(input.key) : "";
|
|
59
|
+
return key ? [`npm_config_${key.replaceAll("-", "_")}`, `LORION_${toEnvKey(key)}`] : [];
|
|
60
|
+
}
|
|
61
|
+
function resolveDescriptorSelectionSeed(input = {}) {
|
|
62
|
+
return parseDescriptorIds(
|
|
63
|
+
firstNonEmptyValue([
|
|
64
|
+
...resolveCliKeys(input).map((key) => readCliValue(input.argv ?? [], key)),
|
|
65
|
+
...resolveEnvKeys(input).map((key) => input.env?.[key]),
|
|
66
|
+
input.defaultValue
|
|
67
|
+
])
|
|
68
|
+
);
|
|
69
|
+
}
|
|
14
70
|
function assertKnownDescriptorIds(descriptorMap, ids, label) {
|
|
15
71
|
const missing = [...new Set(ids)].filter((id) => id && !descriptorMap.has(id)).sort();
|
|
16
72
|
if (!missing.length) return;
|
|
@@ -34,14 +90,14 @@ var defaultRelationDescriptors = [
|
|
|
34
90
|
field: "dependencies"
|
|
35
91
|
}
|
|
36
92
|
];
|
|
37
|
-
function
|
|
93
|
+
function isRecord(value) {
|
|
38
94
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
95
|
}
|
|
40
96
|
function getRelationTargets(descriptor, relationDescriptor) {
|
|
41
97
|
const field = relationDescriptor.field ?? relationDescriptor.id;
|
|
42
98
|
const relationValue = descriptor[field];
|
|
43
|
-
|
|
44
|
-
return
|
|
99
|
+
const targets = Array.isArray(relationValue) ? relationValue : typeof relationValue === "string" ? [relationValue] : isRecord(relationValue) ? relationDescriptor.targetMode === "values" ? Object.values(relationValue) : Object.keys(relationValue) : [];
|
|
100
|
+
return targets.filter((target) => typeof target === "string" && target.length > 0).sort();
|
|
45
101
|
}
|
|
46
102
|
function createOriginType(path) {
|
|
47
103
|
return path.length ? "resolved" : "selected";
|
|
@@ -65,15 +121,16 @@ function buildDescriptorGraph(input) {
|
|
|
65
121
|
for (const relationDescriptor of descriptorRegistry.values()) {
|
|
66
122
|
for (const target of getRelationTargets(descriptor, relationDescriptor)) {
|
|
67
123
|
if (!input.descriptorMap.has(target)) continue;
|
|
124
|
+
const isIncoming = relationDescriptor.direction === "incoming";
|
|
68
125
|
const edge = {
|
|
69
|
-
from: descriptor.id,
|
|
70
|
-
to: target,
|
|
126
|
+
from: isIncoming ? target : descriptor.id,
|
|
127
|
+
to: isIncoming ? descriptor.id : target,
|
|
71
128
|
relation: relationDescriptor.id,
|
|
72
129
|
source: "descriptor"
|
|
73
130
|
};
|
|
74
131
|
edges.push(edge);
|
|
75
|
-
outgoing.get(
|
|
76
|
-
incoming.get(
|
|
132
|
+
outgoing.get(edge.from)?.push(edge);
|
|
133
|
+
incoming.get(edge.to)?.push(edge);
|
|
77
134
|
}
|
|
78
135
|
}
|
|
79
136
|
}
|
|
@@ -327,7 +384,7 @@ function buildDescriptorProfile(descriptor, relationIds, graph) {
|
|
|
327
384
|
disabled: descriptor.disabled === true,
|
|
328
385
|
capabilities: [...descriptor.capabilities ?? []].sort(),
|
|
329
386
|
...descriptor.location ? { location: descriptor.location } : {},
|
|
330
|
-
...typeof descriptor.providesFor === "string" ? { providesFor: descriptor.providesFor } : {},
|
|
387
|
+
...typeof descriptor.providesFor === "string" || Array.isArray(descriptor.providesFor) ? { providesFor: descriptor.providesFor } : {},
|
|
331
388
|
outgoing,
|
|
332
389
|
incoming
|
|
333
390
|
};
|
|
@@ -398,5 +455,6 @@ export {
|
|
|
398
455
|
getDependents,
|
|
399
456
|
getIncomingRelationMap,
|
|
400
457
|
getTransitiveTargets,
|
|
401
|
-
parseDescriptorIds
|
|
458
|
+
parseDescriptorIds,
|
|
459
|
+
resolveDescriptorSelectionSeed
|
|
402
460
|
};
|
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
|
+
}
|