@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.
- package/.nvmrc +1 -0
- package/README.md +327 -0
- package/eslint.config.mjs +38 -0
- package/package.json +54 -0
- package/src/loader/catalyst-loader.ts +261 -0
- package/src/loader/plan-manifest.ts +228 -0
- package/src/merger/facet-merger.ts +225 -0
- package/src/riotplan-catalyst.ts +59 -0
- package/src/schema/schemas.ts +143 -0
- package/src/types.ts +140 -0
- package/tests/catalyst-loader.test.ts +243 -0
- package/tests/facet-merger.test.ts +311 -0
- package/tests/fixtures/complete-catalyst/catalyst.yml +11 -0
- package/tests/fixtures/complete-catalyst/constraints/documentation.md +7 -0
- package/tests/fixtures/complete-catalyst/constraints/testing.md +5 -0
- package/tests/fixtures/complete-catalyst/domain-knowledge/overview.md +11 -0
- package/tests/fixtures/complete-catalyst/output-templates/press-release.md +16 -0
- package/tests/fixtures/complete-catalyst/process-guidance/lifecycle.md +13 -0
- package/tests/fixtures/complete-catalyst/questions/exploration.md +10 -0
- package/tests/fixtures/complete-catalyst/questions/shaping.md +5 -0
- package/tests/fixtures/complete-catalyst/validation-rules/checklist.md +11 -0
- package/tests/fixtures/invalid-catalyst/questions/some-questions.md +3 -0
- package/tests/fixtures/partial-catalyst/catalyst.yml +7 -0
- package/tests/fixtures/partial-catalyst/constraints/general.md +4 -0
- package/tests/fixtures/partial-catalyst/questions/basics.md +4 -0
- package/tests/plan-manifest.test.ts +315 -0
- package/tests/schema.test.ts +308 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +43 -0
- 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
|
+
}
|