@applica-software-guru/sdd-core 1.7.0 → 1.8.1

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.
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { execSync } from 'node:child_process';
7
+ import { createHash } from 'node:crypto';
7
8
  import { SDD } from '../src/sdd.js';
8
9
  import { readRemoteState } from '../src/remote/state.js';
9
10
  import type { RemoteDocResponse, RemoteDocBulkResponse, RemoteCRResponse, RemoteBugResponse } from '../src/remote/types.js';
@@ -12,6 +13,10 @@ function git(cmd: string, cwd: string): string {
12
13
  return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
13
14
  }
14
15
 
16
+ function sha256(content: string): string {
17
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
18
+ }
19
+
15
20
  const VISION_MD = `---
16
21
  title: "Product Vision"
17
22
  status: new
@@ -43,6 +48,67 @@ function makeDocResponse(overrides: Partial<RemoteDocResponse> = {}): RemoteDocR
43
48
  };
44
49
  }
45
50
 
51
+ function makeCRResponse(overrides: Partial<RemoteCRResponse> = {}): RemoteCRResponse {
52
+ return {
53
+ id: 'cr-001',
54
+ project_id: 'proj-001',
55
+ path: 'change-requests/CR-001.md',
56
+ title: 'Add auth flow',
57
+ body: '## Description\n\nAdd JWT authentication.\n',
58
+ status: 'pending',
59
+ author_id: 'user-1',
60
+ assignee_id: null,
61
+ target_files: null,
62
+ closed_at: null,
63
+ created_at: '2026-01-01T00:00:00.000Z',
64
+ updated_at: '2026-01-01T00:00:00.000Z',
65
+ ...overrides,
66
+ };
67
+ }
68
+
69
+ function makeBugResponse(overrides: Partial<RemoteBugResponse> = {}): RemoteBugResponse {
70
+ return {
71
+ id: 'bug-001',
72
+ project_id: 'proj-001',
73
+ path: 'bugs/BUG-001.md',
74
+ title: 'Search returns stale results',
75
+ body: '## Steps\n\n1. Search for a term\n2. Results are outdated\n',
76
+ status: 'open',
77
+ severity: 'major',
78
+ author_id: 'user-1',
79
+ assignee_id: null,
80
+ closed_at: null,
81
+ created_at: '2026-02-01T00:00:00.000Z',
82
+ updated_at: '2026-02-01T00:00:00.000Z',
83
+ ...overrides,
84
+ };
85
+ }
86
+
87
+ const APPLIED_CR = `---
88
+ title: "Add auth flow"
89
+ status: applied
90
+ author: "user"
91
+ created-at: "2026-01-01T00:00:00.000Z"
92
+ ---
93
+
94
+ ## Description
95
+
96
+ Add JWT authentication.
97
+ `;
98
+
99
+ const RESOLVED_BUG = `---
100
+ title: "Search returns stale results"
101
+ status: resolved
102
+ author: "user"
103
+ created-at: "2026-02-01T00:00:00.000Z"
104
+ ---
105
+
106
+ ## Steps
107
+
108
+ 1. Search for a term
109
+ 2. Results are outdated
110
+ `;
111
+
46
112
  describe('Sync engine - push', () => {
47
113
  let tempDir: string;
48
114
  let sdd: SDD;
@@ -67,7 +133,7 @@ describe('Sync engine - push', () => {
67
133
  await rm(tempDir, { recursive: true });
68
134
  });
69
135
 
70
- it('pushes pending files and marks them synced', async () => {
136
+ it('pushes pending files and preserves local frontmatter status', async () => {
71
137
  await writeFile(join(tempDir, 'product/vision.md'), VISION_MD, 'utf-8');
72
138
 
73
139
  const pushResponse: RemoteDocBulkResponse = {
@@ -87,9 +153,9 @@ describe('Sync engine - push', () => {
87
153
  expect(result.updated).toBe(0);
88
154
  expect(result.pushed).toContain('product/vision.md');
89
155
 
90
- // Local file should now be synced
156
+ // Push should not mutate markdown frontmatter status
91
157
  const content = await readFile(join(tempDir, 'product/vision.md'), 'utf-8');
92
- expect(content).toContain('status: synced');
158
+ expect(content).toContain('status: new');
93
159
 
94
160
  // Remote state should be updated
95
161
  const state = await readRemoteState(tempDir);
@@ -101,6 +167,20 @@ describe('Sync engine - push', () => {
101
167
  it('skips synced files when no paths specified', async () => {
102
168
  await writeFile(join(tempDir, 'product/vision.md'), VISION_SYNCED, 'utf-8');
103
169
 
170
+ const { writeRemoteState } = await import('../src/remote/state.js');
171
+ await writeRemoteState(tempDir, {
172
+ documents: {
173
+ 'product/vision.md': {
174
+ remoteId: 'doc-001',
175
+ remoteVersion: 1,
176
+ localHash: sha256(VISION_SYNCED),
177
+ lastSynced: '2026-01-01T00:00:00.000Z',
178
+ },
179
+ },
180
+ changeRequests: {},
181
+ bugs: {},
182
+ });
183
+
104
184
  const result = await sdd.push();
105
185
  expect(result.pushed).toHaveLength(0);
106
186
  });
@@ -123,6 +203,77 @@ describe('Sync engine - push', () => {
123
203
  expect(result.pushed).toContain('product/vision.md');
124
204
  expect(result.updated).toBe(1);
125
205
  });
206
+
207
+ it('repopulates remote from local files after a remote reset', async () => {
208
+ await writeFile(join(tempDir, 'product/vision.md'), VISION_SYNCED, 'utf-8');
209
+ await mkdir(join(tempDir, 'change-requests'), { recursive: true });
210
+ await writeFile(join(tempDir, 'change-requests/CR-001.md'), APPLIED_CR, 'utf-8');
211
+ await mkdir(join(tempDir, 'bugs'), { recursive: true });
212
+ await writeFile(join(tempDir, 'bugs/BUG-001.md'), RESOLVED_BUG, 'utf-8');
213
+
214
+ globalThis.fetch = vi.fn()
215
+ .mockResolvedValueOnce({
216
+ ok: true,
217
+ status: 200,
218
+ json: async () => ({
219
+ message: 'Remote project reset.',
220
+ deleted_documents: 4,
221
+ deleted_change_requests: 2,
222
+ deleted_bugs: 1,
223
+ deleted_comments: 0,
224
+ deleted_notifications: 0,
225
+ }),
226
+ })
227
+ .mockResolvedValueOnce({
228
+ ok: true,
229
+ status: 200,
230
+ json: async () => ({
231
+ created: 1,
232
+ updated: 0,
233
+ documents: [makeDocResponse()],
234
+ }),
235
+ })
236
+ .mockResolvedValueOnce({
237
+ ok: true,
238
+ status: 200,
239
+ json: async () => ({
240
+ created: 1,
241
+ updated: 0,
242
+ change_requests: [makeCRResponse()],
243
+ }),
244
+ })
245
+ .mockResolvedValueOnce({
246
+ ok: true,
247
+ status: 200,
248
+ json: async () => ({
249
+ created: 1,
250
+ updated: 0,
251
+ bugs: [makeBugResponse()],
252
+ }),
253
+ });
254
+
255
+ await sdd.remoteReset('test-project');
256
+
257
+ const resetState = await readRemoteState(tempDir);
258
+ expect(resetState.needsReseed).toBe(true);
259
+ expect(resetState.documents).toEqual({});
260
+ expect(resetState.changeRequests).toEqual({});
261
+ expect(resetState.bugs).toEqual({});
262
+
263
+ const result = await sdd.push();
264
+
265
+ expect(result.created).toBe(3);
266
+ expect(result.updated).toBe(0);
267
+ expect(result.pushed).toContain('product/vision.md');
268
+ expect(result.pushed).toContain('change-requests/CR-001.md');
269
+ expect(result.pushed).toContain('bugs/BUG-001.md');
270
+
271
+ const state = await readRemoteState(tempDir);
272
+ expect(state.needsReseed).toBe(false);
273
+ expect(state.documents['product/vision.md']?.remoteId).toBe('doc-001');
274
+ expect(state.changeRequests?.['change-requests/CR-001.md']?.remoteId).toBe('cr-001');
275
+ expect(state.bugs?.['bugs/BUG-001.md']?.remoteId).toBe('bug-001');
276
+ });
126
277
  });
127
278
 
128
279
  describe('Sync engine - pull', () => {
@@ -505,9 +656,9 @@ Easypick is a marketplace for on-demand services.
505
656
  });
506
657
  await sdd.push();
507
658
 
508
- // Read file after push (status changed to synced)
659
+ // Read file after push (status is preserved)
509
660
  const afterPush = await readFile(join(tempDir, 'product/vision.md'), 'utf-8');
510
- expect(afterPush).toContain('status: synced');
661
+ expect(afterPush).toContain('status: new');
511
662
 
512
663
  // Pull — server returns same content, higher version
513
664
  globalThis.fetch = vi.fn().mockResolvedValue({