Skip to content

Commit ceb3269

Browse files
committed
feat(acceptance-criteria): implement change tracking and note creation for acceptance criteria updates
1 parent 2d9b0c9 commit ceb3269

File tree

9 files changed

+169
-22
lines changed

9 files changed

+169
-22
lines changed

.devlog/entries/287-migrate-existing-json-devlog-entries-to-postgresql.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77
"status": "new",
88
"priority": "high",
99
"createdAt": "2025-07-27T14:46:29.476Z",
10-
"updatedAt": "2025-07-27T14:46:29.476Z",
11-
"notes": [],
10+
"updatedAt": "2025-07-27T15:27:24.888Z",
11+
"notes": [
12+
{
13+
"id": "d27fc3ec-db94-4b68-94b1-ea9745c2d110",
14+
"timestamp": "2025-07-27T15:27:24.888Z",
15+
"category": "progress",
16+
"content": "Starting JSON to PostgreSQL migration. User has truncated existing PG entries and wants fast bulk migration approach. Need to:\n1. Explore existing JSON devlog structure \n2. Map to flattened DevlogEntry structure from #286\n3. Create efficient bulk insert script\n4. Use PostgreSQL connection from .env file"
17+
}
18+
],
1219
"files": [],
1320
"relatedDevlogs": [],
1421
"context": {

packages/core/src/entities/devlog-entry.entity.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,4 @@ export class DevlogEntryEntity {
8585

8686
@JsonColumn({ default: getStorageType() === 'sqlite' ? '[]' : [], name: 'acceptance_criteria' })
8787
acceptanceCriteria!: string[];
88-
89-
// Simple arrays that can remain as JSON columns
90-
@JsonColumn({ default: getStorageType() === 'sqlite' ? '[]' : [] })
91-
files!: string[];
92-
93-
@JsonColumn({ default: getStorageType() === 'sqlite' ? '[]' : [], name: 'related_devlogs' })
94-
relatedDevlogs!: string[];
9588
}

packages/core/src/entities/devlog-note.entity.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export class DevlogNoteEntity {
3939
@Column({ type: 'text', nullable: true, name: 'code_changes' })
4040
codeChanges?: string;
4141

42+
// Metadata for special note types (e.g., acceptance-criteria changes)
43+
@JsonColumn({ nullable: true, default: null })
44+
metadata?: {
45+
previousCriteria?: string[];
46+
newCriteria?: string[];
47+
changeType?: 'added' | 'removed' | 'modified' | 'reordered';
48+
};
49+
4250
// Foreign key relationship
4351
@ManyToOne(() => DevlogEntryEntity, { onDelete: 'CASCADE' })
4452
@JoinColumn({ name: 'devlog_id' })

packages/core/src/managers/devlog/workspace-devlog-manager.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { join } from 'path';
77
import { homedir } from 'os';
88
import * as crypto from 'crypto';
9+
import { createAcceptanceCriteriaNote } from '../../utils/acceptance-criteria.js';
910
import type {
1011
CreateDevlogRequest,
1112
DevlogEntry,
@@ -370,8 +371,6 @@ export class WorkspaceDevlogManager {
370371
updatedAt: now,
371372
assignee: request.assignee,
372373
notes: [],
373-
files: [],
374-
relatedDevlogs: [],
375374
acceptanceCriteria: request.acceptanceCriteria || [],
376375
businessContext: request.businessContext || '',
377376
technicalContext: request.technicalContext || '',
@@ -443,10 +442,32 @@ export class WorkspaceDevlogManager {
443442
initialInsights !== undefined ||
444443
relatedPatterns !== undefined
445444
) {
446-
// Update flattened context fields directly
445+
// Update other flattened context fields directly
447446
if (businessContext !== undefined) updated.businessContext = businessContext;
448447
if (technicalContext !== undefined) updated.technicalContext = technicalContext;
449-
if (acceptanceCriteria !== undefined) updated.acceptanceCriteria = acceptanceCriteria;
448+
}
449+
450+
// Handle acceptance criteria updates with automatic change tracking
451+
if (acceptanceCriteria !== undefined) {
452+
const previousCriteria = existing.acceptanceCriteria || [];
453+
updated.acceptanceCriteria = acceptanceCriteria;
454+
455+
// Create automatic AC change note if criteria actually changed
456+
if (JSON.stringify(previousCriteria) !== JSON.stringify(acceptanceCriteria)) {
457+
const acNote = createAcceptanceCriteriaNote(
458+
previousCriteria,
459+
acceptanceCriteria,
460+
data.acChangeReason, // Optional reason from the update request
461+
);
462+
463+
// Add the note to the entry (will be saved with the entry)
464+
if (!updated.notes) updated.notes = [];
465+
updated.notes.push({
466+
id: crypto.randomUUID(),
467+
timestamp: now,
468+
...acNote,
469+
});
470+
}
450471
}
451472

452473
// Ensure closedAt is set when status changes to 'done' or 'cancelled'

packages/core/src/storage/providers/typeorm-storage.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,6 @@ export class TypeORMStorageProvider implements StorageProvider {
430430
closedAt: entity.closedAt?.toISOString(),
431431
archived: entity.archived,
432432
assignee: entity.assignee,
433-
files: this.parseJsonField(entity.files, []),
434-
relatedDevlogs: this.parseJsonField(entity.relatedDevlogs, []),
435433
acceptanceCriteria: this.parseJsonField(entity.acceptanceCriteria, []),
436434
businessContext: entity.businessContext,
437435
technicalContext: entity.technicalContext,
@@ -456,8 +454,6 @@ export class TypeORMStorageProvider implements StorageProvider {
456454
if (entry.closedAt) entity.closedAt = new Date(entry.closedAt);
457455
entity.archived = entry.archived || false;
458456
entity.assignee = entry.assignee;
459-
entity.files = this.stringifyJsonField(entry.files || []);
460-
entity.relatedDevlogs = this.stringifyJsonField(entry.relatedDevlogs || []);
461457
entity.acceptanceCriteria = this.stringifyJsonField(entry.acceptanceCriteria || []);
462458
entity.businessContext = entry.businessContext;
463459
entity.technicalContext = entry.technicalContext;

packages/core/src/types/core.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ export interface DevlogNote {
158158
content: string;
159159
files?: string[];
160160
codeChanges?: string;
161+
// Metadata for special note types (e.g., acceptance-criteria changes)
162+
metadata?: {
163+
// For acceptance-criteria category
164+
previousCriteria?: string[];
165+
newCriteria?: string[];
166+
changeType?: 'added' | 'removed' | 'modified' | 'reordered';
167+
};
161168
}
162169

163170
export interface DevlogEntry {
@@ -174,12 +181,8 @@ export interface DevlogEntry {
174181
assignee?: string;
175182
archived?: boolean; // For long-term management and performance
176183

177-
// Simple arrays that remain as JSON
178-
files?: string[];
179-
relatedDevlogs?: string[];
184+
// Flattened context fields
180185
acceptanceCriteria?: string[];
181-
182-
// Flattened context fields (simple strings)
183186
businessContext?: string;
184187
technicalContext?: string;
185188

packages/core/src/types/requests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface UpdateDevlogRequest {
4343
businessContext?: string;
4444
technicalContext?: string;
4545
acceptanceCriteria?: string[];
46+
acChangeReason?: string; // Optional reason for AC changes (creates better change notes)
4647
initialInsights?: string[];
4748
relatedPatterns?: string[];
4849

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Utility functions for Acceptance Criteria change tracking
3+
*/
4+
5+
import { DevlogNote } from '../types/index.js';
6+
7+
/**
8+
* Compare two arrays of acceptance criteria and determine what changed
9+
*/
10+
export function compareAcceptanceCriteria(
11+
previous: string[],
12+
current: string[],
13+
): {
14+
changeType: 'added' | 'removed' | 'modified' | 'reordered';
15+
addedItems: string[];
16+
removedItems: string[];
17+
modifiedItems: Array<{ old: string; new: string }>;
18+
} {
19+
const prevSet = new Set(previous);
20+
const currSet = new Set(current);
21+
22+
const addedItems = current.filter((item) => !prevSet.has(item));
23+
const removedItems = previous.filter((item) => !currSet.has(item));
24+
25+
// For simplicity, we'll treat any differences as modifications
26+
// More sophisticated diff logic could be added later
27+
const modifiedItems: Array<{ old: string; new: string }> = [];
28+
29+
// Determine primary change type
30+
let changeType: 'added' | 'removed' | 'modified' | 'reordered';
31+
if (addedItems.length > 0 && removedItems.length === 0) {
32+
changeType = 'added';
33+
} else if (removedItems.length > 0 && addedItems.length === 0) {
34+
changeType = 'removed';
35+
} else if (addedItems.length > 0 || removedItems.length > 0) {
36+
changeType = 'modified';
37+
} else if (JSON.stringify(previous) !== JSON.stringify(current)) {
38+
changeType = 'reordered';
39+
} else {
40+
changeType = 'modified'; // fallback
41+
}
42+
43+
return {
44+
changeType,
45+
addedItems,
46+
removedItems,
47+
modifiedItems,
48+
};
49+
}
50+
51+
/**
52+
* Create a formatted note content for acceptance criteria changes
53+
*/
54+
export function createAcceptanceCriteriaChangeNote(
55+
previous: string[],
56+
current: string[],
57+
changeReason?: string,
58+
): { content: string; metadata: DevlogNote['metadata'] } {
59+
const comparison = compareAcceptanceCriteria(previous, current);
60+
61+
let content = '**Acceptance Criteria Updated**\n\n';
62+
63+
if (changeReason) {
64+
content += `**Reason:** ${changeReason}\n\n`;
65+
}
66+
67+
if (comparison.addedItems.length > 0) {
68+
content += '**Added:**\n';
69+
comparison.addedItems.forEach((item) => {
70+
content += `+ ${item}\n`;
71+
});
72+
content += '\n';
73+
}
74+
75+
if (comparison.removedItems.length > 0) {
76+
content += '**Removed:**\n';
77+
comparison.removedItems.forEach((item) => {
78+
content += `- ${item}\n`;
79+
});
80+
content += '\n';
81+
}
82+
83+
if (comparison.changeType === 'reordered') {
84+
content += '**Reordered acceptance criteria**\n\n';
85+
}
86+
87+
content += '**Current Acceptance Criteria:**\n';
88+
current.forEach((item, index) => {
89+
content += `${index + 1}. ${item}\n`;
90+
});
91+
92+
const metadata: DevlogNote['metadata'] = {
93+
previousCriteria: previous,
94+
newCriteria: current,
95+
changeType: comparison.changeType,
96+
};
97+
98+
return { content, metadata };
99+
}
100+
101+
/**
102+
* Helper to create a complete DevlogNote for AC changes
103+
*/
104+
export function createAcceptanceCriteriaNote(
105+
previous: string[],
106+
current: string[],
107+
changeReason?: string,
108+
changedBy?: string,
109+
): Omit<DevlogNote, 'id' | 'timestamp'> {
110+
const { content, metadata } = createAcceptanceCriteriaChangeNote(previous, current, changeReason);
111+
112+
return {
113+
category: 'acceptance-criteria',
114+
content,
115+
metadata,
116+
};
117+
}

packages/core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './filter-mapping.js';
66
export * from './common.js';
77
export * from './errors.js';
88
export * from './env-loader.js';
9+
export * from './acceptance-criteria.js';

0 commit comments

Comments
 (0)