@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 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
- - string parsing for user-facing selection input belongs in a higher adapter layer
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
- The core graph does not prescribe directional interpretation, hinting, or
209
- weighting. Those concerns belong in higher layers.
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 (!input) return [];
50
- const items = Array.isArray(input) ? input : input.split(/[,\s]+/).map((item) => item.trim());
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 isVersionConstraintMap(value) {
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
- if (!isVersionConstraintMap(relationValue)) return [];
83
- return Object.keys(relationValue).sort();
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(descriptor.id)?.push(edge);
115
- incoming.get(target)?.push(edge);
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?: string;
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?: string;
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?: DescriptorId[] | string): DescriptorId[];
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?: string;
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?: string;
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?: DescriptorId[] | string): DescriptorId[];
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 (!input) return [];
11
- const items = Array.isArray(input) ? input : input.split(/[,\s]+/).map((item) => item.trim());
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 isVersionConstraintMap(value) {
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
- if (!isVersionConstraintMap(relationValue)) return [];
44
- return Object.keys(relationValue).sort();
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(descriptor.id)?.push(edge);
76
- incoming.get(target)?.push(edge);
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
  };
@@ -36,6 +36,7 @@ const catalog = createDescriptorCatalog({
36
36
  {
37
37
  id: 'payment-provider-stripe',
38
38
  version: '1.0.0',
39
+ defaultFor: 'payment-checkout',
39
40
  providesFor: 'payment-checkout',
40
41
  dependencies: { payments: '^1.0.0' },
41
42
  },
@@ -35,6 +35,7 @@ const catalog = createDescriptorCatalog({
35
35
  {
36
36
  id: 'payment-provider-stripe',
37
37
  version: '1.0.0',
38
+ defaultFor: 'payment-checkout',
38
39
  providesFor: 'payment-checkout',
39
40
  dependencies: { payments: '^1.0.0' },
40
41
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorion-org/composition-graph",
3
- "version": "1.0.0-beta.1",
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
+ }