@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.
Files changed (96) hide show
  1. package/dist/config/config-manager.d.ts +9 -0
  2. package/dist/config/config-manager.d.ts.map +1 -0
  3. package/dist/config/config-manager.js +44 -0
  4. package/dist/config/config-manager.js.map +1 -0
  5. package/dist/delta/delta-engine.d.ts +3 -0
  6. package/dist/delta/delta-engine.d.ts.map +1 -0
  7. package/dist/delta/delta-engine.js +18 -0
  8. package/dist/delta/delta-engine.js.map +1 -0
  9. package/dist/delta/hasher.d.ts +2 -0
  10. package/dist/delta/hasher.d.ts.map +1 -0
  11. package/dist/delta/hasher.js +8 -0
  12. package/dist/delta/hasher.js.map +1 -0
  13. package/dist/errors.d.ts +13 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +32 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/git/git.d.ts +12 -0
  18. package/dist/git/git.d.ts.map +1 -0
  19. package/dist/git/git.js +125 -0
  20. package/dist/git/git.js.map +1 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lock/lock-manager.d.ts +6 -0
  26. package/dist/lock/lock-manager.d.ts.map +1 -0
  27. package/dist/lock/lock-manager.js +39 -0
  28. package/dist/lock/lock-manager.js.map +1 -0
  29. package/dist/parser/cr-parser.d.ts +8 -0
  30. package/dist/parser/cr-parser.d.ts.map +1 -0
  31. package/dist/parser/cr-parser.js +45 -0
  32. package/dist/parser/cr-parser.js.map +1 -0
  33. package/dist/parser/frontmatter.d.ts +7 -0
  34. package/dist/parser/frontmatter.d.ts.map +1 -0
  35. package/dist/parser/frontmatter.js +25 -0
  36. package/dist/parser/frontmatter.js.map +1 -0
  37. package/dist/parser/ref-extractor.d.ts +2 -0
  38. package/dist/parser/ref-extractor.d.ts.map +1 -0
  39. package/dist/parser/ref-extractor.js +17 -0
  40. package/dist/parser/ref-extractor.js.map +1 -0
  41. package/dist/parser/section-extractor.d.ts +4 -0
  42. package/dist/parser/section-extractor.d.ts.map +1 -0
  43. package/dist/parser/section-extractor.js +37 -0
  44. package/dist/parser/section-extractor.js.map +1 -0
  45. package/dist/parser/story-parser.d.ts +5 -0
  46. package/dist/parser/story-parser.d.ts.map +1 -0
  47. package/dist/parser/story-parser.js +41 -0
  48. package/dist/parser/story-parser.js.map +1 -0
  49. package/dist/prompt/prompt-generator.d.ts +3 -0
  50. package/dist/prompt/prompt-generator.d.ts.map +1 -0
  51. package/dist/prompt/prompt-generator.js +40 -0
  52. package/dist/prompt/prompt-generator.js.map +1 -0
  53. package/dist/scaffold/init.d.ts +3 -0
  54. package/dist/scaffold/init.d.ts.map +1 -0
  55. package/dist/scaffold/init.js +64 -0
  56. package/dist/scaffold/init.js.map +1 -0
  57. package/dist/scaffold/templates.d.ts +6 -0
  58. package/dist/scaffold/templates.d.ts.map +1 -0
  59. package/dist/scaffold/templates.js +164 -0
  60. package/dist/scaffold/templates.js.map +1 -0
  61. package/dist/sdd.d.ts +20 -0
  62. package/dist/sdd.d.ts.map +1 -0
  63. package/dist/sdd.js +110 -0
  64. package/dist/sdd.js.map +1 -0
  65. package/dist/types.d.ts +65 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +3 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/validate/validator.d.ts +3 -0
  70. package/dist/validate/validator.d.ts.map +1 -0
  71. package/dist/validate/validator.js +44 -0
  72. package/dist/validate/validator.js.map +1 -0
  73. package/package.json +18 -0
  74. package/src/config/config-manager.ts +39 -0
  75. package/src/delta/delta-engine.ts +18 -0
  76. package/src/errors.ts +27 -0
  77. package/src/git/git.ts +113 -0
  78. package/src/index.ts +19 -0
  79. package/src/parser/cr-parser.ts +41 -0
  80. package/src/parser/frontmatter.ts +24 -0
  81. package/src/parser/ref-extractor.ts +14 -0
  82. package/src/parser/section-extractor.ts +38 -0
  83. package/src/parser/story-parser.ts +40 -0
  84. package/src/prompt/prompt-generator.ts +49 -0
  85. package/src/scaffold/init.ts +71 -0
  86. package/src/scaffold/templates.ts +166 -0
  87. package/src/sdd.ts +123 -0
  88. package/src/types.ts +76 -0
  89. package/src/validate/validator.ts +46 -0
  90. package/tests/cr.test.ts +172 -0
  91. package/tests/delta.test.ts +94 -0
  92. package/tests/integration.test.ts +132 -0
  93. package/tests/parser.test.ts +92 -0
  94. package/tests/prompt.test.ts +57 -0
  95. package/tests/validator.test.ts +54 -0
  96. 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
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }