@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,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,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,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
|
+
});
|