From d28cf29598247a1cee398f1eeb923aa1efa90e07 Mon Sep 17 00:00:00 2001 From: Brad Groux Date: Tue, 27 Jan 2026 22:30:53 -0600 Subject: [PATCH] fix: infinite recursion in project/tag service init, ID mismatch in seed migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProjectService.init() had infinite recursion: list() → init() → seed → list() → init()... - TagService seed checked file existence (always true after super.init creates it) - Seed migrations now use seedItem() with raw project/tag strings as IDs so they match existing task.project and task.tags values - Added seedItem() to ManagedListService base for custom-ID seeding - Cleaned up task-types.json (removed duplicate, restored Research) --- .veritas-kanban/projects.json | 35 ++++++++++++++++++++- .veritas-kanban/tags.json | 11 ++++++- .veritas-kanban/task-types.json | 17 +++++----- server/src/services/managed-list-service.ts | 10 ++++++ server/src/services/project-service.ts | 20 ++++++------ server/src/services/tag-service.ts | 23 ++++++-------- 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/.veritas-kanban/projects.json b/.veritas-kanban/projects.json index 0637a088..85572338 100644 --- a/.veritas-kanban/projects.json +++ b/.veritas-kanban/projects.json @@ -1 +1,34 @@ -[] \ No newline at end of file +[ + { + "id": "Sprint 8", + "label": "Sprint 8", + "color": "bg-blue-500/20", + "order": 0, + "created": "2026-01-28T04:30:34.608Z", + "updated": "2026-01-28T04:30:34.608Z" + }, + { + "id": "brain", + "label": "brain", + "color": "bg-green-500/20", + "order": 1, + "created": "2026-01-28T04:30:34.608Z", + "updated": "2026-01-28T04:30:34.608Z" + }, + { + "id": "platform", + "label": "platform", + "color": "bg-purple-500/20", + "order": 2, + "created": "2026-01-28T04:30:34.608Z", + "updated": "2026-01-28T04:30:34.608Z" + }, + { + "id": "veritas-kanban", + "label": "veritas-kanban", + "color": "bg-orange-500/20", + "order": 3, + "created": "2026-01-28T04:30:34.608Z", + "updated": "2026-01-28T04:30:34.608Z" + } +] \ No newline at end of file diff --git a/.veritas-kanban/tags.json b/.veritas-kanban/tags.json index 0637a088..00e24d77 100644 --- a/.veritas-kanban/tags.json +++ b/.veritas-kanban/tags.json @@ -1 +1,10 @@ -[] \ No newline at end of file +[ + { + "id": "feature", + "label": "feature", + "color": "bg-blue-500/20 text-blue-400", + "order": 0, + "created": "2026-01-28T04:30:34.607Z", + "updated": "2026-01-28T04:30:34.607Z" + } +] \ No newline at end of file diff --git a/.veritas-kanban/task-types.json b/.veritas-kanban/task-types.json index 82eac87d..3f59d7ec 100644 --- a/.veritas-kanban/task-types.json +++ b/.veritas-kanban/task-types.json @@ -4,21 +4,20 @@ "label": "Code", "icon": "Code", "color": "border-l-violet-500", - "order": 0, + "order": 1, "isDefault": true, "created": "2026-01-28T04:03:21.782Z", - "updated": "2026-01-28T04:03:21.782Z" + "updated": "2026-01-28T04:27:49.587Z" }, { "id": "research", "label": "Research", - "icon": "Search", + "icon": "BookOpen", "color": "border-l-cyan-500", - "order": 1, + "order": 3, "isDefault": true, "created": "2026-01-28T04:03:21.782Z", - "updated": "2026-01-28T04:10:37.498Z", - "isHidden": true + "updated": "2026-01-28T04:27:49.587Z" }, { "id": "content", @@ -28,16 +27,16 @@ "order": 2, "isDefault": true, "created": "2026-01-28T04:03:21.782Z", - "updated": "2026-01-28T04:03:21.782Z" + "updated": "2026-01-28T04:27:49.587Z" }, { "id": "automation", "label": "Automation", "icon": "Zap", "color": "border-l-emerald-500", - "order": 3, + "order": 0, "isDefault": true, "created": "2026-01-28T04:03:21.782Z", - "updated": "2026-01-28T04:03:21.782Z" + "updated": "2026-01-28T04:27:49.587Z" } ] \ No newline at end of file diff --git a/server/src/services/managed-list-service.ts b/server/src/services/managed-list-service.ts index 92af1cbf..f2310c6a 100644 --- a/server/src/services/managed-list-service.ts +++ b/server/src/services/managed-list-service.ts @@ -125,6 +125,16 @@ export class ManagedListService { return newItem; } + /** + * Seed a pre-built item with a specific ID (for migrations) + * Skips ID generation — caller provides the full item + */ + async seedItem(item: T): Promise { + this.items.push(item); + await this.save(); + return item; + } + /** * Update an existing item */ diff --git a/server/src/services/project-service.ts b/server/src/services/project-service.ts index ab00c3f2..67fc5895 100644 --- a/server/src/services/project-service.ts +++ b/server/src/services/project-service.ts @@ -1,5 +1,4 @@ import { resolve } from 'path'; -import { existsSync } from 'fs'; import type { ProjectConfig } from '@veritas-kanban/shared'; import { ManagedListService } from './managed-list-service.js'; import { TaskService } from './task-service.js'; @@ -43,14 +42,15 @@ export class ProjectService extends ManagedListService { * Initialize service and perform seed migration if needed */ async init(): Promise { + // Prevent re-entrant init (list() calls init(), seed calls list()) + if (this.seeded) return; + this.seeded = true; + // Call parent init first await super.init(); // Seed projects from existing tasks on first run - if (!this.seeded) { - await this.seedProjectsFromTasks(); - this.seeded = true; - } + await this.seedProjectsFromTasks(); } /** @@ -82,21 +82,23 @@ export class ProjectService extends ManagedListService { }); // Create ProjectConfig entries for each unique project + // Use the raw project string as the ID for backward compatibility + // (tasks store project as a plain string that must match the project ID) const projectArray = Array.from(projectStrings).sort(); + const now = new Date().toISOString(); for (let i = 0; i < projectArray.length; i++) { const projectName = projectArray[i]; const color = PROJECT_COLORS[i % PROJECT_COLORS.length]; - // Create the project using the project name as both id and label - const now = new Date().toISOString(); - await this.create({ + await this.seedItem({ + id: projectName, // Must match existing task.project values label: projectName, color, order: i, created: now, updated: now, - } as any); + } as ProjectConfig); } console.log(`✅ Seeded ${projectArray.length} projects from existing tasks`); diff --git a/server/src/services/tag-service.ts b/server/src/services/tag-service.ts index 0abdba33..4ef09716 100644 --- a/server/src/services/tag-service.ts +++ b/server/src/services/tag-service.ts @@ -1,5 +1,4 @@ import { resolve } from 'path'; -import { existsSync } from 'fs'; import type { TagConfig } from '@veritas-kanban/shared'; import { ManagedListService } from './managed-list-service.js'; import { TaskService } from './task-service.js'; @@ -42,16 +41,15 @@ export class TagService extends ManagedListService { * Initialize and seed tags from existing tasks if first run */ async init(): Promise { - await super.init(); - - // Only seed once + // Prevent re-entrant init (list() calls init(), seed calls list()) if (this.seeded) return; this.seeded = true; - const filePath = resolve(process.cwd(), '..', '.veritas-kanban', 'tags.json'); + await super.init(); - // If file doesn't exist, we need to seed from existing tasks - if (!existsSync(filePath)) { + // Seed from existing tasks if no tags exist yet + const existing = await this.list(true); + if (existing.length === 0) { await this.seedFromExistingTasks(); } } @@ -75,22 +73,19 @@ export class TagService extends ManagedListService { // Create TagConfig entries for each unique tag const tagArray = Array.from(uniqueTags).sort(); + const now = new Date().toISOString(); for (let i = 0; i < tagArray.length; i++) { const tagString = tagArray[i]; const color = TAG_COLORS[i % TAG_COLORS.length]; - const now = new Date().toISOString(); - const newTag: TagConfig = { - id: tagString, // Use tag string as ID for backward compatibility + await this.seedItem({ + id: tagString, // Must match existing task.tags values label: tagString, color, order: i, created: now, updated: now, - }; - - // Add to items array directly since we're seeding - await this.create({ label: tagString, color } as any); + } as TagConfig); } console.log(`Seeded ${tagArray.length} tags from existing tasks`);