@applica-software-guru/sdd-core 0.1.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/dist/config/config-manager.d.ts +9 -0
- package/dist/config/config-manager.d.ts.map +1 -0
- package/dist/config/config-manager.js +44 -0
- package/dist/config/config-manager.js.map +1 -0
- package/dist/delta/delta-engine.d.ts +3 -0
- package/dist/delta/delta-engine.d.ts.map +1 -0
- package/dist/delta/delta-engine.js +18 -0
- package/dist/delta/delta-engine.js.map +1 -0
- package/dist/delta/hasher.d.ts +2 -0
- package/dist/delta/hasher.d.ts.map +1 -0
- package/dist/delta/hasher.js +8 -0
- package/dist/delta/hasher.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +32 -0
- package/dist/errors.js.map +1 -0
- package/dist/git/git.d.ts +12 -0
- package/dist/git/git.d.ts.map +1 -0
- package/dist/git/git.js +125 -0
- package/dist/git/git.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/lock/lock-manager.d.ts +6 -0
- package/dist/lock/lock-manager.d.ts.map +1 -0
- package/dist/lock/lock-manager.js +39 -0
- package/dist/lock/lock-manager.js.map +1 -0
- package/dist/parser/cr-parser.d.ts +8 -0
- package/dist/parser/cr-parser.d.ts.map +1 -0
- package/dist/parser/cr-parser.js +45 -0
- package/dist/parser/cr-parser.js.map +1 -0
- package/dist/parser/frontmatter.d.ts +7 -0
- package/dist/parser/frontmatter.d.ts.map +1 -0
- package/dist/parser/frontmatter.js +25 -0
- package/dist/parser/frontmatter.js.map +1 -0
- package/dist/parser/ref-extractor.d.ts +2 -0
- package/dist/parser/ref-extractor.d.ts.map +1 -0
- package/dist/parser/ref-extractor.js +17 -0
- package/dist/parser/ref-extractor.js.map +1 -0
- package/dist/parser/section-extractor.d.ts +4 -0
- package/dist/parser/section-extractor.d.ts.map +1 -0
- package/dist/parser/section-extractor.js +37 -0
- package/dist/parser/section-extractor.js.map +1 -0
- package/dist/parser/story-parser.d.ts +5 -0
- package/dist/parser/story-parser.d.ts.map +1 -0
- package/dist/parser/story-parser.js +41 -0
- package/dist/parser/story-parser.js.map +1 -0
- package/dist/prompt/prompt-generator.d.ts +3 -0
- package/dist/prompt/prompt-generator.d.ts.map +1 -0
- package/dist/prompt/prompt-generator.js +40 -0
- package/dist/prompt/prompt-generator.js.map +1 -0
- package/dist/scaffold/init.d.ts +3 -0
- package/dist/scaffold/init.d.ts.map +1 -0
- package/dist/scaffold/init.js +64 -0
- package/dist/scaffold/init.js.map +1 -0
- package/dist/scaffold/templates.d.ts +6 -0
- package/dist/scaffold/templates.d.ts.map +1 -0
- package/dist/scaffold/templates.js +164 -0
- package/dist/scaffold/templates.js.map +1 -0
- package/dist/sdd.d.ts +20 -0
- package/dist/sdd.d.ts.map +1 -0
- package/dist/sdd.js +110 -0
- package/dist/sdd.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validate/validator.d.ts +3 -0
- package/dist/validate/validator.d.ts.map +1 -0
- package/dist/validate/validator.js +44 -0
- package/dist/validate/validator.js.map +1 -0
- package/package.json +18 -0
- package/src/config/config-manager.ts +39 -0
- package/src/delta/delta-engine.ts +18 -0
- package/src/errors.ts +27 -0
- package/src/git/git.ts +113 -0
- package/src/index.ts +19 -0
- package/src/parser/cr-parser.ts +41 -0
- package/src/parser/frontmatter.ts +24 -0
- package/src/parser/ref-extractor.ts +14 -0
- package/src/parser/section-extractor.ts +38 -0
- package/src/parser/story-parser.ts +40 -0
- package/src/prompt/prompt-generator.ts +49 -0
- package/src/scaffold/init.ts +71 -0
- package/src/scaffold/templates.ts +166 -0
- package/src/sdd.ts +123 -0
- package/src/types.ts +76 -0
- package/src/validate/validator.ts +46 -0
- package/tests/cr.test.ts +172 -0
- package/tests/delta.test.ts +94 -0
- package/tests/integration.test.ts +132 -0
- package/tests/parser.test.ts +92 -0
- package/tests/prompt.test.ts +57 -0
- package/tests/validator.test.ts +54 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { SDD } from '../src/sdd.js';
|
|
8
|
+
|
|
9
|
+
function git(cmd: string, cwd: string): string {
|
|
10
|
+
return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const VISION_MD = `---
|
|
14
|
+
title: "Product Vision"
|
|
15
|
+
status: new
|
|
16
|
+
author: ""
|
|
17
|
+
last-modified: "2024-01-01T00:00:00.000Z"
|
|
18
|
+
version: "1.0"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Product Vision
|
|
22
|
+
|
|
23
|
+
A test project.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
describe('SDD integration', () => {
|
|
27
|
+
let tempDir: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
tempDir = await mkdtemp(join(tmpdir(), 'sdd-integration-'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await rm(tempDir, { recursive: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('init creates .sdd directory with config and git repo', async () => {
|
|
38
|
+
const sdd = new SDD({ root: tempDir });
|
|
39
|
+
|
|
40
|
+
const created = await sdd.init({ description: 'A test app' });
|
|
41
|
+
expect(created).toContain('.sdd/config.yaml');
|
|
42
|
+
expect(created).toContain('INSTRUCTIONS.md');
|
|
43
|
+
expect(existsSync(join(tempDir, '.sdd'))).toBe(true);
|
|
44
|
+
expect(existsSync(join(tempDir, '.git'))).toBe(true);
|
|
45
|
+
expect(existsSync(join(tempDir, 'product'))).toBe(true);
|
|
46
|
+
expect(existsSync(join(tempDir, 'system'))).toBe(true);
|
|
47
|
+
expect(existsSync(join(tempDir, 'code'))).toBe(true);
|
|
48
|
+
|
|
49
|
+
const config = await sdd.config();
|
|
50
|
+
expect(config.description).toBe('A test app');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('full workflow: init → add doc → status → sync → mark-synced', async () => {
|
|
54
|
+
const sdd = new SDD({ root: tempDir });
|
|
55
|
+
await sdd.init({ description: 'test' });
|
|
56
|
+
git('config user.email "test@test.com"', tempDir);
|
|
57
|
+
git('config user.name "Test"', tempDir);
|
|
58
|
+
|
|
59
|
+
// Simulate user creating a doc file
|
|
60
|
+
await writeFile(join(tempDir, 'product/vision.md'), VISION_MD, 'utf-8');
|
|
61
|
+
|
|
62
|
+
// Status — file should be "new"
|
|
63
|
+
const status = await sdd.status();
|
|
64
|
+
expect(status.files.length).toBe(1);
|
|
65
|
+
expect(status.files[0].status).toBe('new');
|
|
66
|
+
|
|
67
|
+
// Sync — prompt should list the new file
|
|
68
|
+
const prompt = await sdd.sync();
|
|
69
|
+
expect(prompt).toContain('# SDD Sync Prompt');
|
|
70
|
+
expect(prompt).toContain('product/vision.md');
|
|
71
|
+
expect(prompt).toContain('**new**');
|
|
72
|
+
|
|
73
|
+
// Validate
|
|
74
|
+
const validation = await sdd.validate();
|
|
75
|
+
expect(validation.valid).toBe(true);
|
|
76
|
+
|
|
77
|
+
// Mark synced — single file
|
|
78
|
+
const marked = await sdd.markSynced(['product/vision.md']);
|
|
79
|
+
expect(marked).toContain('product/vision.md');
|
|
80
|
+
|
|
81
|
+
// Agent commits after mark-synced
|
|
82
|
+
git('add .', tempDir);
|
|
83
|
+
git('commit -m "sync: vision"', tempDir);
|
|
84
|
+
|
|
85
|
+
// After commit, status should be synced
|
|
86
|
+
const statusAfter = await sdd.status();
|
|
87
|
+
expect(statusAfter.files[0].status).toBe('synced');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('mark-synced with changed status', async () => {
|
|
91
|
+
const sdd = new SDD({ root: tempDir });
|
|
92
|
+
await sdd.init({ description: 'test' });
|
|
93
|
+
git('config user.email "test@test.com"', tempDir);
|
|
94
|
+
git('config user.name "Test"', tempDir);
|
|
95
|
+
|
|
96
|
+
const changedMd = VISION_MD.replace('status: new', 'status: changed');
|
|
97
|
+
await writeFile(join(tempDir, 'product/vision.md'), changedMd, 'utf-8');
|
|
98
|
+
|
|
99
|
+
const marked = await sdd.markSynced();
|
|
100
|
+
expect(marked).toContain('product/vision.md');
|
|
101
|
+
|
|
102
|
+
const content = await readFile(join(tempDir, 'product/vision.md'), 'utf-8');
|
|
103
|
+
expect(content).toContain('status: synced');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('mark-synced with deleted status removes the file', async () => {
|
|
107
|
+
const sdd = new SDD({ root: tempDir });
|
|
108
|
+
await sdd.init({ description: 'test' });
|
|
109
|
+
git('config user.email "test@test.com"', tempDir);
|
|
110
|
+
git('config user.name "Test"', tempDir);
|
|
111
|
+
|
|
112
|
+
const deletedMd = VISION_MD.replace('status: new', 'status: deleted');
|
|
113
|
+
await writeFile(join(tempDir, 'product/vision.md'), deletedMd, 'utf-8');
|
|
114
|
+
|
|
115
|
+
const marked = await sdd.markSynced();
|
|
116
|
+
expect(marked[0]).toContain('removed');
|
|
117
|
+
expect(existsSync(join(tempDir, 'product/vision.md'))).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('throws when project not initialized', async () => {
|
|
121
|
+
const sdd = new SDD({ root: tempDir });
|
|
122
|
+
await expect(sdd.status()).rejects.toThrow('No SDD project found');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('init is idempotent for INSTRUCTIONS.md', async () => {
|
|
126
|
+
const sdd = new SDD({ root: tempDir });
|
|
127
|
+
const first = await sdd.init({ description: 'test' });
|
|
128
|
+
const second = await sdd.init({ description: 'test' });
|
|
129
|
+
expect(first).toContain('INSTRUCTIONS.md');
|
|
130
|
+
expect(second).not.toContain('INSTRUCTIONS.md');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseFrontmatter } from '../src/parser/frontmatter.js';
|
|
3
|
+
import { extractPendingItems, extractAgentNotes } from '../src/parser/section-extractor.js';
|
|
4
|
+
import { extractCrossRefs } from '../src/parser/ref-extractor.js';
|
|
5
|
+
|
|
6
|
+
describe('parseFrontmatter', () => {
|
|
7
|
+
it('parses valid frontmatter', () => {
|
|
8
|
+
const content = `---
|
|
9
|
+
title: "Auth Feature"
|
|
10
|
+
status: draft
|
|
11
|
+
author: "test@example.com"
|
|
12
|
+
last-modified: "2024-01-01"
|
|
13
|
+
version: "1.2"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Content here`;
|
|
17
|
+
|
|
18
|
+
const result = parseFrontmatter('test.md', content);
|
|
19
|
+
expect(result.frontmatter.title).toBe('Auth Feature');
|
|
20
|
+
expect(result.frontmatter.status).toBe('draft');
|
|
21
|
+
expect(result.frontmatter.version).toBe('1.2');
|
|
22
|
+
expect(result.body).toContain('# Content here');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('provides defaults for missing fields', () => {
|
|
26
|
+
const content = `---
|
|
27
|
+
title: "Test"
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
Body`;
|
|
31
|
+
|
|
32
|
+
const result = parseFrontmatter('test.md', content);
|
|
33
|
+
expect(result.frontmatter.status).toBe('draft');
|
|
34
|
+
expect(result.frontmatter.version).toBe('1.0');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('extractPendingItems', () => {
|
|
39
|
+
it('extracts unchecked and checked items', () => {
|
|
40
|
+
const body = `# Feature
|
|
41
|
+
|
|
42
|
+
## Pending Changes
|
|
43
|
+
- [ ] Add validation
|
|
44
|
+
- [x] Create model
|
|
45
|
+
- [ ] Write tests
|
|
46
|
+
|
|
47
|
+
## Other`;
|
|
48
|
+
|
|
49
|
+
const items = extractPendingItems(body);
|
|
50
|
+
expect(items).toHaveLength(3);
|
|
51
|
+
expect(items[0]).toEqual({ text: 'Add validation', checked: false });
|
|
52
|
+
expect(items[1]).toEqual({ text: 'Create model', checked: true });
|
|
53
|
+
expect(items[2]).toEqual({ text: 'Write tests', checked: false });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns empty array when section is missing', () => {
|
|
57
|
+
const items = extractPendingItems('# Just a doc\n\nSome content.');
|
|
58
|
+
expect(items).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('extractAgentNotes', () => {
|
|
63
|
+
it('extracts agent notes section', () => {
|
|
64
|
+
const body = `# Feature
|
|
65
|
+
|
|
66
|
+
## Agent Notes
|
|
67
|
+
Do not modify existing auth logic.
|
|
68
|
+
Follow adapter interface.
|
|
69
|
+
|
|
70
|
+
## Other`;
|
|
71
|
+
|
|
72
|
+
const notes = extractAgentNotes(body);
|
|
73
|
+
expect(notes).toContain('Do not modify existing auth logic.');
|
|
74
|
+
expect(notes).toContain('Follow adapter interface.');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null when section is missing', () => {
|
|
78
|
+
expect(extractAgentNotes('# No notes here')).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('extractCrossRefs', () => {
|
|
83
|
+
it('extracts unique cross-references', () => {
|
|
84
|
+
const body = 'Use [[User]] model and [[Session]] entity. Also reference [[User]] again.';
|
|
85
|
+
const refs = extractCrossRefs(body);
|
|
86
|
+
expect(refs).toEqual(['User', 'Session']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns empty array with no refs', () => {
|
|
90
|
+
expect(extractCrossRefs('No references here.')).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generatePrompt } from '../src/prompt/prompt-generator.js';
|
|
3
|
+
import type { StoryFile } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
function makeFile(overrides: Partial<StoryFile> = {}): StoryFile {
|
|
6
|
+
return {
|
|
7
|
+
relativePath: 'product/features/auth.md',
|
|
8
|
+
frontmatter: {
|
|
9
|
+
title: 'Auth',
|
|
10
|
+
status: 'new',
|
|
11
|
+
author: 'test',
|
|
12
|
+
'last-modified': '2024-01-01T00:00:00.000Z',
|
|
13
|
+
version: '1.0',
|
|
14
|
+
},
|
|
15
|
+
body: '# Auth Feature',
|
|
16
|
+
pendingItems: [],
|
|
17
|
+
agentNotes: null,
|
|
18
|
+
crossRefs: [],
|
|
19
|
+
hash: 'abc',
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('generatePrompt', () => {
|
|
25
|
+
it('generates prompt with new files', () => {
|
|
26
|
+
const files = [makeFile()];
|
|
27
|
+
const prompt = generatePrompt(files, '/tmp');
|
|
28
|
+
expect(prompt).toContain('# SDD Sync Prompt');
|
|
29
|
+
expect(prompt).toContain('product/features/auth.md');
|
|
30
|
+
expect(prompt).toContain('**new**');
|
|
31
|
+
expect(prompt).toContain('Read each file listed above');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('generates prompt with deleted files', () => {
|
|
35
|
+
const files = [makeFile({
|
|
36
|
+
frontmatter: { ...makeFile().frontmatter, status: 'deleted' },
|
|
37
|
+
})];
|
|
38
|
+
const prompt = generatePrompt(files, '/tmp');
|
|
39
|
+
expect(prompt).toContain('**deleted**');
|
|
40
|
+
expect(prompt).toContain('Files to remove');
|
|
41
|
+
expect(prompt).toContain('remove all related code');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('generates prompt with changed files', () => {
|
|
45
|
+
const files = [makeFile({
|
|
46
|
+
frontmatter: { ...makeFile().frontmatter, status: 'changed' },
|
|
47
|
+
})];
|
|
48
|
+
const prompt = generatePrompt(files, '/tmp');
|
|
49
|
+
expect(prompt).toContain('**changed**');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('generates empty prompt when no pending files', () => {
|
|
53
|
+
const prompt = generatePrompt([]);
|
|
54
|
+
expect(prompt).toContain('# SDD Sync Prompt');
|
|
55
|
+
expect(prompt).toContain('Nothing to do');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validate } from '../src/validate/validator.js';
|
|
3
|
+
import type { StoryFile } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
function makeFile(overrides: Partial<StoryFile> = {}): StoryFile {
|
|
6
|
+
return {
|
|
7
|
+
relativePath: 'product/vision.md',
|
|
8
|
+
frontmatter: {
|
|
9
|
+
title: 'Vision',
|
|
10
|
+
status: 'draft',
|
|
11
|
+
author: 'test',
|
|
12
|
+
'last-modified': '2024-01-01',
|
|
13
|
+
version: '1.0',
|
|
14
|
+
},
|
|
15
|
+
body: '# Vision',
|
|
16
|
+
pendingItems: [],
|
|
17
|
+
agentNotes: null,
|
|
18
|
+
crossRefs: [],
|
|
19
|
+
hash: 'abc',
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('validate', () => {
|
|
25
|
+
it('returns valid when no issues', () => {
|
|
26
|
+
const result = validate([makeFile()]);
|
|
27
|
+
expect(result.valid).toBe(true);
|
|
28
|
+
expect(result.issues).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('warns on broken cross-references', () => {
|
|
32
|
+
const entities = makeFile({
|
|
33
|
+
relativePath: 'system/entities.md',
|
|
34
|
+
body: '# Entities\n\n### User\n\nA user.',
|
|
35
|
+
});
|
|
36
|
+
const feature = makeFile({
|
|
37
|
+
relativePath: 'product/features/auth.md',
|
|
38
|
+
crossRefs: ['User', 'NonExistent'],
|
|
39
|
+
});
|
|
40
|
+
const result = validate([entities, feature]);
|
|
41
|
+
expect(result.issues).toHaveLength(1);
|
|
42
|
+
expect(result.issues[0].rule).toBe('broken-ref');
|
|
43
|
+
expect(result.issues[0].message).toContain('NonExistent');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('warns on missing title', () => {
|
|
47
|
+
const file = makeFile({
|
|
48
|
+
frontmatter: { ...makeFile().frontmatter, title: '' },
|
|
49
|
+
});
|
|
50
|
+
const result = validate([file]);
|
|
51
|
+
expect(result.issues).toHaveLength(1);
|
|
52
|
+
expect(result.issues[0].rule).toBe('missing-frontmatter');
|
|
53
|
+
});
|
|
54
|
+
});
|