@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 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
+ });
@@ -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 };
@@ -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
+ }