fix: infinite recursion in project/tag service init, ID mismatch in seed migration

- 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)
This commit is contained in:
Brad Groux 2026-01-27 22:30:53 -06:00
parent 93345980d6
commit d28cf29598
6 changed files with 82 additions and 34 deletions

View file

@ -1 +1,34 @@
[]
[
{
"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"
}
]

View file

@ -1 +1,10 @@
[]
[
{
"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"
}
]

View file

@ -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"
}
]

View file

@ -125,6 +125,16 @@ export class ManagedListService<T extends ManagedListItem> {
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<T> {
this.items.push(item);
await this.save();
return item;
}
/**
* Update an existing item
*/

View file

@ -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<ProjectConfig> {
* Initialize service and perform seed migration if needed
*/
async init(): Promise<void> {
// 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<ProjectConfig> {
});
// 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`);

View file

@ -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<TagConfig> {
* Initialize and seed tags from existing tasks if first run
*/
async init(): Promise<void> {
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<TagConfig> {
// 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`);