@lorion-org/descriptor-selection 1.0.0-beta.3
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 +48 -0
- package/dist/index.cjs +138 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +114 -0
- package/package.json +69 -0
- package/src/index.ts +191 -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,48 @@
|
|
|
1
|
+
# @lorion-org/descriptor-selection
|
|
2
|
+
|
|
3
|
+
Framework-free, provider-aware descriptor selection.
|
|
4
|
+
|
|
5
|
+
Given a set of items that each carry a descriptor and a selection seed, it
|
|
6
|
+
resolves the active subset: it parses the seed (explicit selection, or CLI/env
|
|
7
|
+
with a default fallback), applies **one-provider-per-capability** selection,
|
|
8
|
+
builds the dependency graph, and returns the items reachable from the selection
|
|
9
|
+
and the always-on base — in their original order.
|
|
10
|
+
|
|
11
|
+
It is the shared selection brain: build-time bundler plugins, runtime hosts, and
|
|
12
|
+
framework adapters all reuse it instead of re-gluing the graph and provider
|
|
13
|
+
layers themselves.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
pnpm add @lorion-org/descriptor-selection
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
- `selectDescriptors({ items, getDescriptor, withDescriptor, seed, relationDescriptors?, policy? })`
|
|
24
|
+
resolves the active subset of `items`. It is generic over the item type via the
|
|
25
|
+
`getDescriptor` / `withDescriptor` accessors, so a "capability", an "extension",
|
|
26
|
+
or a plain descriptor record all work.
|
|
27
|
+
- `resolveDescriptorSelection(seed)` resolves just the selection ids from a seed.
|
|
28
|
+
- `applyProviderSelection({ items, selected, getDescriptor, withDescriptor })` applies one-provider-per-capability selection to a set of items (the step a host reuses when it drives its own graph resolution, e.g. the Nuxt adapter).
|
|
29
|
+
- `assertSingleDefaultProvider(descriptors)` throws if two descriptors claim
|
|
30
|
+
`defaultFor` the same capability.
|
|
31
|
+
- `providerRelationDescriptors`, `defaultResolutionRelations`, and
|
|
32
|
+
`descriptorSelectionPolicy(policy?)` expose the provider relations and the
|
|
33
|
+
default resolution policy.
|
|
34
|
+
|
|
35
|
+
## What It Is Not
|
|
36
|
+
|
|
37
|
+
- not a disk reader (see `@lorion-org/descriptor-discovery`)
|
|
38
|
+
- not a graph engine (see `@lorion-org/composition-graph`)
|
|
39
|
+
- not a host runtime or activation convention (see `@lorion-org/capability-composition`)
|
|
40
|
+
|
|
41
|
+
## Local Commands
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
cd packages/descriptor-selection
|
|
45
|
+
pnpm build
|
|
46
|
+
pnpm test
|
|
47
|
+
pnpm typecheck
|
|
48
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
applyProviderSelection: () => applyProviderSelection,
|
|
24
|
+
assertSingleDefaultProvider: () => assertSingleDefaultProvider,
|
|
25
|
+
defaultResolutionRelations: () => defaultResolutionRelations,
|
|
26
|
+
descriptorSelectionPolicy: () => descriptorSelectionPolicy,
|
|
27
|
+
providerRelationDescriptors: () => providerRelationDescriptors,
|
|
28
|
+
resolveDescriptorSelection: () => resolveDescriptorSelection,
|
|
29
|
+
selectDescriptors: () => selectDescriptors
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
var import_composition_graph = require("@lorion-org/composition-graph");
|
|
33
|
+
var import_provider_selection = require("@lorion-org/provider-selection");
|
|
34
|
+
var providerRelationDescriptors = [
|
|
35
|
+
{ direction: "incoming", field: "defaultFor", id: "defaultProviders" },
|
|
36
|
+
{ field: "providerPreferences", id: "providerPreferences", targetMode: "values" }
|
|
37
|
+
];
|
|
38
|
+
var defaultResolutionRelations = [
|
|
39
|
+
"dependencies",
|
|
40
|
+
"defaultProviders",
|
|
41
|
+
"providerPreferences"
|
|
42
|
+
];
|
|
43
|
+
function descriptorSelectionPolicy(policy) {
|
|
44
|
+
return {
|
|
45
|
+
...policy,
|
|
46
|
+
inspectionRelationIds: policy?.inspectionRelationIds ?? [...defaultResolutionRelations],
|
|
47
|
+
provenanceRelationIds: policy?.provenanceRelationIds ?? [...defaultResolutionRelations],
|
|
48
|
+
resolutionRelationIds: policy?.resolutionRelationIds ?? [...defaultResolutionRelations]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function resolveDescriptorSelection(seed) {
|
|
52
|
+
if (seed.selected?.length) return [...seed.selected];
|
|
53
|
+
if (seed.selectionSeed === false) return [...seed.defaultSelection ?? []];
|
|
54
|
+
const options = seed.selectionSeed ?? {};
|
|
55
|
+
const selected = (0, import_composition_graph.resolveDescriptorSelectionSeed)({
|
|
56
|
+
argv: options.argv ?? process.argv,
|
|
57
|
+
env: options.env ?? process.env,
|
|
58
|
+
key: options.key ?? "capability",
|
|
59
|
+
...options.cliKeys ? { cliKeys: options.cliKeys } : {},
|
|
60
|
+
...options.envKeys ? { envKeys: options.envKeys } : {}
|
|
61
|
+
});
|
|
62
|
+
return selected.length ? selected : [...seed.defaultSelection ?? []];
|
|
63
|
+
}
|
|
64
|
+
function assertSingleDefaultProvider(descriptors) {
|
|
65
|
+
const providersByCapability = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const descriptor of descriptors) {
|
|
67
|
+
const { defaultFor } = descriptor;
|
|
68
|
+
if (!defaultFor) continue;
|
|
69
|
+
for (const capabilityId of Array.isArray(defaultFor) ? defaultFor : [defaultFor]) {
|
|
70
|
+
const providers = providersByCapability.get(capabilityId) ?? [];
|
|
71
|
+
providers.push(descriptor.id);
|
|
72
|
+
providersByCapability.set(capabilityId, providers);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const conflicts = [...providersByCapability.entries()].filter(([, providers]) => providers.length > 1).map(([capabilityId, providers]) => `${capabilityId}: ${[...providers].sort().join(", ")}`);
|
|
76
|
+
if (conflicts.length) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Descriptor selection requires exactly one defaultFor provider per capability, but found multiple (${conflicts.join("; ")}).`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function applyProviderSelection(input) {
|
|
83
|
+
const { items, selected, getDescriptor, withDescriptor } = input;
|
|
84
|
+
const selectedProviders = (0, import_provider_selection.collectSelectedProviderPreferences)({
|
|
85
|
+
items,
|
|
86
|
+
getCapabilityId: (item) => getDescriptor(item).providesFor,
|
|
87
|
+
getProviderId: (item) => getDescriptor(item).id,
|
|
88
|
+
selectedProviderIds: selected
|
|
89
|
+
});
|
|
90
|
+
if (!Object.keys(selectedProviders).length) return [...items];
|
|
91
|
+
return items.map((item) => {
|
|
92
|
+
const descriptor = { ...getDescriptor(item) };
|
|
93
|
+
const preferences = (0, import_provider_selection.resolveSelectedProviderRelationPreferences)({
|
|
94
|
+
providerId: descriptor.id,
|
|
95
|
+
defaultFor: descriptor.defaultFor,
|
|
96
|
+
providerPreferences: descriptor.providerPreferences,
|
|
97
|
+
selectedProviders
|
|
98
|
+
});
|
|
99
|
+
delete descriptor.defaultFor;
|
|
100
|
+
delete descriptor.providerPreferences;
|
|
101
|
+
return withDescriptor(item, { ...descriptor, ...preferences });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function selectDescriptors(input) {
|
|
105
|
+
const { items, getDescriptor, withDescriptor, seed } = input;
|
|
106
|
+
const enabled = items.filter((item) => getDescriptor(item).disabled !== true);
|
|
107
|
+
assertSingleDefaultProvider(enabled.map(getDescriptor));
|
|
108
|
+
const selected = resolveDescriptorSelection(seed);
|
|
109
|
+
if (!selected.length && !seed.baseDescriptors?.length) return [...enabled];
|
|
110
|
+
const selectionItems = applyProviderSelection({
|
|
111
|
+
items: enabled,
|
|
112
|
+
selected,
|
|
113
|
+
getDescriptor,
|
|
114
|
+
withDescriptor
|
|
115
|
+
});
|
|
116
|
+
const catalog = (0, import_composition_graph.createDescriptorCatalog)({
|
|
117
|
+
descriptors: selectionItems.map(getDescriptor),
|
|
118
|
+
relationDescriptors: [...providerRelationDescriptors, ...input.relationDescriptors ?? []]
|
|
119
|
+
});
|
|
120
|
+
const selection = (0, import_composition_graph.createCompositionSelection)({
|
|
121
|
+
catalog,
|
|
122
|
+
selected: [...selected],
|
|
123
|
+
baseDescriptors: [...seed.baseDescriptors ?? []],
|
|
124
|
+
policy: descriptorSelectionPolicy(input.policy)
|
|
125
|
+
});
|
|
126
|
+
const resolvedIds = new Set(selection.getResolved());
|
|
127
|
+
return selectionItems.filter((item) => resolvedIds.has(getDescriptor(item).id));
|
|
128
|
+
}
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
applyProviderSelection,
|
|
132
|
+
assertSingleDefaultProvider,
|
|
133
|
+
defaultResolutionRelations,
|
|
134
|
+
descriptorSelectionPolicy,
|
|
135
|
+
providerRelationDescriptors,
|
|
136
|
+
resolveDescriptorSelection,
|
|
137
|
+
selectDescriptors
|
|
138
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Descriptor, DescriptorId, RelationDescriptor, CompositionPolicy } from '@lorion-org/composition-graph';
|
|
2
|
+
|
|
3
|
+
declare const providerRelationDescriptors: RelationDescriptor[];
|
|
4
|
+
declare const defaultResolutionRelations: readonly ["dependencies", "defaultProviders", "providerPreferences"];
|
|
5
|
+
declare function descriptorSelectionPolicy(policy?: Partial<CompositionPolicy>): Partial<CompositionPolicy>;
|
|
6
|
+
interface DescriptorSelectionSeed {
|
|
7
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
8
|
+
defaultSelection?: readonly DescriptorId[];
|
|
9
|
+
selected?: readonly DescriptorId[];
|
|
10
|
+
selectionSeed?: false | {
|
|
11
|
+
argv?: string[];
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
key?: string;
|
|
14
|
+
cliKeys?: string[];
|
|
15
|
+
envKeys?: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
declare function resolveDescriptorSelection(seed: DescriptorSelectionSeed): DescriptorId[];
|
|
19
|
+
declare function assertSingleDefaultProvider(descriptors: readonly Descriptor[]): void;
|
|
20
|
+
interface ProviderSelectionInput<T> {
|
|
21
|
+
items: readonly T[];
|
|
22
|
+
selected: readonly DescriptorId[];
|
|
23
|
+
getDescriptor: (item: T) => Descriptor;
|
|
24
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
25
|
+
}
|
|
26
|
+
declare function applyProviderSelection<T>(input: ProviderSelectionInput<T>): T[];
|
|
27
|
+
interface DescriptorSelectionInput<T> {
|
|
28
|
+
items: readonly T[];
|
|
29
|
+
getDescriptor: (item: T) => Descriptor;
|
|
30
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
31
|
+
seed: DescriptorSelectionSeed;
|
|
32
|
+
relationDescriptors?: readonly RelationDescriptor[];
|
|
33
|
+
policy?: Partial<CompositionPolicy>;
|
|
34
|
+
}
|
|
35
|
+
declare function selectDescriptors<T>(input: DescriptorSelectionInput<T>): T[];
|
|
36
|
+
|
|
37
|
+
export { type DescriptorSelectionInput, type DescriptorSelectionSeed, type ProviderSelectionInput, applyProviderSelection, assertSingleDefaultProvider, defaultResolutionRelations, descriptorSelectionPolicy, providerRelationDescriptors, resolveDescriptorSelection, selectDescriptors };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Descriptor, DescriptorId, RelationDescriptor, CompositionPolicy } from '@lorion-org/composition-graph';
|
|
2
|
+
|
|
3
|
+
declare const providerRelationDescriptors: RelationDescriptor[];
|
|
4
|
+
declare const defaultResolutionRelations: readonly ["dependencies", "defaultProviders", "providerPreferences"];
|
|
5
|
+
declare function descriptorSelectionPolicy(policy?: Partial<CompositionPolicy>): Partial<CompositionPolicy>;
|
|
6
|
+
interface DescriptorSelectionSeed {
|
|
7
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
8
|
+
defaultSelection?: readonly DescriptorId[];
|
|
9
|
+
selected?: readonly DescriptorId[];
|
|
10
|
+
selectionSeed?: false | {
|
|
11
|
+
argv?: string[];
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
key?: string;
|
|
14
|
+
cliKeys?: string[];
|
|
15
|
+
envKeys?: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
declare function resolveDescriptorSelection(seed: DescriptorSelectionSeed): DescriptorId[];
|
|
19
|
+
declare function assertSingleDefaultProvider(descriptors: readonly Descriptor[]): void;
|
|
20
|
+
interface ProviderSelectionInput<T> {
|
|
21
|
+
items: readonly T[];
|
|
22
|
+
selected: readonly DescriptorId[];
|
|
23
|
+
getDescriptor: (item: T) => Descriptor;
|
|
24
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
25
|
+
}
|
|
26
|
+
declare function applyProviderSelection<T>(input: ProviderSelectionInput<T>): T[];
|
|
27
|
+
interface DescriptorSelectionInput<T> {
|
|
28
|
+
items: readonly T[];
|
|
29
|
+
getDescriptor: (item: T) => Descriptor;
|
|
30
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
31
|
+
seed: DescriptorSelectionSeed;
|
|
32
|
+
relationDescriptors?: readonly RelationDescriptor[];
|
|
33
|
+
policy?: Partial<CompositionPolicy>;
|
|
34
|
+
}
|
|
35
|
+
declare function selectDescriptors<T>(input: DescriptorSelectionInput<T>): T[];
|
|
36
|
+
|
|
37
|
+
export { type DescriptorSelectionInput, type DescriptorSelectionSeed, type ProviderSelectionInput, applyProviderSelection, assertSingleDefaultProvider, defaultResolutionRelations, descriptorSelectionPolicy, providerRelationDescriptors, resolveDescriptorSelection, selectDescriptors };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createCompositionSelection,
|
|
4
|
+
createDescriptorCatalog,
|
|
5
|
+
resolveDescriptorSelectionSeed
|
|
6
|
+
} from "@lorion-org/composition-graph";
|
|
7
|
+
import {
|
|
8
|
+
collectSelectedProviderPreferences,
|
|
9
|
+
resolveSelectedProviderRelationPreferences
|
|
10
|
+
} from "@lorion-org/provider-selection";
|
|
11
|
+
var providerRelationDescriptors = [
|
|
12
|
+
{ direction: "incoming", field: "defaultFor", id: "defaultProviders" },
|
|
13
|
+
{ field: "providerPreferences", id: "providerPreferences", targetMode: "values" }
|
|
14
|
+
];
|
|
15
|
+
var defaultResolutionRelations = [
|
|
16
|
+
"dependencies",
|
|
17
|
+
"defaultProviders",
|
|
18
|
+
"providerPreferences"
|
|
19
|
+
];
|
|
20
|
+
function descriptorSelectionPolicy(policy) {
|
|
21
|
+
return {
|
|
22
|
+
...policy,
|
|
23
|
+
inspectionRelationIds: policy?.inspectionRelationIds ?? [...defaultResolutionRelations],
|
|
24
|
+
provenanceRelationIds: policy?.provenanceRelationIds ?? [...defaultResolutionRelations],
|
|
25
|
+
resolutionRelationIds: policy?.resolutionRelationIds ?? [...defaultResolutionRelations]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveDescriptorSelection(seed) {
|
|
29
|
+
if (seed.selected?.length) return [...seed.selected];
|
|
30
|
+
if (seed.selectionSeed === false) return [...seed.defaultSelection ?? []];
|
|
31
|
+
const options = seed.selectionSeed ?? {};
|
|
32
|
+
const selected = resolveDescriptorSelectionSeed({
|
|
33
|
+
argv: options.argv ?? process.argv,
|
|
34
|
+
env: options.env ?? process.env,
|
|
35
|
+
key: options.key ?? "capability",
|
|
36
|
+
...options.cliKeys ? { cliKeys: options.cliKeys } : {},
|
|
37
|
+
...options.envKeys ? { envKeys: options.envKeys } : {}
|
|
38
|
+
});
|
|
39
|
+
return selected.length ? selected : [...seed.defaultSelection ?? []];
|
|
40
|
+
}
|
|
41
|
+
function assertSingleDefaultProvider(descriptors) {
|
|
42
|
+
const providersByCapability = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const descriptor of descriptors) {
|
|
44
|
+
const { defaultFor } = descriptor;
|
|
45
|
+
if (!defaultFor) continue;
|
|
46
|
+
for (const capabilityId of Array.isArray(defaultFor) ? defaultFor : [defaultFor]) {
|
|
47
|
+
const providers = providersByCapability.get(capabilityId) ?? [];
|
|
48
|
+
providers.push(descriptor.id);
|
|
49
|
+
providersByCapability.set(capabilityId, providers);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const conflicts = [...providersByCapability.entries()].filter(([, providers]) => providers.length > 1).map(([capabilityId, providers]) => `${capabilityId}: ${[...providers].sort().join(", ")}`);
|
|
53
|
+
if (conflicts.length) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Descriptor selection requires exactly one defaultFor provider per capability, but found multiple (${conflicts.join("; ")}).`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function applyProviderSelection(input) {
|
|
60
|
+
const { items, selected, getDescriptor, withDescriptor } = input;
|
|
61
|
+
const selectedProviders = collectSelectedProviderPreferences({
|
|
62
|
+
items,
|
|
63
|
+
getCapabilityId: (item) => getDescriptor(item).providesFor,
|
|
64
|
+
getProviderId: (item) => getDescriptor(item).id,
|
|
65
|
+
selectedProviderIds: selected
|
|
66
|
+
});
|
|
67
|
+
if (!Object.keys(selectedProviders).length) return [...items];
|
|
68
|
+
return items.map((item) => {
|
|
69
|
+
const descriptor = { ...getDescriptor(item) };
|
|
70
|
+
const preferences = resolveSelectedProviderRelationPreferences({
|
|
71
|
+
providerId: descriptor.id,
|
|
72
|
+
defaultFor: descriptor.defaultFor,
|
|
73
|
+
providerPreferences: descriptor.providerPreferences,
|
|
74
|
+
selectedProviders
|
|
75
|
+
});
|
|
76
|
+
delete descriptor.defaultFor;
|
|
77
|
+
delete descriptor.providerPreferences;
|
|
78
|
+
return withDescriptor(item, { ...descriptor, ...preferences });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function selectDescriptors(input) {
|
|
82
|
+
const { items, getDescriptor, withDescriptor, seed } = input;
|
|
83
|
+
const enabled = items.filter((item) => getDescriptor(item).disabled !== true);
|
|
84
|
+
assertSingleDefaultProvider(enabled.map(getDescriptor));
|
|
85
|
+
const selected = resolveDescriptorSelection(seed);
|
|
86
|
+
if (!selected.length && !seed.baseDescriptors?.length) return [...enabled];
|
|
87
|
+
const selectionItems = applyProviderSelection({
|
|
88
|
+
items: enabled,
|
|
89
|
+
selected,
|
|
90
|
+
getDescriptor,
|
|
91
|
+
withDescriptor
|
|
92
|
+
});
|
|
93
|
+
const catalog = createDescriptorCatalog({
|
|
94
|
+
descriptors: selectionItems.map(getDescriptor),
|
|
95
|
+
relationDescriptors: [...providerRelationDescriptors, ...input.relationDescriptors ?? []]
|
|
96
|
+
});
|
|
97
|
+
const selection = createCompositionSelection({
|
|
98
|
+
catalog,
|
|
99
|
+
selected: [...selected],
|
|
100
|
+
baseDescriptors: [...seed.baseDescriptors ?? []],
|
|
101
|
+
policy: descriptorSelectionPolicy(input.policy)
|
|
102
|
+
});
|
|
103
|
+
const resolvedIds = new Set(selection.getResolved());
|
|
104
|
+
return selectionItems.filter((item) => resolvedIds.has(getDescriptor(item).id));
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
applyProviderSelection,
|
|
108
|
+
assertSingleDefaultProvider,
|
|
109
|
+
defaultResolutionRelations,
|
|
110
|
+
descriptorSelectionPolicy,
|
|
111
|
+
providerRelationDescriptors,
|
|
112
|
+
resolveDescriptorSelection,
|
|
113
|
+
selectDescriptors
|
|
114
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lorion-org/descriptor-selection",
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
|
+
"description": "Framework-free provider-aware descriptor selection: resolve the active set from a seed with dependency and single-provider resolution.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lorion-org/lorion.git",
|
|
9
|
+
"directory": "packages/descriptor-selection"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/lorion-org/lorion/tree/main/packages/descriptor-selection#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/lorion-org/lorion/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"lorion-source": {
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"default": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.cts",
|
|
35
|
+
"default": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"src/**/*.ts",
|
|
43
|
+
"!src/**/*.spec.ts"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
47
|
+
"clean": "rimraf coverage dist tsconfig.tsbuildinfo",
|
|
48
|
+
"lint": "eslint src --ext .ts",
|
|
49
|
+
"prepack": "pnpm build",
|
|
50
|
+
"test": "vitest run --root ../.. --config vitest.config.mts packages/descriptor-selection/src/index.spec.ts",
|
|
51
|
+
"coverage": "node -e \"require('node:fs').mkdirSync('../../coverage/.tmp', { recursive: true })\" && vitest run --coverage --coverage.include=packages/descriptor-selection/src/**/*.ts --root ../.. --config vitest.config.mts packages/descriptor-selection/src/index.spec.ts",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"package:check": "pnpm build && pnpm pack --dry-run && publint"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@lorion-org/composition-graph": "workspace:^",
|
|
57
|
+
"@lorion-org/provider-selection": "workspace:^"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"descriptor",
|
|
61
|
+
"selection",
|
|
62
|
+
"composition",
|
|
63
|
+
"provider",
|
|
64
|
+
"typescript"
|
|
65
|
+
],
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCompositionSelection,
|
|
3
|
+
createDescriptorCatalog,
|
|
4
|
+
resolveDescriptorSelectionSeed,
|
|
5
|
+
type CompositionPolicy,
|
|
6
|
+
type Descriptor,
|
|
7
|
+
type DescriptorId,
|
|
8
|
+
type RelationDescriptor,
|
|
9
|
+
} from '@lorion-org/composition-graph';
|
|
10
|
+
import {
|
|
11
|
+
collectSelectedProviderPreferences,
|
|
12
|
+
resolveSelectedProviderRelationPreferences,
|
|
13
|
+
type ProviderPreferenceMap,
|
|
14
|
+
} from '@lorion-org/provider-selection';
|
|
15
|
+
|
|
16
|
+
// Provider-aware descriptor selection: given a set of items that each carry a
|
|
17
|
+
// descriptor and a selection seed, resolve the active subset — applying
|
|
18
|
+
// dependency resolution and one-provider-per-capability selection. Generic over
|
|
19
|
+
// the item type, so build-time bundlers, runtime hosts, and framework adapters
|
|
20
|
+
// share one selection brain instead of re-gluing the graph and provider layers.
|
|
21
|
+
|
|
22
|
+
// The relations that make provider resolution work: a capability's default
|
|
23
|
+
// provider (an incoming `defaultFor` edge) and its explicit provider preferences.
|
|
24
|
+
export const providerRelationDescriptors: RelationDescriptor[] = [
|
|
25
|
+
{ direction: 'incoming', field: 'defaultFor', id: 'defaultProviders' },
|
|
26
|
+
{ field: 'providerPreferences', id: 'providerPreferences', targetMode: 'values' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The relations walked when resolving, inspecting, and tracing provenance.
|
|
30
|
+
export const defaultResolutionRelations = [
|
|
31
|
+
'dependencies',
|
|
32
|
+
'defaultProviders',
|
|
33
|
+
'providerPreferences',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export function descriptorSelectionPolicy(
|
|
37
|
+
policy?: Partial<CompositionPolicy>,
|
|
38
|
+
): Partial<CompositionPolicy> {
|
|
39
|
+
return {
|
|
40
|
+
...policy,
|
|
41
|
+
inspectionRelationIds: policy?.inspectionRelationIds ?? [...defaultResolutionRelations],
|
|
42
|
+
provenanceRelationIds: policy?.provenanceRelationIds ?? [...defaultResolutionRelations],
|
|
43
|
+
resolutionRelationIds: policy?.resolutionRelationIds ?? [...defaultResolutionRelations],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DescriptorSelectionSeed {
|
|
48
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
49
|
+
defaultSelection?: readonly DescriptorId[];
|
|
50
|
+
selected?: readonly DescriptorId[];
|
|
51
|
+
selectionSeed?:
|
|
52
|
+
| false
|
|
53
|
+
| {
|
|
54
|
+
argv?: string[];
|
|
55
|
+
env?: Record<string, string | undefined>;
|
|
56
|
+
key?: string;
|
|
57
|
+
cliKeys?: string[];
|
|
58
|
+
envKeys?: string[];
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The active selection ids from a seed: explicit `selected` wins; otherwise the
|
|
63
|
+
// CLI/env seed is parsed, falling back to `defaultSelection`. Base descriptors are
|
|
64
|
+
// resolved separately by the graph and are not part of this list.
|
|
65
|
+
export function resolveDescriptorSelection(seed: DescriptorSelectionSeed): DescriptorId[] {
|
|
66
|
+
if (seed.selected?.length) return [...seed.selected];
|
|
67
|
+
if (seed.selectionSeed === false) return [...(seed.defaultSelection ?? [])];
|
|
68
|
+
|
|
69
|
+
const options = seed.selectionSeed ?? {};
|
|
70
|
+
const selected = resolveDescriptorSelectionSeed({
|
|
71
|
+
argv: options.argv ?? process.argv,
|
|
72
|
+
env: options.env ?? process.env,
|
|
73
|
+
key: options.key ?? 'capability',
|
|
74
|
+
...(options.cliKeys ? { cliKeys: options.cliKeys } : {}),
|
|
75
|
+
...(options.envKeys ? { envKeys: options.envKeys } : {}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return selected.length ? selected : [...(seed.defaultSelection ?? [])];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A capability may declare exactly one default provider. Two descriptors both
|
|
82
|
+
// claiming `defaultFor` the same capability is a misconfiguration: without an
|
|
83
|
+
// explicit provider selection nothing disambiguates them and both would resolve.
|
|
84
|
+
export function assertSingleDefaultProvider(descriptors: readonly Descriptor[]): void {
|
|
85
|
+
const providersByCapability = new Map<string, string[]>();
|
|
86
|
+
|
|
87
|
+
for (const descriptor of descriptors) {
|
|
88
|
+
const { defaultFor } = descriptor;
|
|
89
|
+
if (!defaultFor) continue;
|
|
90
|
+
for (const capabilityId of Array.isArray(defaultFor) ? defaultFor : [defaultFor]) {
|
|
91
|
+
const providers = providersByCapability.get(capabilityId) ?? [];
|
|
92
|
+
providers.push(descriptor.id);
|
|
93
|
+
providersByCapability.set(capabilityId, providers);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const conflicts = [...providersByCapability.entries()]
|
|
98
|
+
.filter(([, providers]) => providers.length > 1)
|
|
99
|
+
.map(([capabilityId, providers]) => `${capabilityId}: ${[...providers].sort().join(', ')}`);
|
|
100
|
+
|
|
101
|
+
if (conflicts.length) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Descriptor selection requires exactly one defaultFor provider per capability, but found multiple (${conflicts.join('; ')}).`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ProviderSelectionInput<T> {
|
|
109
|
+
items: readonly T[];
|
|
110
|
+
selected: readonly DescriptorId[];
|
|
111
|
+
getDescriptor: (item: T) => Descriptor;
|
|
112
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Apply one-provider-per-capability selection: for each explicitly selected
|
|
116
|
+
// provider, drop the competing `defaultFor`/`providerPreferences` from the other
|
|
117
|
+
// items so the graph resolves exactly one provider. Items are returned untouched
|
|
118
|
+
// when no provider is selected.
|
|
119
|
+
export function applyProviderSelection<T>(input: ProviderSelectionInput<T>): T[] {
|
|
120
|
+
const { items, selected, getDescriptor, withDescriptor } = input;
|
|
121
|
+
|
|
122
|
+
const selectedProviders = collectSelectedProviderPreferences({
|
|
123
|
+
items,
|
|
124
|
+
getCapabilityId: (item) => getDescriptor(item).providesFor,
|
|
125
|
+
getProviderId: (item) => getDescriptor(item).id,
|
|
126
|
+
selectedProviderIds: selected,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!Object.keys(selectedProviders).length) return [...items];
|
|
130
|
+
|
|
131
|
+
return items.map((item) => {
|
|
132
|
+
const descriptor: Descriptor = { ...getDescriptor(item) };
|
|
133
|
+
const preferences = resolveSelectedProviderRelationPreferences({
|
|
134
|
+
providerId: descriptor.id,
|
|
135
|
+
defaultFor: descriptor.defaultFor,
|
|
136
|
+
providerPreferences: descriptor.providerPreferences as ProviderPreferenceMap | undefined,
|
|
137
|
+
selectedProviders,
|
|
138
|
+
});
|
|
139
|
+
delete descriptor.defaultFor;
|
|
140
|
+
delete descriptor.providerPreferences;
|
|
141
|
+
return withDescriptor(item, { ...descriptor, ...preferences });
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DescriptorSelectionInput<T> {
|
|
146
|
+
items: readonly T[];
|
|
147
|
+
// Read the descriptor an item carries.
|
|
148
|
+
getDescriptor: (item: T) => Descriptor;
|
|
149
|
+
// Return a copy of the item with a rewritten descriptor (provider preferences
|
|
150
|
+
// applied). Keeps the item type opaque to this package.
|
|
151
|
+
withDescriptor: (item: T, descriptor: Descriptor) => T;
|
|
152
|
+
seed: DescriptorSelectionSeed;
|
|
153
|
+
// Extra relations to resolve alongside the provider relations (for example a
|
|
154
|
+
// host's own dependency or grouping edges).
|
|
155
|
+
relationDescriptors?: readonly RelationDescriptor[];
|
|
156
|
+
policy?: Partial<CompositionPolicy>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Resolve the active subset of items: apply provider selection, build the
|
|
160
|
+
// descriptor graph, resolve the seed + base + transitive dependencies, and return
|
|
161
|
+
// the items whose descriptor is in the resolved set — in their original order.
|
|
162
|
+
export function selectDescriptors<T>(input: DescriptorSelectionInput<T>): T[] {
|
|
163
|
+
const { items, getDescriptor, withDescriptor, seed } = input;
|
|
164
|
+
|
|
165
|
+
const enabled = items.filter((item) => getDescriptor(item).disabled !== true);
|
|
166
|
+
assertSingleDefaultProvider(enabled.map(getDescriptor));
|
|
167
|
+
|
|
168
|
+
const selected = resolveDescriptorSelection(seed);
|
|
169
|
+
if (!selected.length && !seed.baseDescriptors?.length) return [...enabled];
|
|
170
|
+
|
|
171
|
+
const selectionItems = applyProviderSelection({
|
|
172
|
+
items: enabled,
|
|
173
|
+
selected,
|
|
174
|
+
getDescriptor,
|
|
175
|
+
withDescriptor,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const catalog = createDescriptorCatalog({
|
|
179
|
+
descriptors: selectionItems.map(getDescriptor),
|
|
180
|
+
relationDescriptors: [...providerRelationDescriptors, ...(input.relationDescriptors ?? [])],
|
|
181
|
+
});
|
|
182
|
+
const selection = createCompositionSelection({
|
|
183
|
+
catalog,
|
|
184
|
+
selected: [...selected],
|
|
185
|
+
baseDescriptors: [...(seed.baseDescriptors ?? [])],
|
|
186
|
+
policy: descriptorSelectionPolicy(input.policy),
|
|
187
|
+
});
|
|
188
|
+
const resolvedIds = new Set(selection.getResolved());
|
|
189
|
+
|
|
190
|
+
return selectionItems.filter((item) => resolvedIds.has(getDescriptor(item).id));
|
|
191
|
+
}
|