@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan manifest read/write
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
9
|
+
import { PlanManifestSchema, type PlanManifestOutput } from '@/schema/schemas';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Plan manifest stored in plan.yaml
|
|
13
|
+
*
|
|
14
|
+
* This is the metadata file for a plan that records:
|
|
15
|
+
* - The plan's identity (ID and title)
|
|
16
|
+
* - Which catalysts are associated with the plan
|
|
17
|
+
* - When the plan was created
|
|
18
|
+
* - Arbitrary metadata for extensibility
|
|
19
|
+
*/
|
|
20
|
+
export interface PlanManifest extends PlanManifestOutput {}
|
|
21
|
+
|
|
22
|
+
const MANIFEST_FILENAME = 'plan.yaml';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read a plan manifest from plan.yaml
|
|
26
|
+
*
|
|
27
|
+
* Returns null if the manifest file doesn't exist (graceful handling
|
|
28
|
+
* for backward compatibility with plans created before catalyst support).
|
|
29
|
+
* Throws if the file exists but contains invalid data.
|
|
30
|
+
*
|
|
31
|
+
* @param planDirectory - Absolute path to plan directory
|
|
32
|
+
* @returns Parsed and validated manifest, or null if not present
|
|
33
|
+
* @throws Error if manifest exists but is invalid
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const manifest = await readPlanManifest('./my-plan');
|
|
38
|
+
* if (manifest) {
|
|
39
|
+
* console.log(`Plan: ${manifest.title}`);
|
|
40
|
+
* console.log(`Uses catalysts: ${manifest.catalysts?.join(', ')}`);
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export async function readPlanManifest(planDirectory: string): Promise<PlanManifest | null> {
|
|
45
|
+
const manifestPath = join(planDirectory, MANIFEST_FILENAME);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(manifestPath, 'utf-8');
|
|
49
|
+
const parsed = parseYaml(content);
|
|
50
|
+
|
|
51
|
+
// Validate with Zod schema
|
|
52
|
+
const result = PlanManifestSchema.safeParse(parsed);
|
|
53
|
+
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
const errors = result.error.issues.map(issue =>
|
|
56
|
+
`${issue.path.join('.')}: ${issue.message}`
|
|
57
|
+
).join('; ');
|
|
58
|
+
throw new Error(`Invalid plan manifest: ${errors}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result.data;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
64
|
+
// File doesn't exist - return null for backward compatibility
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Write a plan manifest to plan.yaml
|
|
73
|
+
*
|
|
74
|
+
* Creates or overwrites the plan.yaml file with the provided manifest.
|
|
75
|
+
* Automatically timestamps the manifest if `created` is not provided.
|
|
76
|
+
*
|
|
77
|
+
* @param planDirectory - Absolute path to plan directory
|
|
78
|
+
* @param manifest - Manifest to write
|
|
79
|
+
* @throws Error if manifest is invalid or write fails
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const manifest: PlanManifest = {
|
|
84
|
+
* id: 'add-user-auth',
|
|
85
|
+
* title: 'Add User Authentication',
|
|
86
|
+
* catalysts: ['@kjerneverk/catalyst-nodejs'],
|
|
87
|
+
* created: new Date().toISOString(),
|
|
88
|
+
* };
|
|
89
|
+
* await writePlanManifest('./my-plan', manifest);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export async function writePlanManifest(
|
|
93
|
+
planDirectory: string,
|
|
94
|
+
manifest: PlanManifest
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
// Validate manifest
|
|
97
|
+
const result = PlanManifestSchema.safeParse(manifest);
|
|
98
|
+
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
const errors = result.error.issues.map(issue =>
|
|
101
|
+
`${issue.path.join('.')}: ${issue.message}`
|
|
102
|
+
).join('; ');
|
|
103
|
+
throw new Error(`Invalid plan manifest: ${errors}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ensure created timestamp exists
|
|
107
|
+
const manifestToWrite: PlanManifest = {
|
|
108
|
+
...result.data,
|
|
109
|
+
created: result.data.created || new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Serialize to YAML
|
|
113
|
+
const yaml = stringifyYaml(manifestToWrite, {
|
|
114
|
+
indent: 2,
|
|
115
|
+
lineWidth: 120,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Write to file
|
|
119
|
+
const manifestPath = join(planDirectory, MANIFEST_FILENAME);
|
|
120
|
+
await writeFile(manifestPath, yaml, 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update specific fields in a plan manifest
|
|
125
|
+
*
|
|
126
|
+
* Reads the existing manifest (or creates a new one if it doesn't exist),
|
|
127
|
+
* merges the provided updates, and writes it back.
|
|
128
|
+
*
|
|
129
|
+
* @param planDirectory - Absolute path to plan directory
|
|
130
|
+
* @param updates - Partial manifest to merge (only specified fields are changed)
|
|
131
|
+
* @throws Error if updates are invalid or operation fails
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* // Add a catalyst to an existing plan
|
|
136
|
+
* await updatePlanManifest('./my-plan', {
|
|
137
|
+
* catalysts: ['@kjerneverk/catalyst-nodejs', '@kjerneverk/catalyst-react'],
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export async function updatePlanManifest(
|
|
142
|
+
planDirectory: string,
|
|
143
|
+
updates: Partial<PlanManifest>
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
// Read existing manifest
|
|
146
|
+
let existing = await readPlanManifest(planDirectory);
|
|
147
|
+
|
|
148
|
+
// If no existing manifest, start with a minimal one
|
|
149
|
+
if (!existing) {
|
|
150
|
+
// Updates must contain at least id and title
|
|
151
|
+
if (!updates.id || !updates.title) {
|
|
152
|
+
throw new Error('Cannot create manifest without id and title');
|
|
153
|
+
}
|
|
154
|
+
existing = {
|
|
155
|
+
id: '',
|
|
156
|
+
title: '',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Merge updates
|
|
161
|
+
const merged: PlanManifest = {
|
|
162
|
+
...existing,
|
|
163
|
+
...updates,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Write back
|
|
167
|
+
await writePlanManifest(planDirectory, merged);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add a catalyst to a plan's manifest
|
|
172
|
+
*
|
|
173
|
+
* Convenience function that adds a catalyst ID to the catalysts array
|
|
174
|
+
* without affecting other fields.
|
|
175
|
+
*
|
|
176
|
+
* @param planDirectory - Absolute path to plan directory
|
|
177
|
+
* @param catalystId - Catalyst ID to add
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* await addCatalystToManifest('./my-plan', '@kjerneverk/catalyst-nodejs');
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export async function addCatalystToManifest(
|
|
185
|
+
planDirectory: string,
|
|
186
|
+
catalystId: string
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const manifest = await readPlanManifest(planDirectory);
|
|
189
|
+
const currentCatalysts = manifest?.catalysts ?? [];
|
|
190
|
+
|
|
191
|
+
// Only add if not already present
|
|
192
|
+
if (!currentCatalysts.includes(catalystId)) {
|
|
193
|
+
currentCatalysts.push(catalystId);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await updatePlanManifest(planDirectory, {
|
|
197
|
+
catalysts: currentCatalysts,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Remove a catalyst from a plan's manifest
|
|
203
|
+
*
|
|
204
|
+
* Convenience function that removes a catalyst ID from the catalysts array.
|
|
205
|
+
*
|
|
206
|
+
* @param planDirectory - Absolute path to plan directory
|
|
207
|
+
* @param catalystId - Catalyst ID to remove
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* await removeCatalystFromManifest('./my-plan', '@kjerneverk/catalyst-old');
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export async function removeCatalystFromManifest(
|
|
215
|
+
planDirectory: string,
|
|
216
|
+
catalystId: string
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const manifest = await readPlanManifest(planDirectory);
|
|
219
|
+
const currentCatalysts = manifest?.catalysts ?? [];
|
|
220
|
+
|
|
221
|
+
const filtered = currentCatalysts.filter(id => id !== catalystId);
|
|
222
|
+
|
|
223
|
+
if (filtered.length !== currentCatalysts.length) {
|
|
224
|
+
await updatePlanManifest(planDirectory, {
|
|
225
|
+
catalysts: filtered.length > 0 ? filtered : undefined,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet merging for multiple catalysts
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Catalyst } from '@/types';
|
|
7
|
+
import type { FacetType } from '@/schema/schemas';
|
|
8
|
+
import { FACET_TYPES } from '@/schema/schemas';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A single piece of content with source attribution
|
|
12
|
+
*/
|
|
13
|
+
export interface AttributedContent {
|
|
14
|
+
content: string;
|
|
15
|
+
sourceId: string;
|
|
16
|
+
filename?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Facets merged from multiple catalysts with source attribution
|
|
21
|
+
*/
|
|
22
|
+
export interface MergedFacets {
|
|
23
|
+
questions?: AttributedContent[];
|
|
24
|
+
constraints?: AttributedContent[];
|
|
25
|
+
outputTemplates?: AttributedContent[];
|
|
26
|
+
domainKnowledge?: AttributedContent[];
|
|
27
|
+
processGuidance?: AttributedContent[];
|
|
28
|
+
validationRules?: AttributedContent[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of merging multiple catalysts
|
|
33
|
+
*/
|
|
34
|
+
export interface MergedCatalyst {
|
|
35
|
+
/** Ordered list of catalyst IDs that were merged */
|
|
36
|
+
catalystIds: string[];
|
|
37
|
+
/** Merged facets with source attribution */
|
|
38
|
+
facets: MergedFacets;
|
|
39
|
+
/** Metadata about each catalyst's contribution */
|
|
40
|
+
contributions: Map<string, {
|
|
41
|
+
facetTypes: FacetType[];
|
|
42
|
+
contentCount: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Merge multiple catalysts in order
|
|
48
|
+
*
|
|
49
|
+
* Catalysts are merged in the order provided, with later catalysts
|
|
50
|
+
* layering on top of earlier ones. Content is concatenated (no conflict
|
|
51
|
+
* resolution in v1), and each piece of content retains its source catalyst ID.
|
|
52
|
+
*
|
|
53
|
+
* @param catalysts - Ordered array of catalysts to merge (can be empty)
|
|
54
|
+
* @returns Merged catalyst with source attribution
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const catalyst1 = await loadCatalyst('./base-catalyst');
|
|
59
|
+
* const catalyst2 = await loadCatalyst('./nodejs-catalyst');
|
|
60
|
+
* const merged = mergeCatalysts([catalyst1, catalyst2]);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function mergeCatalysts(catalysts: Catalyst[]): MergedCatalyst {
|
|
64
|
+
const mergedFacets: MergedFacets = {};
|
|
65
|
+
const catalystIds = catalysts.map(c => c.manifest.id);
|
|
66
|
+
const contributions = new Map<string, {
|
|
67
|
+
facetTypes: FacetType[];
|
|
68
|
+
contentCount: number;
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
// Process each facet type
|
|
72
|
+
for (const facetType of FACET_TYPES) {
|
|
73
|
+
const mergedContent: AttributedContent[] = [];
|
|
74
|
+
|
|
75
|
+
// Iterate through catalysts in order
|
|
76
|
+
for (const catalyst of catalysts) {
|
|
77
|
+
const facetContent = catalyst.facets[facetType];
|
|
78
|
+
|
|
79
|
+
if (facetContent && facetContent.length > 0) {
|
|
80
|
+
// Add each file's content with source attribution
|
|
81
|
+
for (const file of facetContent) {
|
|
82
|
+
mergedContent.push({
|
|
83
|
+
content: file.content,
|
|
84
|
+
sourceId: catalyst.manifest.id,
|
|
85
|
+
filename: file.filename,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Track contribution
|
|
90
|
+
const contrib = contributions.get(catalyst.manifest.id) || {
|
|
91
|
+
facetTypes: [],
|
|
92
|
+
contentCount: 0,
|
|
93
|
+
};
|
|
94
|
+
if (!contrib.facetTypes.includes(facetType)) {
|
|
95
|
+
contrib.facetTypes.push(facetType);
|
|
96
|
+
}
|
|
97
|
+
contrib.contentCount += facetContent.length;
|
|
98
|
+
contributions.set(catalyst.manifest.id, contrib);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Only set facet type if there's content
|
|
103
|
+
if (mergedContent.length > 0) {
|
|
104
|
+
mergedFacets[facetType] = mergedContent;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
catalystIds,
|
|
110
|
+
facets: mergedFacets,
|
|
111
|
+
contributions,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Format a facet type name for display
|
|
117
|
+
*/
|
|
118
|
+
function formatFacetName(facetType: FacetType): string {
|
|
119
|
+
const names: Record<FacetType, string> = {
|
|
120
|
+
questions: 'Questions',
|
|
121
|
+
constraints: 'Constraints',
|
|
122
|
+
outputTemplates: 'Output Templates',
|
|
123
|
+
domainKnowledge: 'Domain Knowledge',
|
|
124
|
+
processGuidance: 'Process Guidance',
|
|
125
|
+
validationRules: 'Validation Rules',
|
|
126
|
+
};
|
|
127
|
+
return names[facetType];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render merged facet content into a prompt-ready string
|
|
132
|
+
*
|
|
133
|
+
* Groups content by catalyst source and formats it for use in AI prompts.
|
|
134
|
+
* Each source is labeled and content is separated for readability.
|
|
135
|
+
*
|
|
136
|
+
* @param merged - Merged catalyst
|
|
137
|
+
* @param facetType - Facet type to render
|
|
138
|
+
* @returns Formatted string ready for prompt injection, or empty string if no content
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const merged = mergeCatalysts([catalyst1, catalyst2]);
|
|
143
|
+
* const constraints = renderFacet(merged, 'constraints');
|
|
144
|
+
* // Returns:
|
|
145
|
+
* // From @kjerneverk/base-catalyst:
|
|
146
|
+
* // [content from first catalyst]
|
|
147
|
+
* // From @kjerneverk/nodejs-catalyst:
|
|
148
|
+
* // [content from second catalyst]
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function renderFacet(
|
|
152
|
+
merged: MergedCatalyst,
|
|
153
|
+
facetType: FacetType
|
|
154
|
+
): string {
|
|
155
|
+
const content = merged.facets[facetType];
|
|
156
|
+
|
|
157
|
+
if (!content || content.length === 0) {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
let currentSource = '';
|
|
163
|
+
|
|
164
|
+
for (const item of content) {
|
|
165
|
+
// Add source header when it changes
|
|
166
|
+
if (item.sourceId !== currentSource) {
|
|
167
|
+
if (lines.length > 0) {
|
|
168
|
+
lines.push(''); // Blank line between sources
|
|
169
|
+
}
|
|
170
|
+
lines.push(`From ${item.sourceId}:`);
|
|
171
|
+
currentSource = item.sourceId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add content
|
|
175
|
+
lines.push(item.content);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render all facets from a merged catalyst
|
|
183
|
+
*
|
|
184
|
+
* Returns an object with each facet type mapped to its rendered string.
|
|
185
|
+
* Empty strings for facets with no content.
|
|
186
|
+
*
|
|
187
|
+
* @param merged - Merged catalyst
|
|
188
|
+
* @returns Object with all facets rendered
|
|
189
|
+
*/
|
|
190
|
+
export function renderAllFacets(merged: MergedCatalyst): Record<FacetType, string> {
|
|
191
|
+
const rendered: Partial<Record<FacetType, string>> = {};
|
|
192
|
+
|
|
193
|
+
for (const facetType of FACET_TYPES) {
|
|
194
|
+
rendered[facetType] = renderFacet(merged, facetType);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return rendered as Record<FacetType, string>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate a summary of merged catalysts
|
|
202
|
+
*
|
|
203
|
+
* Returns human-readable text about what was merged and what each
|
|
204
|
+
* catalyst contributed.
|
|
205
|
+
*
|
|
206
|
+
* @param merged - Merged catalyst
|
|
207
|
+
* @returns Summary string
|
|
208
|
+
*/
|
|
209
|
+
export function summarizeMerge(merged: MergedCatalyst): string {
|
|
210
|
+
if (merged.catalystIds.length === 0) {
|
|
211
|
+
return 'No catalysts merged';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lines: string[] = [];
|
|
215
|
+
lines.push(`Merged ${merged.catalystIds.length} catalyst(s):`);
|
|
216
|
+
lines.push('');
|
|
217
|
+
|
|
218
|
+
for (const [catalystId, contrib] of merged.contributions) {
|
|
219
|
+
lines.push(`- ${catalystId}:`);
|
|
220
|
+
lines.push(` - Facets: ${contrib.facetTypes.map(t => formatFacetName(t)).join(', ')}`);
|
|
221
|
+
lines.push(` - Content items: ${contrib.contentCount}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* Catalyst system for RiotPlan - composable, layerable guidance packages for plan creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Type exports
|
|
7
|
+
export type {
|
|
8
|
+
Catalyst,
|
|
9
|
+
CatalystManifest,
|
|
10
|
+
CatalystFacets,
|
|
11
|
+
FacetContent,
|
|
12
|
+
CatalystLoadResult,
|
|
13
|
+
CatalystLoadOptions,
|
|
14
|
+
FacetDirectoryMap,
|
|
15
|
+
PlanManifest,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
// Schema exports
|
|
19
|
+
export {
|
|
20
|
+
CatalystManifestSchema,
|
|
21
|
+
PlanManifestSchema,
|
|
22
|
+
FacetsDeclarationSchema,
|
|
23
|
+
FACET_DIRECTORIES,
|
|
24
|
+
FACET_TYPES,
|
|
25
|
+
} from '@/schema/schemas';
|
|
26
|
+
|
|
27
|
+
export type {
|
|
28
|
+
FacetType,
|
|
29
|
+
CatalystManifestInput,
|
|
30
|
+
CatalystManifestOutput,
|
|
31
|
+
PlanManifestInput,
|
|
32
|
+
PlanManifestOutput,
|
|
33
|
+
FacetsDeclaration,
|
|
34
|
+
} from '@/schema/schemas';
|
|
35
|
+
|
|
36
|
+
// Loader exports
|
|
37
|
+
export {
|
|
38
|
+
loadCatalyst,
|
|
39
|
+
loadCatalystSafe,
|
|
40
|
+
resolveCatalysts,
|
|
41
|
+
} from '@/loader/catalyst-loader';
|
|
42
|
+
|
|
43
|
+
// Merger exports
|
|
44
|
+
export {
|
|
45
|
+
mergeCatalysts,
|
|
46
|
+
renderFacet,
|
|
47
|
+
renderAllFacets,
|
|
48
|
+
summarizeMerge,
|
|
49
|
+
} from '@/merger/facet-merger';
|
|
50
|
+
export type { MergedCatalyst, AttributedContent, MergedFacets } from '@/merger/facet-merger';
|
|
51
|
+
|
|
52
|
+
// Plan manifest exports
|
|
53
|
+
export {
|
|
54
|
+
readPlanManifest,
|
|
55
|
+
writePlanManifest,
|
|
56
|
+
updatePlanManifest,
|
|
57
|
+
addCatalystToManifest,
|
|
58
|
+
removeCatalystFromManifest,
|
|
59
|
+
} from '@/loader/plan-manifest';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for catalyst.yml and plan.yaml manifests
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Facet directory names as they appear on disk
|
|
10
|
+
*/
|
|
11
|
+
export const FACET_DIRECTORIES = {
|
|
12
|
+
questions: 'questions',
|
|
13
|
+
constraints: 'constraints',
|
|
14
|
+
outputTemplates: 'output-templates',
|
|
15
|
+
domainKnowledge: 'domain-knowledge',
|
|
16
|
+
processGuidance: 'process-guidance',
|
|
17
|
+
validationRules: 'validation-rules',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* All valid facet types
|
|
22
|
+
*/
|
|
23
|
+
export const FACET_TYPES = [
|
|
24
|
+
'questions',
|
|
25
|
+
'constraints',
|
|
26
|
+
'outputTemplates',
|
|
27
|
+
'domainKnowledge',
|
|
28
|
+
'processGuidance',
|
|
29
|
+
'validationRules',
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type FacetType = typeof FACET_TYPES[number];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Schema for the facets declaration in catalyst.yml
|
|
36
|
+
* Each facet can be declared as present (true) or explicitly absent (false)
|
|
37
|
+
*/
|
|
38
|
+
export const FacetsDeclarationSchema = z.object({
|
|
39
|
+
questions: z.boolean().optional().describe('Whether this catalyst provides guiding questions'),
|
|
40
|
+
constraints: z.boolean().optional().describe('Whether this catalyst provides constraints/rules'),
|
|
41
|
+
outputTemplates: z.boolean().optional().describe('Whether this catalyst provides output templates'),
|
|
42
|
+
domainKnowledge: z.boolean().optional().describe('Whether this catalyst provides domain knowledge'),
|
|
43
|
+
processGuidance: z.boolean().optional().describe('Whether this catalyst provides process guidance'),
|
|
44
|
+
validationRules: z.boolean().optional().describe('Whether this catalyst provides validation rules'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Semver pattern for version validation
|
|
49
|
+
* Matches: 1.0.0, 1.0.0-dev.0, 1.0.0-alpha.1, etc.
|
|
50
|
+
*/
|
|
51
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* NPM package name pattern
|
|
55
|
+
* Matches: @scope/name, name, @scope/name-with-dashes
|
|
56
|
+
*/
|
|
57
|
+
const NPM_PACKAGE_PATTERN = /^(@[\w-]+\/)?[\w-]+$/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Schema for catalyst.yml manifest file
|
|
61
|
+
*
|
|
62
|
+
* The manifest defines the catalyst's identity and declares which facets it provides.
|
|
63
|
+
* Facets are optional - a catalyst can provide any subset of the six facet types.
|
|
64
|
+
*/
|
|
65
|
+
export const CatalystManifestSchema = z.object({
|
|
66
|
+
/**
|
|
67
|
+
* Catalyst identifier - should match NPM package name
|
|
68
|
+
* @example "@kjerneverk/catalyst-nodejs"
|
|
69
|
+
*/
|
|
70
|
+
id: z.string()
|
|
71
|
+
.regex(NPM_PACKAGE_PATTERN, 'ID must be a valid NPM package name (e.g., @scope/name or name)')
|
|
72
|
+
.describe('Catalyst identifier (NPM package name)'),
|
|
73
|
+
|
|
74
|
+
/** Human-readable name for display */
|
|
75
|
+
name: z.string()
|
|
76
|
+
.min(1, 'Name cannot be empty')
|
|
77
|
+
.describe('Human-readable name'),
|
|
78
|
+
|
|
79
|
+
/** Description of what this catalyst provides */
|
|
80
|
+
description: z.string()
|
|
81
|
+
.min(1, 'Description cannot be empty')
|
|
82
|
+
.describe('What this catalyst provides'),
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Semver version string
|
|
86
|
+
* @example "1.0.0" or "1.0.0-dev.0"
|
|
87
|
+
*/
|
|
88
|
+
version: z.string()
|
|
89
|
+
.regex(SEMVER_PATTERN, 'Version must be valid semver (e.g., 1.0.0 or 1.0.0-dev.0)')
|
|
90
|
+
.describe('Semver version'),
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Declaration of which facets this catalyst provides
|
|
94
|
+
* If omitted, facets are auto-detected from directory structure
|
|
95
|
+
*/
|
|
96
|
+
facets: FacetsDeclarationSchema.optional()
|
|
97
|
+
.describe('Declaration of which facets this catalyst provides'),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Schema for plan.yaml manifest file
|
|
102
|
+
*
|
|
103
|
+
* The plan manifest gives each plan an identity and records which catalysts
|
|
104
|
+
* are associated with it.
|
|
105
|
+
*/
|
|
106
|
+
export const PlanManifestSchema = z.object({
|
|
107
|
+
/** Plan identifier (typically kebab-case) */
|
|
108
|
+
id: z.string()
|
|
109
|
+
.min(1, 'ID cannot be empty')
|
|
110
|
+
.describe('Plan identifier'),
|
|
111
|
+
|
|
112
|
+
/** Human-readable title */
|
|
113
|
+
title: z.string()
|
|
114
|
+
.min(1, 'Title cannot be empty')
|
|
115
|
+
.describe('Human-readable title'),
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ordered list of catalyst IDs associated with this plan
|
|
119
|
+
* Catalysts are applied in order (first = base, last = top layer)
|
|
120
|
+
*/
|
|
121
|
+
catalysts: z.array(z.string())
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('Ordered list of catalyst IDs'),
|
|
124
|
+
|
|
125
|
+
/** ISO timestamp of when the plan was created */
|
|
126
|
+
created: z.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('ISO timestamp of creation'),
|
|
129
|
+
|
|
130
|
+
/** Extensible metadata for future use */
|
|
131
|
+
metadata: z.record(z.string())
|
|
132
|
+
.optional()
|
|
133
|
+
.describe('Extensible metadata'),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inferred TypeScript types from Zod schemas
|
|
138
|
+
*/
|
|
139
|
+
export type CatalystManifestInput = z.input<typeof CatalystManifestSchema>;
|
|
140
|
+
export type CatalystManifestOutput = z.output<typeof CatalystManifestSchema>;
|
|
141
|
+
export type PlanManifestInput = z.input<typeof PlanManifestSchema>;
|
|
142
|
+
export type PlanManifestOutput = z.output<typeof PlanManifestSchema>;
|
|
143
|
+
export type FacetsDeclaration = z.infer<typeof FacetsDeclarationSchema>;
|