Skip to content

Commit e9ae80e

Browse files
author
Marvin Zhang
committed
feat: Enhance devlog management with integer ID system and semantic key generation
1 parent 7d2d2e7 commit e9ae80e

File tree

9 files changed

+93
-75
lines changed

9 files changed

+93
-75
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ build/
141141

142142
# Devlog directory - contains local development notes
143143
.devlog/
144+
.devlog*
144145

145146
# Devlog configuration with credentials
146147
devlog.config.json

devlog.config.json.backup

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/core/src/devlog-manager.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,14 @@ export class DevlogManager {
124124
return existing;
125125
}
126126

127+
// Generate semantic key for reference
128+
const key = this.generateKey(request.title, request.type);
129+
127130
// Create new entry
128131
const now = new Date().toISOString();
129132
const entry: DevlogEntry = {
130133
id,
134+
key,
131135
title: request.title,
132136
type: request.type,
133137
description: request.description,
@@ -474,9 +478,7 @@ export class DevlogManager {
474478
*/
475479
async syncDevlog(id: DevlogId): Promise<DevlogEntry | null> {
476480
await this.ensureInitialized();
477-
// Convert ID to string for integration service compatibility
478-
const idStr = IdManager.idToString(id);
479-
return await this.integrationService.syncEntry(idStr);
481+
return await this.integrationService.syncEntry(id);
480482
}
481483

482484
/**
@@ -513,17 +515,29 @@ export class DevlogManager {
513515
// Use integer ID system
514516
return await this.idManager.generateNextId();
515517
} else {
516-
// Fall back to legacy hash-based system
517-
const slug = title
518-
.toLowerCase()
519-
.replace(/[^a-z0-9]+/g, "-")
520-
.replace(/^-+|-+$/g, "")
521-
.substring(0, 50);
522-
523-
// Create hash from title and type for consistency
518+
// Legacy system no longer supported - always use integers
519+
throw new Error("Legacy string IDs are no longer supported. Use integer ID system.");
520+
}
521+
}
522+
523+
/**
524+
* Generate semantic key for the entry (used for referencing and legacy compatibility)
525+
*/
526+
private generateKey(title: string, type?: DevlogType): string {
527+
const slug = title
528+
.toLowerCase()
529+
.replace(/[^a-z0-9]+/g, "-")
530+
.replace(/^-+|-+$/g, "")
531+
.substring(0, 50);
532+
533+
// For consistency with legacy system, optionally include type-based hash
534+
if (type && this.useIntegerIds) {
535+
// Clean key without hash for new system
536+
return slug;
537+
} else {
538+
// Legacy format with hash
524539
const content = type ? `${title}:${type}` : title;
525540
const hash = crypto.createHash("sha256").update(content).digest("hex").substring(0, 8);
526-
527541
return `${slug}--${hash}`;
528542
}
529543
}

packages/core/src/integration-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Integration service that handles synchronization between local storage and external systems
33
*/
44

5-
import { DevlogEntry, EnterpriseIntegration, ExternalReference } from "@devlog/types";
5+
import { DevlogEntry, EnterpriseIntegration, ExternalReference, DevlogId } from "@devlog/types";
66
import { StorageProvider } from "./storage/storage-provider.js";
77
import { EnterpriseSync } from "./integrations/enterprise-sync.js";
88
import { SyncStrategy } from "./configuration-manager.js";
@@ -86,7 +86,7 @@ export class IntegrationService {
8686
/**
8787
* Manually sync an entry to external systems
8888
*/
89-
async syncEntry(entryId: string): Promise<DevlogEntry | null> {
89+
async syncEntry(entryId: DevlogId): Promise<DevlogEntry | null> {
9090
const entry = await this.storage.get(entryId);
9191
if (!entry || !this.enterpriseSync) {
9292
return entry;

packages/core/src/utils/devlog-utils.ts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,46 @@
11
import * as crypto from "crypto";
2-
import { DevlogType, DevlogEntry, DevlogFilter, DevlogNote } from "@devlog/types";
2+
import { DevlogType, DevlogEntry, DevlogFilter, DevlogNote, DevlogId } from "@devlog/types";
33

44
export class DevlogUtils {
5-
static generateId(title: string, type?: DevlogType): string {
5+
/**
6+
* Generate semantic key for devlog entry (used for the key field)
7+
*/
8+
static generateKey(title: string, type?: DevlogType): string {
69
// Create a clean slug from the title
710
const slug = title
811
.toLowerCase()
912
.replace(/[^a-z0-9]+/g, "-")
1013
.replace(/^-+|-+$/g, "")
1114
.substring(0, 50); // Limit length
1215

13-
// Add type prefix if provided
14-
const prefix = type ? `${type}-` : "";
15-
16-
// Create a hash of the full input for uniqueness
17-
const hash = crypto.createHash('md5')
18-
.update(`${type || 'unknown'}-${title}`)
19-
.digest('hex')
20-
.substring(0, 8);
21-
22-
return `${prefix}${slug}-${hash}`;
16+
return slug;
17+
}
18+
19+
/**
20+
* Legacy method for backward compatibility - now generates keys instead of IDs
21+
* @deprecated Use generateKey instead
22+
*/
23+
static generateId(title: string, type?: DevlogType): string {
24+
return DevlogUtils.generateKey(title, type);
2325
}
2426

27+
/**
28+
* Generate unique key (deprecated - integer IDs handle uniqueness automatically)
29+
* @deprecated No longer needed with integer ID system
30+
*/
2531
static async generateUniqueId(
2632
title: string,
2733
type: DevlogType | undefined,
2834
checkExisting: (id: string) => Promise<DevlogEntry | null>
2935
): Promise<string> {
30-
const baseId = DevlogUtils.generateId(title, type);
31-
const existing = await checkExisting(baseId);
32-
if (!existing) {
33-
return baseId;
34-
}
35-
36-
// If it exists, add a counter suffix
37-
let counter = 1;
38-
let uniqueId: string;
39-
40-
do {
41-
uniqueId = `${baseId}-${counter}`;
42-
counter++;
43-
} while (await checkExisting(uniqueId) && counter < 100); // Prevent infinite loop
44-
45-
// Fallback to timestamp if we can't find a unique ID
46-
return counter >= 100 ? `${baseId}-${Date.now()}` : uniqueId;
36+
return DevlogUtils.generateKey(title, type);
4737
}
4838

4939
static async checkForDuplicateTitle(
5040
title: string,
5141
type: DevlogType | undefined,
5242
getAllEntries: () => Promise<DevlogEntry[]>,
53-
excludeId?: string
43+
excludeId?: DevlogId
5444
): Promise<DevlogEntry | null> {
5545
const entries = await getAllEntries();
5646
const normalizedTitle = title.toLowerCase().trim();

packages/core/src/utils/id-manager.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ export class IdManager {
2222
}
2323

2424
/**
25-
* Convert string ID back to appropriate type (detect if it's a number)
25+
* Convert string ID back to integer (since all IDs are now integers)
2626
*/
2727
static stringToId(idStr: string): DevlogId {
28-
// Check if it's a pure integer
2928
const parsed = parseInt(idStr, 10);
30-
if (!isNaN(parsed) && String(parsed) === idStr) {
31-
return parsed;
29+
if (isNaN(parsed)) {
30+
throw new Error(`Invalid integer ID: ${idStr}`);
3231
}
33-
return idStr;
32+
return parsed;
3433
}
3534

3635
/**

packages/mcp/src/ai-context-demo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ export class AIContextManager {
2424
}): Promise<DevlogEntry> {
2525

2626
const now = new Date().toISOString();
27-
const id = `task-${Date.now()}`;
27+
// Generate integer ID using timestamp-based approach
28+
const id = parseInt(Date.now().toString().slice(-8), 10);
2829

2930
const entry: DevlogEntry = {
3031
id,
32+
key: `task-${Date.now()}`, // Use string for the key field
3133
title: taskData.title,
3234
type: "feature",
3335
description: taskData.description,

packages/mcp/src/mcp-adapter.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ export class MCPDevlogAdapter {
1717
this.devlogManager = new DevlogManager();
1818
}
1919

20+
// Helper function to parse string ID to number
21+
private parseId(idStr: string): number {
22+
const id = parseInt(idStr, 10);
23+
if (isNaN(id)) {
24+
throw new Error(`Invalid devlog ID "${idStr}". Must be a number.`);
25+
}
26+
return id;
27+
}
28+
2029
/**
2130
* Initialize the adapter with appropriate storage configuration
2231
*/
@@ -63,7 +72,8 @@ export class MCPDevlogAdapter {
6372
async getDevlog(args: { id: string }): Promise<CallToolResult> {
6473
await this.ensureInitialized();
6574

66-
const entry = await this.devlogManager.getDevlog(args.id);
75+
const id = this.parseId(args.id);
76+
const entry = await this.devlogManager.getDevlog(id);
6777

6878
if (!entry) {
6979
return {
@@ -156,8 +166,9 @@ export class MCPDevlogAdapter {
156166
async addDevlogNote(args: { id: string; note: string; category?: string }): Promise<CallToolResult> {
157167
await this.ensureInitialized();
158168

169+
const id = this.parseId(args.id);
159170
const category = args.category as any || "progress";
160-
const entry = await this.devlogManager.addNote(args.id, args.note, category);
171+
const entry = await this.devlogManager.addNote(id, args.note, category);
161172

162173
return {
163174
content: [
@@ -172,7 +183,8 @@ export class MCPDevlogAdapter {
172183
async addDecision(args: { id: string; decision: string; rationale: string; decisionMaker: string; alternatives?: string[] }): Promise<CallToolResult> {
173184
await this.ensureInitialized();
174185

175-
const entry = await this.devlogManager.getDevlog(args.id);
186+
const id = this.parseId(args.id);
187+
const entry = await this.devlogManager.getDevlog(id);
176188
if (!entry) {
177189
return {
178190
content: [
@@ -198,7 +210,7 @@ export class MCPDevlogAdapter {
198210

199211
// Update the entry to trigger save
200212
const updated = await this.devlogManager.updateDevlog({
201-
id: args.id,
213+
id: id,
202214
// Use a field that exists in UpdateDevlogRequest to trigger save
203215
tags: entry.tags
204216
});
@@ -216,7 +228,8 @@ export class MCPDevlogAdapter {
216228
async completeDevlog(args: { id: string; summary?: string }): Promise<CallToolResult> {
217229
await this.ensureInitialized();
218230

219-
const entry = await this.devlogManager.completeDevlog(args.id, args.summary);
231+
const id = this.parseId(args.id);
232+
const entry = await this.devlogManager.completeDevlog(id, args.summary);
220233

221234
return {
222235
content: [
@@ -271,7 +284,8 @@ export class MCPDevlogAdapter {
271284
async getContextForAI(args: { id: string }): Promise<CallToolResult> {
272285
await this.ensureInitialized();
273286

274-
const entry = await this.devlogManager.getContextForAI(args.id);
287+
const id = this.parseId(args.id);
288+
const entry = await this.devlogManager.getContextForAI(id);
275289

276290
if (!entry) {
277291
return {
@@ -320,6 +334,7 @@ export class MCPDevlogAdapter {
320334
}): Promise<CallToolResult> {
321335
await this.ensureInitialized();
322336

337+
const id = this.parseId(args.id);
323338
const contextUpdate: any = {};
324339

325340
if (args.summary) contextUpdate.currentSummary = args.summary;
@@ -331,7 +346,7 @@ export class MCPDevlogAdapter {
331346
contextUpdate.lastAIUpdate = new Date().toISOString();
332347
contextUpdate.contextVersion = (contextUpdate.contextVersion || 0) + 1;
333348

334-
const entry = await this.devlogManager.updateAIContext(args.id, contextUpdate);
349+
const entry = await this.devlogManager.updateAIContext(id, contextUpdate);
335350

336351
return {
337352
content: [

packages/web/src/server/routes/devlog-routes.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { Router } from 'express';
22
import { DevlogManager } from '@devlog/core';
33
import { CreateDevlogRequest, UpdateDevlogRequest, DevlogFilter } from '@devlog/types';
44

5+
// Utility function to parse ID from string parameter
6+
function parseDevlogId(idParam: string): number {
7+
const id = parseInt(idParam, 10);
8+
if (isNaN(id)) {
9+
throw new Error(`Invalid devlog ID: ${idParam}`);
10+
}
11+
return id;
12+
}
13+
514
export function devlogRoutes(devlogManager: DevlogManager): Router {
615
const router = Router();
716

@@ -26,7 +35,8 @@ export function devlogRoutes(devlogManager: DevlogManager): Router {
2635
// Get devlog by ID
2736
router.get('/:id', async (req, res) => {
2837
try {
29-
const devlog = await devlogManager.getDevlog(req.params.id);
38+
const id = parseDevlogId(req.params.id);
39+
const devlog = await devlogManager.getDevlog(id);
3040
if (!devlog) {
3141
return res.status(404).json({ error: 'Devlog not found' });
3242
}
@@ -64,7 +74,8 @@ export function devlogRoutes(devlogManager: DevlogManager): Router {
6474
// Delete devlog
6575
router.delete('/:id', async (req, res) => {
6676
try {
67-
await devlogManager.deleteDevlog(req.params.id);
77+
const id = parseDevlogId(req.params.id);
78+
await devlogManager.deleteDevlog(id);
6879
res.status(204).send();
6980
} catch (error) {
7081
console.error('Error deleting devlog:', error);
@@ -75,8 +86,9 @@ export function devlogRoutes(devlogManager: DevlogManager): Router {
7586
// Add note to devlog
7687
router.post('/:id/notes', async (req, res) => {
7788
try {
89+
const id = parseDevlogId(req.params.id);
7890
const { note } = req.body;
79-
const devlog = await devlogManager.addNote(req.params.id, note);
91+
const devlog = await devlogManager.addNote(id, note);
8092
res.json(devlog);
8193
} catch (error) {
8294
console.error('Error adding note:', error);

0 commit comments

Comments
 (0)