@lorion-org/capability-composition 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,52 @@
1
+ # @lorion-org/capability-composition
2
+
3
+ Framework-free capability composition over the LORION core.
4
+
5
+ It resolves a descriptor-defined capability set (discovery, dependency-graph selection, provider selection, and seeding), detects surfaces by a host-defined convention, and composes the active capabilities into any runtime. One composition path is shared by build-time hosts (for example a Vite adapter) and runtime hosts (for example a Bun server); each host supplies only its activation convention and its registration.
6
+
7
+ ## Install
8
+
9
+ ```shell
10
+ pnpm add @lorion-org/capability-composition
11
+ ```
12
+
13
+ ## API
14
+
15
+ - `resolveSelectedCapabilities({ workspaceRoot, capabilitiesDir, seed })` resolves the active capabilities: base descriptors, the selection seed, transitive dependencies, and exactly one provider per capability.
16
+ - `conventionActivation(surfaces)` builds an activation resolver from per-surface conventions (a file-layout marker plus an export-name derivation), so descriptors carry no surface config. Re-exported from [`@lorion-org/surface-activation`](../surface-activation), which owns the addressing convention.
17
+ - `composeCapabilities({ workspaceRoot, capabilitiesDir, seed, surface, activation, load, register })` resolves the active set and, for each capability that provides the surface, loads its module and hands the exported value to the host's registration. Registry- and framework-agnostic.
18
+ - Build-time hosts that code-generate static imports use `resolveSurfaceModules` from [`@lorion-org/surface-activation`](../surface-activation) directly — the same seam `composeCapabilities` uses internally. It is intentionally not re-exported here, so a build-time host depends only on the light addressing package, not this runtime host.
19
+
20
+ ## What It Is Not
21
+
22
+ - not a framework runtime or plugin registry
23
+ - not a bundler or a router
24
+ - not an application naming convention
25
+
26
+ ## Composition timing: runtime vs build-time
27
+
28
+ The same descriptor selection drives two host styles, differing only in _when_
29
+ composition runs and _how_ modules are loaded:
30
+
31
+ - **Runtime** — call `composeCapabilities` at boot with a dynamic
32
+ `load: (specifier) => import(specifier)`. Simple and fine for a source-run
33
+ server that starts once; resolution is a one-time boot cost.
34
+ - **Build-time** — run `resolveSelectedCapabilities` (here) + `resolveSurfaceModules`
35
+ (from `@lorion-org/surface-activation`) in a build step and code-generate _static_
36
+ imports. The injected set is fixed and
37
+ auditable at build time, with no runtime discovery or dynamic `import()` —
38
+ suited to bundled or air-gapped artifacts.
39
+
40
+ Both compose the identical set from one seam — `resolveSurfaceModules` in
41
+ `@lorion-org/surface-activation`. See `snippets/buildtime-composition.ts` for the
42
+ build-time manifest.
43
+
44
+ ## Local Commands
45
+
46
+ ```shell
47
+ cd packages/capability-composition
48
+ pnpm build
49
+ pnpm test
50
+ pnpm typecheck
51
+ pnpm package:check
52
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,86 @@
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
+ composeCapabilities: () => composeCapabilities,
24
+ conventionActivation: () => import_surface_activation2.conventionActivation,
25
+ fileSurfaceConvention: () => import_surface_activation2.fileSurfaceConvention,
26
+ resolveSelectedCapabilities: () => resolveSelectedCapabilities
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_node_fs = require("fs");
30
+ var import_node_path = require("path");
31
+ var import_descriptor_discovery = require("@lorion-org/descriptor-discovery");
32
+ var import_descriptor_selection = require("@lorion-org/descriptor-selection");
33
+ var import_surface_activation = require("@lorion-org/surface-activation");
34
+ var import_surface_activation2 = require("@lorion-org/surface-activation");
35
+ function readPackageName(directory) {
36
+ const path = (0, import_node_path.resolve)(directory, "package.json");
37
+ const json = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
38
+ return (0, import_descriptor_discovery.requirePackageName)(json, path);
39
+ }
40
+ function resolveSelectedCapabilities(options) {
41
+ const capabilitiesDir = options.capabilitiesDir ?? "capabilities";
42
+ const items = (0, import_descriptor_discovery.discoverDescriptors)({
43
+ cwd: options.workspaceRoot,
44
+ descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
45
+ validation: { schema: import_descriptor_discovery.descriptorSchema }
46
+ }).map((entry) => ({
47
+ id: entry.descriptor.id,
48
+ directory: entry.cwd,
49
+ descriptor: entry.descriptor
50
+ }));
51
+ const selected = (0, import_descriptor_selection.selectDescriptors)({
52
+ items,
53
+ getDescriptor: (item) => item.descriptor,
54
+ withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
55
+ seed: options.seed,
56
+ ...options.policy ? { policy: options.policy } : {}
57
+ });
58
+ return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
59
+ }
60
+ async function composeCapabilities(options) {
61
+ const active = resolveSelectedCapabilities({
62
+ workspaceRoot: options.workspaceRoot,
63
+ capabilitiesDir: options.capabilitiesDir ?? "capabilities",
64
+ seed: options.seed
65
+ });
66
+ const activated = [];
67
+ for (const { capability, specifier, exportName } of (0, import_surface_activation.resolveSurfaceModules)(
68
+ active,
69
+ options.surface,
70
+ options.activation
71
+ )) {
72
+ const module2 = await options.load(specifier);
73
+ const exportValue = module2[exportName];
74
+ if (exportValue === void 0) continue;
75
+ await options.register(exportValue, capability);
76
+ activated.push(capability);
77
+ }
78
+ return activated;
79
+ }
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ composeCapabilities,
83
+ conventionActivation,
84
+ fileSurfaceConvention,
85
+ resolveSelectedCapabilities
86
+ });
@@ -0,0 +1,35 @@
1
+ import { DescriptorId, Descriptor, CompositionPolicy } from '@lorion-org/composition-graph';
2
+ import { SurfaceCapability, ActivationResolver } from '@lorion-org/surface-activation';
3
+ export { ActivationResolver, FileSurfaceConventionOptions, SurfaceActivation, SurfaceConvention, conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
4
+
5
+ interface CapabilitySelectionSeed {
6
+ baseDescriptors?: readonly DescriptorId[];
7
+ defaultSelection?: readonly DescriptorId[];
8
+ selected?: readonly DescriptorId[];
9
+ selectionSeed?: false | {
10
+ argv?: string[];
11
+ cliKeys?: string[];
12
+ env?: Record<string, string | undefined>;
13
+ envKeys?: string[];
14
+ };
15
+ }
16
+ interface ResolvedCapability extends SurfaceCapability {
17
+ descriptor: Descriptor;
18
+ }
19
+ declare function resolveSelectedCapabilities(options: {
20
+ workspaceRoot: string;
21
+ capabilitiesDir?: string;
22
+ seed: CapabilitySelectionSeed;
23
+ policy?: Partial<CompositionPolicy>;
24
+ }): ResolvedCapability[];
25
+ declare function composeCapabilities(options: {
26
+ workspaceRoot: string;
27
+ capabilitiesDir?: string;
28
+ seed: CapabilitySelectionSeed;
29
+ surface: string;
30
+ activation: ActivationResolver;
31
+ load: (specifier: string) => Promise<Record<string, unknown>>;
32
+ register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
33
+ }): Promise<ResolvedCapability[]>;
34
+
35
+ export { type CapabilitySelectionSeed, type ResolvedCapability, composeCapabilities, resolveSelectedCapabilities };
@@ -0,0 +1,35 @@
1
+ import { DescriptorId, Descriptor, CompositionPolicy } from '@lorion-org/composition-graph';
2
+ import { SurfaceCapability, ActivationResolver } from '@lorion-org/surface-activation';
3
+ export { ActivationResolver, FileSurfaceConventionOptions, SurfaceActivation, SurfaceConvention, conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
4
+
5
+ interface CapabilitySelectionSeed {
6
+ baseDescriptors?: readonly DescriptorId[];
7
+ defaultSelection?: readonly DescriptorId[];
8
+ selected?: readonly DescriptorId[];
9
+ selectionSeed?: false | {
10
+ argv?: string[];
11
+ cliKeys?: string[];
12
+ env?: Record<string, string | undefined>;
13
+ envKeys?: string[];
14
+ };
15
+ }
16
+ interface ResolvedCapability extends SurfaceCapability {
17
+ descriptor: Descriptor;
18
+ }
19
+ declare function resolveSelectedCapabilities(options: {
20
+ workspaceRoot: string;
21
+ capabilitiesDir?: string;
22
+ seed: CapabilitySelectionSeed;
23
+ policy?: Partial<CompositionPolicy>;
24
+ }): ResolvedCapability[];
25
+ declare function composeCapabilities(options: {
26
+ workspaceRoot: string;
27
+ capabilitiesDir?: string;
28
+ seed: CapabilitySelectionSeed;
29
+ surface: string;
30
+ activation: ActivationResolver;
31
+ load: (specifier: string) => Promise<Record<string, unknown>>;
32
+ register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
33
+ }): Promise<ResolvedCapability[]>;
34
+
35
+ export { type CapabilitySelectionSeed, type ResolvedCapability, composeCapabilities, resolveSelectedCapabilities };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ // src/index.ts
2
+ import { readFileSync } from "fs";
3
+ import { resolve } from "path";
4
+ import {
5
+ descriptorSchema,
6
+ discoverDescriptors,
7
+ requirePackageName
8
+ } from "@lorion-org/descriptor-discovery";
9
+ import { selectDescriptors } from "@lorion-org/descriptor-selection";
10
+ import {
11
+ resolveSurfaceModules
12
+ } from "@lorion-org/surface-activation";
13
+ import { conventionActivation, fileSurfaceConvention } from "@lorion-org/surface-activation";
14
+ function readPackageName(directory) {
15
+ const path = resolve(directory, "package.json");
16
+ const json = JSON.parse(readFileSync(path, "utf8"));
17
+ return requirePackageName(json, path);
18
+ }
19
+ function resolveSelectedCapabilities(options) {
20
+ const capabilitiesDir = options.capabilitiesDir ?? "capabilities";
21
+ const items = discoverDescriptors({
22
+ cwd: options.workspaceRoot,
23
+ descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
24
+ validation: { schema: descriptorSchema }
25
+ }).map((entry) => ({
26
+ id: entry.descriptor.id,
27
+ directory: entry.cwd,
28
+ descriptor: entry.descriptor
29
+ }));
30
+ const selected = selectDescriptors({
31
+ items,
32
+ getDescriptor: (item) => item.descriptor,
33
+ withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
34
+ seed: options.seed,
35
+ ...options.policy ? { policy: options.policy } : {}
36
+ });
37
+ return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
38
+ }
39
+ async function composeCapabilities(options) {
40
+ const active = resolveSelectedCapabilities({
41
+ workspaceRoot: options.workspaceRoot,
42
+ capabilitiesDir: options.capabilitiesDir ?? "capabilities",
43
+ seed: options.seed
44
+ });
45
+ const activated = [];
46
+ for (const { capability, specifier, exportName } of resolveSurfaceModules(
47
+ active,
48
+ options.surface,
49
+ options.activation
50
+ )) {
51
+ const module = await options.load(specifier);
52
+ const exportValue = module[exportName];
53
+ if (exportValue === void 0) continue;
54
+ await options.register(exportValue, capability);
55
+ activated.push(capability);
56
+ }
57
+ return activated;
58
+ }
59
+ export {
60
+ composeCapabilities,
61
+ conventionActivation,
62
+ fileSurfaceConvention,
63
+ resolveSelectedCapabilities
64
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@lorion-org/capability-composition",
3
+ "version": "1.0.0-beta.3",
4
+ "description": "Framework-free capability composition: on-disk descriptor selection and runtime/build-time composition, built on the surface-activation convention.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/lorion-org/lorion.git",
9
+ "directory": "packages/capability-composition"
10
+ },
11
+ "homepage": "https://github.com/lorion-org/lorion/tree/main/packages/capability-composition#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/capability-composition/src/index.spec.ts",
51
+ "coverage": "node -e \"require('node:fs').mkdirSync('../../coverage/.tmp', { recursive: true })\" && vitest run --coverage --coverage.include=packages/capability-composition/src/**/*.ts --root ../.. --config vitest.config.mts packages/capability-composition/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/descriptor-discovery": "workspace:^",
58
+ "@lorion-org/descriptor-selection": "workspace:^",
59
+ "@lorion-org/surface-activation": "workspace:^"
60
+ },
61
+ "keywords": [
62
+ "lorion",
63
+ "capability",
64
+ "composition",
65
+ "typescript"
66
+ ],
67
+ "engines": {
68
+ "node": "^20.19.0 || >=22.12.0"
69
+ }
70
+ }
package/src/index.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { CompositionPolicy, Descriptor, DescriptorId } from '@lorion-org/composition-graph';
4
+ import {
5
+ descriptorSchema,
6
+ discoverDescriptors,
7
+ requirePackageName,
8
+ } from '@lorion-org/descriptor-discovery';
9
+ import { selectDescriptors } from '@lorion-org/descriptor-selection';
10
+ import {
11
+ type ActivationResolver,
12
+ resolveSurfaceModules,
13
+ type SurfaceCapability,
14
+ } from '@lorion-org/surface-activation';
15
+
16
+ // Capability composition: descriptor-defined capabilities that live as filesystem
17
+ // packages, composed into a host. This package owns disk discovery and the
18
+ // runtime/build-time compose loop; resolving the active set (seed, dependencies,
19
+ // one provider per capability) is delegated to @lorion-org/descriptor-selection and
20
+ // the surface-addressing convention to @lorion-org/surface-activation, so no logic
21
+ // is duplicated here.
22
+
23
+ // Re-export the convention-building companions a `composeCapabilities` caller needs
24
+ // to build the activation it passes in: `conventionActivation`, the
25
+ // `fileSurfaceConvention` preset, and the describing types. The build-time
26
+ // addressing tools (`resolveSurfaceModules`, `capabilitySpecifier`) stay owned
27
+ // solely by @lorion-org/surface-activation, so a build-time host depends on that
28
+ // light package directly instead of pulling in this runtime host.
29
+ export { conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
30
+ export type {
31
+ ActivationResolver,
32
+ FileSurfaceConventionOptions,
33
+ SurfaceActivation,
34
+ SurfaceConvention,
35
+ } from '@lorion-org/surface-activation';
36
+
37
+ export interface CapabilitySelectionSeed {
38
+ baseDescriptors?: readonly DescriptorId[];
39
+ defaultSelection?: readonly DescriptorId[];
40
+ selected?: readonly DescriptorId[];
41
+ selectionSeed?:
42
+ | false
43
+ | {
44
+ argv?: string[];
45
+ cliKeys?: string[];
46
+ env?: Record<string, string | undefined>;
47
+ envKeys?: string[];
48
+ };
49
+ }
50
+
51
+ export interface ResolvedCapability extends SurfaceCapability {
52
+ descriptor: Descriptor;
53
+ }
54
+
55
+ function readPackageName(directory: string): string {
56
+ const path = resolve(directory, 'package.json');
57
+ const json = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown };
58
+ return requirePackageName(json, path);
59
+ }
60
+
61
+ // Resolves the active capability set: base + seed + transitive dependencies +
62
+ // exactly one provider per capability, over capabilities discovered on disk.
63
+ // The selection itself is owned by @lorion-org/descriptor-selection.
64
+ export function resolveSelectedCapabilities(options: {
65
+ workspaceRoot: string;
66
+ capabilitiesDir?: string;
67
+ seed: CapabilitySelectionSeed;
68
+ policy?: Partial<CompositionPolicy>;
69
+ }): ResolvedCapability[] {
70
+ const capabilitiesDir = options.capabilitiesDir ?? 'capabilities';
71
+ const items = discoverDescriptors({
72
+ cwd: options.workspaceRoot,
73
+ descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
74
+ validation: { schema: descriptorSchema },
75
+ }).map((entry) => ({
76
+ id: entry.descriptor.id,
77
+ directory: entry.cwd,
78
+ descriptor: entry.descriptor,
79
+ }));
80
+
81
+ const selected = selectDescriptors({
82
+ items,
83
+ getDescriptor: (item) => item.descriptor,
84
+ withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
85
+ seed: options.seed,
86
+ ...(options.policy ? { policy: options.policy } : {}),
87
+ });
88
+
89
+ // Read package.json only for the resolved set: an unrelated broken or nameless
90
+ // package.json must not abort a composition that never imports that capability.
91
+ return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
92
+ }
93
+
94
+ // Runtime composition: resolve the active set, and for each capability that
95
+ // provides the requested surface, load its module and hand the exported value to
96
+ // the host's registration. Registry- and framework-agnostic. A build-time host
97
+ // uses `resolveSurfaceModules` directly to emit static imports instead.
98
+ export async function composeCapabilities(options: {
99
+ workspaceRoot: string;
100
+ capabilitiesDir?: string;
101
+ seed: CapabilitySelectionSeed;
102
+ surface: string;
103
+ activation: ActivationResolver;
104
+ load: (specifier: string) => Promise<Record<string, unknown>>;
105
+ register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
106
+ }): Promise<ResolvedCapability[]> {
107
+ const active = resolveSelectedCapabilities({
108
+ workspaceRoot: options.workspaceRoot,
109
+ capabilitiesDir: options.capabilitiesDir ?? 'capabilities',
110
+ seed: options.seed,
111
+ });
112
+ const activated: ResolvedCapability[] = [];
113
+
114
+ for (const { capability, specifier, exportName } of resolveSurfaceModules(
115
+ active,
116
+ options.surface,
117
+ options.activation,
118
+ )) {
119
+ const module = await options.load(specifier);
120
+ const exportValue = module[exportName];
121
+ if (exportValue === undefined) continue;
122
+ await options.register(exportValue, capability);
123
+ activated.push(capability);
124
+ }
125
+
126
+ return activated;
127
+ }