Skip to content

Commit a9fc506

Browse files
committed
feat(discovery): Implement comprehensive devlog discovery feature to prevent duplicate work
1 parent 9cadf9e commit a9fc506

File tree

6 files changed

+249
-65
lines changed

6 files changed

+249
-65
lines changed

.github/copilot-instructions.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
This project uses **itself** for development tracking. When working on devlog features, ALWAYS:
2424

2525
### 1. Use Devlog for All Development Work
26-
- Create devlog entries for new features, bugs, or improvements using `@devlog/mcp`
27-
- Use `find_or_create_devlog` to prevent duplicates
26+
- **Always discover first**: Use `discover_related_devlogs` to find existing relevant work before creating new entries
27+
- Create devlog entries for new features, bugs, or improvements using `@devlog/mcp` only after thorough discovery
2828
- Track progress with notes and status updates through the MCP server tools
2929

3030
### 2. Standard Entry Format
@@ -39,13 +39,15 @@ This project uses **itself** for development tracking. When working on devlog fe
3939
```
4040

4141
### 3. Key Practices
42-
- **Always check existing entries** before creating new ones using MCP server tools
42+
- **Always discover first**: Use `discover_related_devlogs` before creating new entries to prevent duplicate work
43+
- **Review related work**: Analyze discovered entries to build upon existing insights and avoid overlapping efforts
4344
- **Update progress** as work continues through devlog MCP functions
4445
- **Document decisions** and technical details in notes using `add_devlog_note`
4546
- **Use enterprise integrations** when configured via MCP sync functions
4647
- **Demonstrate new features** by using them through the MCP interface
4748

4849
### 4. Duplicate Prevention
50+
- Use `discover_related_devlogs` to thoroughly search for existing relevant work before creating new entries
4951
- Use `find_or_create_devlog` instead of `create_devlog`
5052
- Same title + same type = same entry (by design)
5153
- Different types can have same title (different IDs)
@@ -57,11 +59,12 @@ This project uses **itself** for development tracking. When working on devlog fe
5759
- **Duplicate Prevention**: Built-in safeguards against creating duplicate entries
5860

5961
### 6. When Adding New Features
60-
1. Create devlog entry for the feature using MCP server
61-
2. Use the feature to track its own development
62-
3. Update the entry as you implement via MCP functions
63-
4. Document the feature in the entry notes using MCP tools
64-
5. Demo the feature by using it through the MCP interface
62+
1. **Discover first**: Use `discover_related_devlogs` to find existing relevant work
63+
2. Create devlog entry for the feature using MCP server only if no overlapping work exists
64+
3. Use the feature to track its own development
65+
4. Update the entry as you implement via MCP functions
66+
5. Document the feature in the entry notes using MCP tools
67+
6. Demo the feature by using it through the MCP interface
6568

6669
This ensures the devlog system is continuously tested and improved through real-world usage.
6770

packages/core/src/devlog-manager.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ import type {
1717
EnterpriseIntegration
1818
} from "@devlog/types";
1919

20+
// Discovery-related interfaces
21+
export interface DiscoverDevlogsRequest {
22+
workDescription: string;
23+
workType: DevlogType;
24+
keywords?: string[];
25+
scope?: string;
26+
}
27+
28+
export interface DiscoveredDevlogEntry {
29+
entry: DevlogEntry;
30+
relevance: 'direct-text-match' | 'same-type' | 'keyword-in-notes';
31+
matchedTerms: string[];
32+
}
33+
34+
export interface DiscoveryResult {
35+
relatedEntries: DiscoveredDevlogEntry[];
36+
activeCount: number;
37+
recommendation: string;
38+
searchParameters: DiscoverDevlogsRequest;
39+
}
40+
2041
import { StorageProvider, StorageConfig, StorageProviderFactory } from "./storage/storage-provider.js";
2142
import { ConfigurationManager } from "./configuration-manager.js";
2243

@@ -274,6 +295,121 @@ export class DevlogManager {
274295
}
275296
}
276297

298+
/**
299+
* Comprehensively discover related devlog entries before creating new work
300+
* Prevents duplicate work by finding relevant historical context
301+
*/
302+
async discoverRelatedDevlogs(request: DiscoverDevlogsRequest): Promise<DiscoveryResult> {
303+
await this.ensureInitialized();
304+
305+
const { workDescription, workType, keywords = [], scope } = request;
306+
307+
// Build comprehensive search terms
308+
const searchTerms = [
309+
workDescription,
310+
workType,
311+
scope,
312+
...keywords
313+
].filter(Boolean);
314+
315+
// Get all entries for analysis
316+
const allEntries = await this.listDevlogs();
317+
const relatedEntries: DiscoveredDevlogEntry[] = [];
318+
319+
// 1. Direct text matching in title/description/context
320+
for (const entry of allEntries) {
321+
const entryText = `${entry.title} ${entry.description} ${entry.context.businessContext || ''} ${entry.context.technicalContext || ''}`.toLowerCase();
322+
const matchedTerms = searchTerms.filter((term): term is string =>
323+
term !== undefined && entryText.includes(term.toLowerCase())
324+
);
325+
326+
if (matchedTerms.length > 0) {
327+
relatedEntries.push({
328+
entry,
329+
relevance: 'direct-text-match',
330+
matchedTerms
331+
});
332+
}
333+
}
334+
335+
// 2. Same type entries (if not already included)
336+
const sameTypeEntries = allEntries.filter(entry =>
337+
entry.type === workType &&
338+
!relatedEntries.some(r => r.entry.id === entry.id)
339+
);
340+
341+
for (const entry of sameTypeEntries) {
342+
relatedEntries.push({
343+
entry,
344+
relevance: 'same-type',
345+
matchedTerms: [workType]
346+
});
347+
}
348+
349+
// 3. Keyword matching in notes and decisions
350+
for (const entry of allEntries) {
351+
if (relatedEntries.some(r => r.entry.id === entry.id)) continue;
352+
353+
const noteText = entry.notes.map(n => n.content).join(' ').toLowerCase();
354+
const decisionText = entry.context.decisions.map(d => `${d.decision} ${d.rationale}`).join(' ').toLowerCase();
355+
const combinedText = `${noteText} ${decisionText}`;
356+
357+
const matchedKeywords = keywords.filter((keyword): keyword is string =>
358+
keyword !== undefined && combinedText.includes(keyword.toLowerCase())
359+
);
360+
361+
if (matchedKeywords.length > 0) {
362+
relatedEntries.push({
363+
entry,
364+
relevance: 'keyword-in-notes',
365+
matchedTerms: matchedKeywords
366+
});
367+
}
368+
}
369+
370+
// Sort by relevance and status priority
371+
relatedEntries.sort((a, b) => {
372+
type RelevanceType = 'direct-text-match' | 'same-type' | 'keyword-in-notes';
373+
374+
const relevanceOrder: Record<RelevanceType, number> = {
375+
'direct-text-match': 0,
376+
'same-type': 1,
377+
'keyword-in-notes': 2
378+
};
379+
const statusOrder: Record<DevlogStatus, number> = {
380+
'in-progress': 0,
381+
'review': 1,
382+
'todo': 2,
383+
'testing': 3,
384+
'done': 4,
385+
'archived': 5
386+
};
387+
388+
const relevanceDiff = relevanceOrder[a.relevance as RelevanceType] - relevanceOrder[b.relevance as RelevanceType];
389+
if (relevanceDiff !== 0) return relevanceDiff;
390+
391+
return statusOrder[a.entry.status] - statusOrder[b.entry.status];
392+
});
393+
394+
// Calculate active entries and generate recommendation
395+
const activeCount = relatedEntries.filter(r =>
396+
['todo', 'in-progress', 'review', 'testing'].includes(r.entry.status)
397+
).length;
398+
399+
const recommendation = activeCount > 0
400+
? `⚠️ RECOMMENDATION: Review ${activeCount} active related entries before creating new work. Consider updating existing entries or coordinating efforts.`
401+
: relatedEntries.length > 0
402+
? `✅ RECOMMENDATION: Related entries are completed. Safe to create new devlog entry, but review completed work for insights and patterns.`
403+
: `✅ RECOMMENDATION: No related work found. Safe to create new devlog entry.`;
404+
405+
return {
406+
relatedEntries,
407+
activeCount,
408+
recommendation,
409+
searchParameters: request
410+
};
411+
}
412+
277413
// Private methods
278414

279415
private async ensureInitialized(): Promise<void> {

packages/core/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export { DevlogManager, type DevlogManagerOptions } from "./devlog-manager.js";
1+
export {
2+
DevlogManager,
3+
type DevlogManagerOptions,
4+
type DiscoverDevlogsRequest,
5+
type DiscoveredDevlogEntry,
6+
type DiscoveryResult
7+
} from "./devlog-manager.js";
28
export { ConfigurationManager, type DevlogConfig } from "./configuration-manager.js";
39

410
// Storage Providers

packages/mcp/src/index.ts

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -94,55 +94,31 @@ const tools: Tool[] = [
9494
},
9595
},
9696
{
97-
name: "find_or_create_devlog",
98-
description: "Find an existing devlog entry by title and type or create a new one if it doesn't exist",
97+
name: "discover_related_devlogs",
98+
description: "Comprehensively search for existing devlog entries related to planned work before creating new entries. Returns detailed analysis of relevant historical context to prevent duplicate work.",
9999
inputSchema: {
100100
type: "object",
101101
properties: {
102-
title: {
102+
workDescription: {
103103
type: "string",
104-
description: "Title of the task/feature/bugfix",
104+
description: "Detailed description of the work you plan to do",
105105
},
106-
type: {
106+
workType: {
107107
type: "string",
108108
enum: ["feature", "bugfix", "task", "refactor", "docs"],
109-
description: "Type of work being done",
110-
},
111-
description: {
112-
type: "string",
113-
description: "Detailed description of the work",
114-
},
115-
priority: {
116-
type: "string",
117-
enum: ["low", "medium", "high", "critical"],
118-
default: "medium",
119-
description: "Priority level",
120-
},
121-
businessContext: {
122-
type: "string",
123-
description: "Business context - why this work matters and what problem it solves",
109+
description: "Type of work being planned",
124110
},
125-
technicalContext: {
126-
type: "string",
127-
description: "Technical context - architecture decisions, constraints, assumptions",
128-
},
129-
acceptanceCriteria: {
111+
keywords: {
130112
type: "array",
131113
items: { type: "string" },
132-
description: "Acceptance criteria or definition of done",
114+
description: "Key terms, technologies, components, or concepts involved in the work",
133115
},
134-
initialInsights: {
135-
type: "array",
136-
items: { type: "string" },
137-
description: "Initial insights or knowledge about this work",
138-
},
139-
relatedPatterns: {
140-
type: "array",
141-
items: { type: "string" },
142-
description: "Related patterns or examples from other projects",
116+
scope: {
117+
type: "string",
118+
description: "Scope or area of the codebase this work affects",
143119
},
144120
},
145-
required: ["title", "type", "description"],
121+
required: ["workDescription", "workType"],
146122
},
147123
},
148124
{
@@ -390,8 +366,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
390366
case "create_devlog":
391367
return await adapter.createDevlog(args as any);
392368

393-
case "find_or_create_devlog":
394-
return await adapter.findOrCreateDevlog(args as any);
369+
case "discover_related_devlogs":
370+
return await adapter.discoverRelatedDevlogs(args as any);
395371

396372
case "update_devlog":
397373
return await adapter.updateDevlog(args as any);

packages/mcp/src/mcp-adapter.ts

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as crypto from "crypto";
66
import { DevlogManager, ConfigurationManager, type DevlogConfig } from "@devlog/core";
7-
import { DevlogEntry, CreateDevlogRequest, UpdateDevlogRequest } from "@devlog/types";
7+
import { DevlogEntry, DevlogStatus, DevlogType, CreateDevlogRequest, UpdateDevlogRequest } from "@devlog/types";
88
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
99

1010
export class MCPDevlogAdapter {
@@ -45,23 +45,6 @@ export class MCPDevlogAdapter {
4545
};
4646
}
4747

48-
async findOrCreateDevlog(args: CreateDevlogRequest): Promise<CallToolResult> {
49-
await this.ensureInitialized();
50-
51-
const entry = await this.devlogManager.findOrCreateDevlog(args);
52-
53-
const statusText = entry.createdAt === entry.updatedAt ? "Created" : "Found existing";
54-
55-
return {
56-
content: [
57-
{
58-
type: "text",
59-
text: `${statusText} devlog entry: ${entry.id}\nTitle: ${entry.title}\nType: ${entry.type}\nPriority: ${entry.priority}\nStatus: ${entry.status}\n\nBusiness Context: ${entry.context.businessContext}\nTechnical Context: ${entry.context.technicalContext}`,
60-
},
61-
],
62-
};
63-
}
64-
6548
async updateDevlog(args: UpdateDevlogRequest): Promise<CallToolResult> {
6649
await this.ensureInitialized();
6750

@@ -371,4 +354,64 @@ export class MCPDevlogAdapter {
371354
await this.initialize();
372355
}
373356
}
357+
358+
async discoverRelatedDevlogs(args: {
359+
workDescription: string;
360+
workType: DevlogType;
361+
keywords?: string[];
362+
scope?: string;
363+
}): Promise<CallToolResult> {
364+
await this.ensureInitialized();
365+
366+
const discoveryResult = await this.devlogManager.discoverRelatedDevlogs(args);
367+
368+
if (discoveryResult.relatedEntries.length === 0) {
369+
return {
370+
content: [
371+
{
372+
type: "text",
373+
text: `No related devlog entries found for:\n` +
374+
`Work: ${args.workDescription}\n` +
375+
`Type: ${args.workType}\n` +
376+
`Keywords: ${args.keywords?.join(', ') || 'None'}\n` +
377+
`Scope: ${args.scope || 'N/A'}\n\n` +
378+
`✅ Safe to create a new devlog entry - no overlapping work detected.`,
379+
},
380+
],
381+
};
382+
}
383+
384+
// Generate detailed analysis
385+
const analysis = discoveryResult.relatedEntries.slice(0, 10).map(({ entry, relevance, matchedTerms }) => {
386+
const statusEmoji: Record<DevlogStatus, string> = {
387+
'todo': '📋',
388+
'in-progress': '🔄',
389+
'review': '👀',
390+
'testing': '🧪',
391+
'done': '✅',
392+
'archived': '📦'
393+
};
394+
395+
return `${statusEmoji[entry.status]} **${entry.title}** (${entry.type})\n` +
396+
` ID: ${entry.id}\n` +
397+
` Status: ${entry.status} | Priority: ${entry.priority}\n` +
398+
` Relevance: ${relevance} (matched: ${matchedTerms.join(', ')})\n` +
399+
` Description: ${entry.description.substring(0, 150)}${entry.description.length > 150 ? '...' : ''}\n` +
400+
` Last Updated: ${new Date(entry.updatedAt).toLocaleDateString()}\n`;
401+
}).join('\n');
402+
403+
return {
404+
content: [
405+
{
406+
type: "text",
407+
text: `## Discovery Analysis for: "${args.workDescription}"\n\n` +
408+
`**Search Parameters:**\n` +
409+
`- Type: ${args.workType}\n` +
410+
`- Keywords: ${args.keywords?.join(', ') || 'None'}\n` +
411+
`- Scope: ${args.scope || 'Not specified'}\n\n` +
412+
`**Found ${discoveryResult.relatedEntries.length} related entries:**\n\n${analysis}\n\n${discoveryResult.recommendation}`,
413+
},
414+
],
415+
};
416+
}
374417
}

0 commit comments

Comments
 (0)