@kjerneverk/riotplan-catalyst 1.0.0-dev.0

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.
Files changed (31) hide show
  1. package/.nvmrc +1 -0
  2. package/README.md +327 -0
  3. package/eslint.config.mjs +38 -0
  4. package/package.json +54 -0
  5. package/src/loader/catalyst-loader.ts +261 -0
  6. package/src/loader/plan-manifest.ts +228 -0
  7. package/src/merger/facet-merger.ts +225 -0
  8. package/src/riotplan-catalyst.ts +59 -0
  9. package/src/schema/schemas.ts +143 -0
  10. package/src/types.ts +140 -0
  11. package/tests/catalyst-loader.test.ts +243 -0
  12. package/tests/facet-merger.test.ts +311 -0
  13. package/tests/fixtures/complete-catalyst/catalyst.yml +11 -0
  14. package/tests/fixtures/complete-catalyst/constraints/documentation.md +7 -0
  15. package/tests/fixtures/complete-catalyst/constraints/testing.md +5 -0
  16. package/tests/fixtures/complete-catalyst/domain-knowledge/overview.md +11 -0
  17. package/tests/fixtures/complete-catalyst/output-templates/press-release.md +16 -0
  18. package/tests/fixtures/complete-catalyst/process-guidance/lifecycle.md +13 -0
  19. package/tests/fixtures/complete-catalyst/questions/exploration.md +10 -0
  20. package/tests/fixtures/complete-catalyst/questions/shaping.md +5 -0
  21. package/tests/fixtures/complete-catalyst/validation-rules/checklist.md +11 -0
  22. package/tests/fixtures/invalid-catalyst/questions/some-questions.md +3 -0
  23. package/tests/fixtures/partial-catalyst/catalyst.yml +7 -0
  24. package/tests/fixtures/partial-catalyst/constraints/general.md +4 -0
  25. package/tests/fixtures/partial-catalyst/questions/basics.md +4 -0
  26. package/tests/plan-manifest.test.ts +315 -0
  27. package/tests/schema.test.ts +308 -0
  28. package/tests/setup.ts +1 -0
  29. package/tsconfig.json +22 -0
  30. package/vite.config.ts +43 -0
  31. package/vitest.config.ts +28 -0
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v24.0.0
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # @kjerneverk/riotplan-catalyst
2
+
3
+ Catalyst system for RiotPlan - composable, layerable guidance packages that shape the entire planning process.
4
+
5
+ ## What is a Catalyst?
6
+
7
+ A catalyst is a collection of resources that affects the questions asked and the process used to come up with a plan. It provides guidance through six facets:
8
+
9
+ - **Questions** - Things to consider during exploration
10
+ - **Constraints** - Rules the plan must satisfy
11
+ - **Output Templates** - Expected deliverables
12
+ - **Domain Knowledge** - Context about the domain
13
+ - **Process Guidance** - How to approach the planning process
14
+ - **Validation Rules** - Post-creation checks
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @kjerneverk/riotplan-catalyst
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Loading a Single Catalyst
25
+
26
+ ```typescript
27
+ import { loadCatalyst } from '@kjerneverk/riotplan-catalyst';
28
+
29
+ // Load a catalyst from a directory
30
+ const result = await loadCatalyst('./my-catalyst');
31
+
32
+ if (result.success) {
33
+ console.log('Loaded:', result.catalyst.manifest.name);
34
+ console.log('Facets:', Object.keys(result.catalyst.facets));
35
+ } else {
36
+ console.error('Failed to load:', result.error);
37
+ }
38
+ ```
39
+
40
+ ### Loading Multiple Catalysts
41
+
42
+ ```typescript
43
+ import { resolveCatalysts } from '@kjerneverk/riotplan-catalyst';
44
+
45
+ // Resolve multiple catalysts by path
46
+ const catalysts = await resolveCatalysts([
47
+ './catalysts/software',
48
+ './catalysts/nodejs',
49
+ './catalysts/company'
50
+ ]);
51
+
52
+ console.log(`Loaded ${catalysts.length} catalysts`);
53
+ ```
54
+
55
+ ### Merging Catalysts
56
+
57
+ ```typescript
58
+ import { resolveCatalysts, mergeCatalysts } from '@kjerneverk/riotplan-catalyst';
59
+
60
+ // Load and merge catalysts
61
+ const catalysts = await resolveCatalysts(['./catalyst-1', './catalyst-2']);
62
+ const merged = mergeCatalysts(catalysts);
63
+
64
+ // Access merged content
65
+ console.log('Applied catalysts:', merged.catalystIds);
66
+ console.log('Questions:', merged.facets.questions);
67
+ console.log('Constraints:', merged.facets.constraints);
68
+ ```
69
+
70
+ ### Working with Plan Manifests
71
+
72
+ ```typescript
73
+ import {
74
+ readPlanManifest,
75
+ writePlanManifest,
76
+ addCatalystToManifest
77
+ } from '@kjerneverk/riotplan-catalyst';
78
+
79
+ // Read plan manifest
80
+ const manifest = await readPlanManifest('./my-plan');
81
+ console.log('Plan:', manifest.title);
82
+ console.log('Catalysts:', manifest.catalysts);
83
+
84
+ // Add a catalyst to a plan
85
+ await addCatalystToManifest('./my-plan', '@kjerneverk/catalyst-nodejs');
86
+
87
+ // Create a new plan manifest
88
+ await writePlanManifest('./new-plan', {
89
+ id: 'new-plan',
90
+ title: 'New Plan',
91
+ catalysts: ['@kjerneverk/catalyst-nodejs'],
92
+ created: new Date().toISOString()
93
+ });
94
+ ```
95
+
96
+ ## Catalyst Directory Structure
97
+
98
+ ```
99
+ my-catalyst/
100
+ catalyst.yml # Manifest (required)
101
+ questions/ # Optional
102
+ exploration.md # Questions for idea phase
103
+ constraints/ # Optional
104
+ testing.md # Testing requirements
105
+ output-templates/ # Optional
106
+ press-release.md # Press release template
107
+ domain-knowledge/ # Optional
108
+ overview.md # Domain context
109
+ process-guidance/ # Optional
110
+ lifecycle.md # Process guidance
111
+ validation-rules/ # Optional
112
+ checklist.md # Validation checklist
113
+ ```
114
+
115
+ ## Schema Reference
116
+
117
+ ### catalyst.yml
118
+
119
+ ```yaml
120
+ id: '@myorg/catalyst-name' # NPM package name format (required)
121
+ name: 'Human Readable Name' # Display name (required)
122
+ version: '1.0.0' # Semver version (required)
123
+ description: 'What this provides' # Description (required)
124
+ facets: # Optional - auto-detected if omitted
125
+ questions: true
126
+ constraints: true
127
+ outputTemplates: true
128
+ domainKnowledge: true
129
+ processGuidance: true
130
+ validationRules: true
131
+ ```
132
+
133
+ **Validation Rules:**
134
+ - `id` must be a valid NPM package name (e.g., `@scope/name` or `name`)
135
+ - `version` must be valid semver (e.g., `1.0.0` or `1.0.0-dev.0`)
136
+ - `name` and `description` cannot be empty
137
+ - Facets are optional - if omitted, detected from directory structure
138
+
139
+ ### plan.yaml
140
+
141
+ ```yaml
142
+ id: 'plan-identifier' # Plan ID (required)
143
+ title: 'Plan Title' # Display title (required)
144
+ catalysts: # Optional - list of catalyst IDs
145
+ - '@myorg/catalyst-nodejs'
146
+ - '@myorg/catalyst-company'
147
+ created: '2026-01-15T10:30:00Z' # ISO timestamp (optional)
148
+ metadata: # Optional - extensible
149
+ author: 'John Doe'
150
+ tags: ['feature', 'api']
151
+ ```
152
+
153
+ ## API Reference
154
+
155
+ ### Loader Functions
156
+
157
+ #### `loadCatalyst(path: string): Promise<CatalystLoadResult>`
158
+
159
+ Load a single catalyst from a directory.
160
+
161
+ **Returns:**
162
+ ```typescript
163
+ {
164
+ success: true,
165
+ catalyst: {
166
+ manifest: { id, name, version, description, facets },
167
+ directory: '/absolute/path',
168
+ facets: {
169
+ questions: [{ filename, content }],
170
+ constraints: [{ filename, content }],
171
+ // ... other facets
172
+ }
173
+ }
174
+ }
175
+ // or
176
+ {
177
+ success: false,
178
+ error: 'Error message'
179
+ }
180
+ ```
181
+
182
+ #### `loadCatalystSafe(path: string): Promise<Catalyst | null>`
183
+
184
+ Load a catalyst, returning null on error (no exception thrown).
185
+
186
+ #### `resolveCatalysts(identifiers: string[]): Promise<Catalyst[]>`
187
+
188
+ Load multiple catalysts by path. Skips any that fail to load.
189
+
190
+ **Example:**
191
+ ```typescript
192
+ const catalysts = await resolveCatalysts([
193
+ './catalyst-1',
194
+ './catalyst-2',
195
+ './catalyst-3'
196
+ ]);
197
+ // Returns only successfully loaded catalysts
198
+ ```
199
+
200
+ ### Merger Functions
201
+
202
+ #### `mergeCatalysts(catalysts: Catalyst[]): MergedCatalyst`
203
+
204
+ Merge multiple catalysts into a single structure with source attribution.
205
+
206
+ **Returns:**
207
+ ```typescript
208
+ {
209
+ catalystIds: ['@org/cat-1', '@org/cat-2'],
210
+ facets: {
211
+ questions: [
212
+ { content: '...', sourceId: '@org/cat-1', filename: 'setup.md' },
213
+ { content: '...', sourceId: '@org/cat-2', filename: 'config.md' }
214
+ ],
215
+ // ... other facets
216
+ }
217
+ }
218
+ ```
219
+
220
+ #### `renderFacet(facetName: string, merged: MergedCatalyst): string`
221
+
222
+ Render a single facet as markdown with source attribution.
223
+
224
+ #### `renderAllFacets(merged: MergedCatalyst): Record<string, string>`
225
+
226
+ Render all facets as markdown strings.
227
+
228
+ **Example:**
229
+ ```typescript
230
+ const rendered = renderAllFacets(merged);
231
+ console.log(rendered.questions);
232
+ console.log(rendered.constraints);
233
+ ```
234
+
235
+ ### Plan Manifest Functions
236
+
237
+ #### `readPlanManifest(planPath: string): Promise<PlanManifest>`
238
+
239
+ Read plan.yaml from a plan directory.
240
+
241
+ #### `writePlanManifest(planPath: string, manifest: PlanManifest): Promise<void>`
242
+
243
+ Write plan.yaml to a plan directory.
244
+
245
+ #### `updatePlanManifest(planPath: string, updates: Partial<PlanManifest>): Promise<void>`
246
+
247
+ Update specific fields in plan.yaml.
248
+
249
+ #### `addCatalystToManifest(planPath: string, catalystId: string): Promise<void>`
250
+
251
+ Add a catalyst ID to the plan's catalyst list (if not already present).
252
+
253
+ #### `removeCatalystFromManifest(planPath: string, catalystId: string): Promise<void>`
254
+
255
+ Remove a catalyst ID from the plan's catalyst list.
256
+
257
+ ## TypeScript Types
258
+
259
+ ```typescript
260
+ // Catalyst manifest
261
+ interface CatalystManifest {
262
+ id: string;
263
+ name: string;
264
+ version: string;
265
+ description: string;
266
+ facets?: FacetsDeclaration;
267
+ }
268
+
269
+ // Facet content
270
+ interface FacetContent {
271
+ filename: string;
272
+ content: string;
273
+ }
274
+
275
+ // Loaded catalyst
276
+ interface Catalyst {
277
+ manifest: CatalystManifest;
278
+ directory: string;
279
+ facets: CatalystFacets;
280
+ }
281
+
282
+ // Merged catalyst with attribution
283
+ interface MergedCatalyst {
284
+ catalystIds: string[];
285
+ facets: Record<string, AttributedContent[]>;
286
+ }
287
+
288
+ interface AttributedContent {
289
+ content: string;
290
+ sourceId: string;
291
+ filename?: string;
292
+ }
293
+
294
+ // Plan manifest
295
+ interface PlanManifest {
296
+ id: string;
297
+ title: string;
298
+ catalysts?: string[];
299
+ created?: string;
300
+ metadata?: Record<string, string>;
301
+ }
302
+ ```
303
+
304
+ ## Error Handling
305
+
306
+ All loader functions return structured results:
307
+
308
+ ```typescript
309
+ // Success
310
+ { success: true, catalyst: Catalyst }
311
+
312
+ // Failure
313
+ { success: false, error: string }
314
+ ```
315
+
316
+ Safe variants return `null` on error:
317
+
318
+ ```typescript
319
+ const catalyst = await loadCatalystSafe('./path');
320
+ if (catalyst === null) {
321
+ console.error('Failed to load');
322
+ }
323
+ ```
324
+
325
+ ## License
326
+
327
+ Apache-2.0
@@ -0,0 +1,38 @@
1
+ import js from '@eslint/js';
2
+ import ts from '@typescript-eslint/eslint-plugin';
3
+ import tsParser from '@typescript-eslint/parser';
4
+ import importPlugin from 'eslint-plugin-import';
5
+
6
+ export default [
7
+ {
8
+ ignores: ['dist/**', 'node_modules/**', '**/*.test.ts', 'vitest.config.ts', 'vite.config.ts'],
9
+ },
10
+ {
11
+ files: ['**/*.ts'],
12
+ languageOptions: {
13
+ parser: tsParser,
14
+ parserOptions: {
15
+ ecmaVersion: 2024,
16
+ sourceType: 'module',
17
+ },
18
+ globals: {
19
+ console: 'readonly',
20
+ process: 'readonly',
21
+ NodeJS: 'readonly',
22
+ },
23
+ },
24
+ plugins: {
25
+ '@typescript-eslint': ts,
26
+ import: importPlugin,
27
+ },
28
+ rules: {
29
+ ...js.configs.recommended.rules,
30
+ ...ts.configs.recommended.rules,
31
+ indent: ['error', 2],
32
+ quotes: ['error', 'single'],
33
+ semi: ['error', 'always'],
34
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
35
+ 'import/order': 'error',
36
+ },
37
+ },
38
+ ];
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@kjerneverk/riotplan-catalyst",
3
+ "version": "1.0.0-dev.0",
4
+ "description": "Catalyst system for RiotPlan - composable, layerable guidance packages for plan creation",
5
+ "type": "module",
6
+ "main": "./dist/riotplan-catalyst.js",
7
+ "types": "./dist/riotplan-catalyst.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/riotplan-catalyst.d.ts",
11
+ "import": "./dist/riotplan-catalyst.js"
12
+ }
13
+ },
14
+ "engines": {
15
+ "node": ">=24.0.0"
16
+ },
17
+ "scripts": {
18
+ "build": "npm run lint && vite build",
19
+ "test": "npm run test:coverage",
20
+ "test:coverage": "vitest run --coverage",
21
+ "test:debug": "vitest --run --coverage --reporter verbose",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
24
+ "clean": "rm -rf dist",
25
+ "precommit": "npm run build && npm run lint && npm run test",
26
+ "prepublishOnly": "npm run clean && npm run build"
27
+ },
28
+ "dependencies": {
29
+ "yaml": "^2.4.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "devDependencies": {
33
+ "@rollup/plugin-replace": "^5.0.5",
34
+ "@types/node": "^25.2.2",
35
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
36
+ "@typescript-eslint/parser": "^6.0.0",
37
+ "@vitest/coverage-v8": "^1.1.0",
38
+ "eslint": "^8.56.0",
39
+ "eslint-plugin-import": "^2.29.0",
40
+ "typescript": "^5.4.0",
41
+ "vite": "^5.1.0",
42
+ "vite-plugin-dts": "^1.0.0",
43
+ "vitest": "^1.1.0"
44
+ },
45
+ "keywords": [
46
+ "riotplan",
47
+ "catalyst",
48
+ "planning",
49
+ "guidance",
50
+ "ai"
51
+ ],
52
+ "author": "Kjerneverk",
53
+ "license": "Apache-2.0"
54
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Catalyst loading from local directories
3
+ * @packageDocumentation
4
+ */
5
+
6
+ import { readFile, readdir, stat } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { parse as parseYaml } from 'yaml';
9
+ import {
10
+ CatalystManifestSchema,
11
+ FACET_DIRECTORIES,
12
+ type FacetType,
13
+ } from '@/schema/schemas';
14
+ import type {
15
+ Catalyst,
16
+ CatalystManifest,
17
+ CatalystFacets,
18
+ FacetContent,
19
+ CatalystLoadOptions,
20
+ CatalystLoadResult,
21
+ } from '@/types';
22
+
23
+ /**
24
+ * Load a catalyst manifest from catalyst.yml
25
+ * @param directoryPath - Absolute path to catalyst directory
26
+ * @returns Parsed and validated manifest
27
+ * @throws Error if manifest is missing or invalid
28
+ */
29
+ async function loadManifest(directoryPath: string): Promise<CatalystManifest> {
30
+ const manifestPath = join(directoryPath, 'catalyst.yml');
31
+
32
+ try {
33
+ const content = await readFile(manifestPath, 'utf-8');
34
+ const parsed = parseYaml(content);
35
+
36
+ // Validate with Zod schema
37
+ const result = CatalystManifestSchema.safeParse(parsed);
38
+
39
+ if (!result.success) {
40
+ const errors = result.error.issues.map(issue =>
41
+ `${issue.path.join('.')}: ${issue.message}`
42
+ ).join('; ');
43
+ throw new Error(`Invalid catalyst manifest: ${errors}`);
44
+ }
45
+
46
+ return result.data;
47
+ } catch (error) {
48
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
49
+ throw new Error(`Catalyst manifest not found at ${manifestPath}`);
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Load all markdown files from a facet directory
57
+ * @param facetPath - Path to facet directory
58
+ * @returns Array of FacetContent objects
59
+ */
60
+ async function loadFacetFiles(facetPath: string): Promise<FacetContent[]> {
61
+ try {
62
+ const entries = await readdir(facetPath, { withFileTypes: true });
63
+ const markdownFiles = entries.filter(
64
+ entry => entry.isFile() && entry.name.endsWith('.md')
65
+ );
66
+
67
+ const contents = await Promise.all(
68
+ markdownFiles.map(async (file) => {
69
+ const filePath = join(facetPath, file.name);
70
+ const content = await readFile(filePath, 'utf-8');
71
+ return {
72
+ filename: file.name,
73
+ content,
74
+ };
75
+ })
76
+ );
77
+
78
+ return contents;
79
+ } catch (error) {
80
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
81
+ // Directory doesn't exist - return empty array
82
+ return [];
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if a directory exists
90
+ */
91
+ async function directoryExists(path: string): Promise<boolean> {
92
+ try {
93
+ const stats = await stat(path);
94
+ return stats.isDirectory();
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Load all facets from a catalyst directory
102
+ * @param directoryPath - Absolute path to catalyst directory
103
+ * @param manifest - Parsed manifest (for cross-referencing)
104
+ * @param options - Load options
105
+ * @returns Loaded facets and any warnings
106
+ */
107
+ async function loadFacets(
108
+ directoryPath: string,
109
+ manifest: CatalystManifest,
110
+ options: CatalystLoadOptions = {}
111
+ ): Promise<{ facets: CatalystFacets; warnings: string[] }> {
112
+ const facets: CatalystFacets = {};
113
+ const warnings: string[] = [];
114
+
115
+ // Load each facet type
116
+ for (const [facetKey, dirName] of Object.entries(FACET_DIRECTORIES)) {
117
+ const facetType = facetKey as FacetType;
118
+ const facetPath = join(directoryPath, dirName);
119
+ const exists = await directoryExists(facetPath);
120
+
121
+ // Check if facet is declared in manifest
122
+ const declared = manifest.facets?.[facetType];
123
+
124
+ if (exists) {
125
+ // Load the facet content
126
+ const content = await loadFacetFiles(facetPath);
127
+ if (content.length > 0) {
128
+ facets[facetType] = content;
129
+ }
130
+
131
+ // Warn if present but explicitly declared as false
132
+ if (declared === false && options.warnOnUndeclaredFacets) {
133
+ warnings.push(
134
+ `Facet '${facetType}' is present but declared as false in manifest`
135
+ );
136
+ }
137
+ } else {
138
+ // Warn if declared but missing
139
+ if (declared === true && options.warnOnMissingFacets) {
140
+ warnings.push(
141
+ `Facet '${facetType}' is declared in manifest but directory '${dirName}' not found`
142
+ );
143
+ }
144
+ }
145
+ }
146
+
147
+ return { facets, warnings };
148
+ }
149
+
150
+ /**
151
+ * Load a catalyst from a local directory
152
+ *
153
+ * Reads the catalyst.yml manifest, validates it, and loads all facet content
154
+ * from the directory structure.
155
+ *
156
+ * @param directoryPath - Path to catalyst directory (absolute or relative)
157
+ * @param options - Load options
158
+ * @returns Loaded catalyst
159
+ * @throws Error if manifest is missing or invalid
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const catalyst = await loadCatalyst('./my-catalyst');
164
+ * console.log(catalyst.manifest.name);
165
+ * console.log(catalyst.facets.constraints?.length);
166
+ * ```
167
+ */
168
+ export async function loadCatalyst(
169
+ directoryPath: string,
170
+ options: CatalystLoadOptions = {}
171
+ ): Promise<Catalyst> {
172
+ // Resolve to absolute path
173
+ const absolutePath = resolve(directoryPath);
174
+
175
+ // Check if directory exists
176
+ if (!await directoryExists(absolutePath)) {
177
+ throw new Error(`Catalyst directory not found: ${absolutePath}`);
178
+ }
179
+
180
+ // Load and validate manifest
181
+ const manifest = await loadManifest(absolutePath);
182
+
183
+ // Load all facets
184
+ const { facets, warnings } = await loadFacets(absolutePath, manifest, options);
185
+
186
+ // Log warnings if any
187
+ if (warnings.length > 0 && !options.strict) {
188
+ for (const warning of warnings) {
189
+ console.warn(`[catalyst-loader] ${warning}`);
190
+ }
191
+ }
192
+
193
+ return {
194
+ manifest,
195
+ facets,
196
+ directoryPath: absolutePath,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Load a catalyst with full error handling
202
+ *
203
+ * Like loadCatalyst but returns a result object instead of throwing
204
+ *
205
+ * @param directoryPath - Path to catalyst directory
206
+ * @param options - Load options
207
+ * @returns Result object with success/error
208
+ */
209
+ export async function loadCatalystSafe(
210
+ directoryPath: string,
211
+ options: CatalystLoadOptions = {}
212
+ ): Promise<CatalystLoadResult> {
213
+ try {
214
+ const catalyst = await loadCatalyst(directoryPath, options);
215
+ return {
216
+ success: true,
217
+ catalyst,
218
+ };
219
+ } catch (error) {
220
+ return {
221
+ success: false,
222
+ error: error instanceof Error ? error.message : String(error),
223
+ };
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Resolve catalyst identifiers to directories and load them
229
+ *
230
+ * Phase 1: Only supports local directory paths (absolute or relative)
231
+ * Phase 2 (future): Will support NPM package resolution from node_modules
232
+ *
233
+ * @param identifiers - Array of catalyst paths
234
+ * @param basePath - Base path for resolving relative paths
235
+ * @param options - Load options
236
+ * @returns Array of loaded catalysts
237
+ */
238
+ export async function resolveCatalysts(
239
+ identifiers: string[],
240
+ basePath: string = process.cwd(),
241
+ options: CatalystLoadOptions = {}
242
+ ): Promise<Catalyst[]> {
243
+ const catalysts: Catalyst[] = [];
244
+
245
+ for (const identifier of identifiers) {
246
+ // Phase 1: treat identifier as a path
247
+ // If it's relative, resolve from basePath
248
+ const path = resolve(basePath, identifier);
249
+
250
+ try {
251
+ const catalyst = await loadCatalyst(path, options);
252
+ catalysts.push(catalyst);
253
+ } catch (error) {
254
+ throw new Error(
255
+ `Failed to load catalyst '${identifier}': ${error instanceof Error ? error.message : String(error)}`
256
+ );
257
+ }
258
+ }
259
+
260
+ return catalysts;
261
+ }