@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
@@ -0,0 +1,7 @@
1
+ # Documentation Constraints
2
+
3
+ - Every plan must include a documentation step
4
+ - Documentation must cover:
5
+ - README.md
6
+ - AI guide
7
+ - Website updates (if applicable)
@@ -0,0 +1,5 @@
1
+ # Testing Constraints
2
+
3
+ - Every plan must include a testing step
4
+ - Tests must cover happy path and error cases
5
+ - Coverage threshold: 80%
@@ -0,0 +1,11 @@
1
+ # Domain Knowledge
2
+
3
+ ## Technology Stack
4
+ - Node.js with TypeScript
5
+ - ESM modules
6
+ - Vitest for testing
7
+
8
+ ## Conventions
9
+ - Use kebab-case for file names
10
+ - Use camelCase for variables
11
+ - Use PascalCase for types and interfaces
@@ -0,0 +1,16 @@
1
+ # Press Release Template
2
+
3
+ ## [Feature Name] Now Available
4
+
5
+ **[Date]** - [Company] today announced [feature name], which [brief description of what it does].
6
+
7
+ ### Key Benefits
8
+ - Benefit 1
9
+ - Benefit 2
10
+ - Benefit 3
11
+
12
+ ### Customer Quote
13
+ "[Quote from customer about the feature]" - [Customer Name], [Title]
14
+
15
+ ### Availability
16
+ [Feature name] is available starting [date].
@@ -0,0 +1,13 @@
1
+ # Process Guidance
2
+
3
+ ## Planning Lifecycle
4
+
5
+ 1. **Idea** - Explore the concept without commitment
6
+ 2. **Shaping** - Evaluate approaches and tradeoffs
7
+ 3. **Built** - Generate the execution plan
8
+ 4. **Executing** - Work through the steps
9
+
10
+ ## Best Practices
11
+ - Capture all thinking in notes
12
+ - Gather evidence before making decisions
13
+ - Consider multiple approaches before selecting one
@@ -0,0 +1,10 @@
1
+ # Exploration Questions
2
+
3
+ ## Core Questions
4
+ - What problem are you trying to solve?
5
+ - Who is the target audience?
6
+ - What are the success criteria?
7
+
8
+ ## Technical Questions
9
+ - What technologies are involved?
10
+ - What are the integration points?
@@ -0,0 +1,5 @@
1
+ # Shaping Questions
2
+
3
+ - What approaches have you considered?
4
+ - What are the tradeoffs of each approach?
5
+ - What risks need to be mitigated?
@@ -0,0 +1,11 @@
1
+ # Validation Checklist
2
+
3
+ ## Required Steps
4
+ - [ ] Plan has a testing step
5
+ - [ ] Plan has a documentation step
6
+ - [ ] All dependencies are identified
7
+
8
+ ## Quality Checks
9
+ - [ ] Steps are actionable and specific
10
+ - [ ] Acceptance criteria are defined
11
+ - [ ] Dependencies between steps are clear
@@ -0,0 +1,3 @@
1
+ # Some Questions
2
+
3
+ This catalyst has no manifest file, so it should fail validation.
@@ -0,0 +1,7 @@
1
+ id: "@test/partial-catalyst"
2
+ name: Partial Test Catalyst
3
+ description: A test catalyst with only constraints and questions
4
+ version: "1.0.0-dev.0"
5
+ facets:
6
+ questions: true
7
+ constraints: true
@@ -0,0 +1,4 @@
1
+ # General Constraints
2
+
3
+ - Keep it simple
4
+ - Follow existing patterns
@@ -0,0 +1,4 @@
1
+ # Basic Questions
2
+
3
+ - What is the goal?
4
+ - What are the constraints?
@@ -0,0 +1,315 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rmdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import {
6
+ readPlanManifest,
7
+ writePlanManifest,
8
+ updatePlanManifest,
9
+ addCatalystToManifest,
10
+ removeCatalystFromManifest,
11
+ } from '@/loader/plan-manifest';
12
+
13
+ let tempDir: string;
14
+
15
+ beforeEach(async () => {
16
+ tempDir = await mkdtemp(join(tmpdir(), 'catalyst-'));
17
+ });
18
+
19
+ afterEach(async () => {
20
+ try {
21
+ await rmdir(tempDir, { recursive: true });
22
+ } catch {
23
+ // Ignore cleanup errors
24
+ }
25
+ });
26
+
27
+ describe('Plan Manifest', () => {
28
+ describe('readPlanManifest', () => {
29
+ it('returns null for missing manifest', async () => {
30
+ const manifest = await readPlanManifest(tempDir);
31
+ expect(manifest).toBeNull();
32
+ });
33
+
34
+ it('reads a valid manifest', async () => {
35
+ const original: typeof import('@/loader/plan-manifest').PlanManifest = {
36
+ id: 'my-plan',
37
+ title: 'My Plan',
38
+ catalysts: ['@test/catalyst-1'],
39
+ };
40
+
41
+ await writePlanManifest(tempDir, original);
42
+ const read = await readPlanManifest(tempDir);
43
+
44
+ expect(read).toBeDefined();
45
+ expect(read?.id).toBe('my-plan');
46
+ expect(read?.title).toBe('My Plan');
47
+ expect(read?.catalysts).toEqual(['@test/catalyst-1']);
48
+ });
49
+
50
+ it('validates manifest schema', async () => {
51
+ // Write invalid YAML manually
52
+ const { writeFile } = await import('node:fs/promises');
53
+ const manifestPath = join(tempDir, 'plan.yaml');
54
+
55
+ // Missing required field
56
+ await writeFile(manifestPath, 'title: "Only Title"', 'utf-8');
57
+
58
+ await expect(readPlanManifest(tempDir)).rejects.toThrow('Invalid plan manifest');
59
+ });
60
+
61
+ it('includes created timestamp', async () => {
62
+ const manifest: typeof import('@/loader/plan-manifest').PlanManifest = {
63
+ id: 'test-plan',
64
+ title: 'Test',
65
+ };
66
+
67
+ await writePlanManifest(tempDir, manifest);
68
+ const read = await readPlanManifest(tempDir);
69
+
70
+ expect(read?.created).toBeDefined();
71
+ expect(typeof read?.created).toBe('string');
72
+ // Should be a valid ISO timestamp
73
+ expect(new Date(read?.created || '').getTime()).not.toBeNaN();
74
+ });
75
+ });
76
+
77
+ describe('writePlanManifest', () => {
78
+ it('writes a valid manifest', async () => {
79
+ const manifest: typeof import('@/loader/plan-manifest').PlanManifest = {
80
+ id: 'write-test',
81
+ title: 'Write Test',
82
+ catalysts: ['cat1', 'cat2'],
83
+ metadata: { key: 'value' },
84
+ };
85
+
86
+ await writePlanManifest(tempDir, manifest);
87
+ const read = await readPlanManifest(tempDir);
88
+
89
+ expect(read?.id).toBe('write-test');
90
+ expect(read?.title).toBe('Write Test');
91
+ expect(read?.catalysts).toEqual(['cat1', 'cat2']);
92
+ expect(read?.metadata?.key).toBe('value');
93
+ });
94
+
95
+ it('rejects invalid manifest', async () => {
96
+ const invalid = {
97
+ // Missing required 'title'
98
+ id: 'test',
99
+ };
100
+
101
+ await expect(writePlanManifest(tempDir, invalid as any)).rejects.toThrow();
102
+ });
103
+
104
+ it('creates file in YAML format', async () => {
105
+ const manifest: typeof import('@/loader/plan-manifest').PlanManifest = {
106
+ id: 'yaml-test',
107
+ title: 'YAML Test',
108
+ };
109
+
110
+ await writePlanManifest(tempDir, manifest);
111
+
112
+ const { readFileSync } = require('node:fs');
113
+ const content = readFileSync(join(tempDir, 'plan.yaml'), 'utf-8');
114
+
115
+ expect(content).toContain('id: yaml-test');
116
+ expect(content).toContain('title: YAML Test');
117
+ });
118
+ });
119
+
120
+ describe('updatePlanManifest', () => {
121
+ it('creates new manifest if none exists', async () => {
122
+ await updatePlanManifest(tempDir, {
123
+ id: 'new-plan',
124
+ title: 'New Plan',
125
+ });
126
+
127
+ const read = await readPlanManifest(tempDir);
128
+ expect(read?.id).toBe('new-plan');
129
+ expect(read?.title).toBe('New Plan');
130
+ });
131
+
132
+ it('updates specific fields', async () => {
133
+ await writePlanManifest(tempDir, {
134
+ id: 'original',
135
+ title: 'Original Title',
136
+ catalysts: ['cat1'],
137
+ });
138
+
139
+ await updatePlanManifest(tempDir, {
140
+ title: 'Updated Title',
141
+ });
142
+
143
+ const read = await readPlanManifest(tempDir);
144
+ expect(read?.id).toBe('original'); // Unchanged
145
+ expect(read?.title).toBe('Updated Title'); // Updated
146
+ expect(read?.catalysts).toEqual(['cat1']); // Preserved
147
+ });
148
+
149
+ it('merges fields without overwriting', async () => {
150
+ await writePlanManifest(tempDir, {
151
+ id: 'merge-test',
152
+ title: 'Test',
153
+ catalysts: ['cat1', 'cat2'],
154
+ metadata: { existing: 'value' },
155
+ });
156
+
157
+ await updatePlanManifest(tempDir, {
158
+ metadata: { new: 'field' },
159
+ });
160
+
161
+ const read = await readPlanManifest(tempDir);
162
+ // Note: metadata is completely replaced, not merged
163
+ expect(read?.metadata?.new).toBe('field');
164
+ expect(read?.catalysts).toEqual(['cat1', 'cat2']);
165
+ });
166
+
167
+ it('requires id and title for new manifest', async () => {
168
+ await expect(
169
+ updatePlanManifest(tempDir, {
170
+ catalysts: ['cat1'],
171
+ })
172
+ ).rejects.toThrow('Cannot create manifest without id and title');
173
+ });
174
+ });
175
+
176
+ describe('addCatalystToManifest', () => {
177
+ it('adds catalyst to existing manifest', async () => {
178
+ await writePlanManifest(tempDir, {
179
+ id: 'test',
180
+ title: 'Test',
181
+ catalysts: ['cat1'],
182
+ });
183
+
184
+ await addCatalystToManifest(tempDir, 'cat2');
185
+
186
+ const read = await readPlanManifest(tempDir);
187
+ expect(read?.catalysts).toEqual(['cat1', 'cat2']);
188
+ });
189
+
190
+ it('creates catalyst array if missing', async () => {
191
+ await writePlanManifest(tempDir, {
192
+ id: 'test',
193
+ title: 'Test',
194
+ });
195
+
196
+ await addCatalystToManifest(tempDir, 'cat1');
197
+
198
+ const read = await readPlanManifest(tempDir);
199
+ expect(read?.catalysts).toEqual(['cat1']);
200
+ });
201
+
202
+ it('does not add duplicate catalysts', async () => {
203
+ await writePlanManifest(tempDir, {
204
+ id: 'test',
205
+ title: 'Test',
206
+ catalysts: ['cat1'],
207
+ });
208
+
209
+ await addCatalystToManifest(tempDir, 'cat1');
210
+
211
+ const read = await readPlanManifest(tempDir);
212
+ expect(read?.catalysts).toEqual(['cat1']);
213
+ });
214
+ });
215
+
216
+ describe('removeCatalystFromManifest', () => {
217
+ it('removes catalyst from manifest', async () => {
218
+ await writePlanManifest(tempDir, {
219
+ id: 'test',
220
+ title: 'Test',
221
+ catalysts: ['cat1', 'cat2', 'cat3'],
222
+ });
223
+
224
+ await removeCatalystFromManifest(tempDir, 'cat2');
225
+
226
+ const read = await readPlanManifest(tempDir);
227
+ expect(read?.catalysts).toEqual(['cat1', 'cat3']);
228
+ });
229
+
230
+ it('removes catalysts array if last one removed', async () => {
231
+ await writePlanManifest(tempDir, {
232
+ id: 'test',
233
+ title: 'Test',
234
+ catalysts: ['cat1'],
235
+ });
236
+
237
+ await removeCatalystFromManifest(tempDir, 'cat1');
238
+
239
+ const read = await readPlanManifest(tempDir);
240
+ expect(read?.catalysts).toBeUndefined();
241
+ });
242
+
243
+ it('does nothing if catalyst not found', async () => {
244
+ await writePlanManifest(tempDir, {
245
+ id: 'test',
246
+ title: 'Test',
247
+ catalysts: ['cat1'],
248
+ });
249
+
250
+ await removeCatalystFromManifest(tempDir, 'cat-nonexistent');
251
+
252
+ const read = await readPlanManifest(tempDir);
253
+ expect(read?.catalysts).toEqual(['cat1']);
254
+ });
255
+ });
256
+
257
+ describe('roundtrip tests', () => {
258
+ it('write and read preserve all fields', async () => {
259
+ const original: typeof import('@/loader/plan-manifest').PlanManifest = {
260
+ id: 'roundtrip',
261
+ title: 'Roundtrip Test',
262
+ catalysts: ['@org/catalyst-1', '@org/catalyst-2'],
263
+ metadata: { key1: 'value1', key2: 'value2' },
264
+ created: '2026-02-08T12:00:00Z',
265
+ };
266
+
267
+ await writePlanManifest(tempDir, original);
268
+ const read = await readPlanManifest(tempDir);
269
+
270
+ expect(read?.id).toBe(original.id);
271
+ expect(read?.title).toBe(original.title);
272
+ expect(read?.catalysts).toEqual(original.catalysts);
273
+ expect(read?.metadata).toEqual(original.metadata);
274
+ expect(read?.created).toBe(original.created);
275
+ });
276
+
277
+ it('multiple updates preserve data', async () => {
278
+ const manifest: typeof import('@/loader/plan-manifest').PlanManifest = {
279
+ id: 'multi-update',
280
+ title: 'Multi Update',
281
+ };
282
+
283
+ await writePlanManifest(tempDir, manifest);
284
+ await addCatalystToManifest(tempDir, 'cat1');
285
+ await addCatalystToManifest(tempDir, 'cat2');
286
+ await updatePlanManifest(tempDir, { metadata: { step: '1' } });
287
+
288
+ const read = await readPlanManifest(tempDir);
289
+ expect(read?.id).toBe('multi-update');
290
+ expect(read?.catalysts).toEqual(['cat1', 'cat2']);
291
+ expect(read?.metadata?.step).toBe('1');
292
+ });
293
+ });
294
+
295
+ describe('backward compatibility', () => {
296
+ it('handles missing manifest gracefully', async () => {
297
+ const manifest = await readPlanManifest(tempDir);
298
+ expect(manifest).toBeNull();
299
+ // Should not throw
300
+ });
301
+
302
+ it('plans without catalysts work fine', async () => {
303
+ const manifest: typeof import('@/loader/plan-manifest').PlanManifest = {
304
+ id: 'no-catalysts',
305
+ title: 'No Catalysts',
306
+ };
307
+
308
+ await writePlanManifest(tempDir, manifest);
309
+ const read = await readPlanManifest(tempDir);
310
+
311
+ expect(read?.id).toBe('no-catalysts');
312
+ expect(read?.catalysts).toBeUndefined();
313
+ });
314
+ });
315
+ });