143 lines
3.9 KiB
TypeScript
143 lines
3.9 KiB
TypeScript
/**
|
|
* @fileoverview ID generation utilities for Task Master
|
|
* Provides functions to generate unique identifiers for tasks and subtasks
|
|
*/
|
|
|
|
import { randomBytes } from 'node:crypto';
|
|
|
|
/**
|
|
* Generates a unique task ID using the format: TASK-{timestamp}-{random}
|
|
*
|
|
* @returns A unique task ID string
|
|
* @example
|
|
* ```typescript
|
|
* const taskId = generateTaskId();
|
|
* // Returns something like: "TASK-1704067200000-A7B3"
|
|
* ```
|
|
*/
|
|
export function generateTaskId(): string {
|
|
const timestamp = Date.now();
|
|
const random = generateRandomString(4);
|
|
return `TASK-${timestamp}-${random}`;
|
|
}
|
|
|
|
/**
|
|
* Generates a subtask ID using the format: {parentId}.{sequential}
|
|
*
|
|
* @param parentId - The ID of the parent task
|
|
* @param existingSubtasks - Array of existing subtask IDs to determine the next sequential number
|
|
* @returns A unique subtask ID string
|
|
* @example
|
|
* ```typescript
|
|
* const subtaskId = generateSubtaskId("TASK-123-A7B3", ["TASK-123-A7B3.1"]);
|
|
* // Returns: "TASK-123-A7B3.2"
|
|
* ```
|
|
*/
|
|
export function generateSubtaskId(
|
|
parentId: string,
|
|
existingSubtasks: string[] = []
|
|
): string {
|
|
// Find existing subtasks for this parent
|
|
const parentSubtasks = existingSubtasks.filter((id) =>
|
|
id.startsWith(`${parentId}.`)
|
|
);
|
|
|
|
// Extract sequential numbers and find the highest
|
|
const sequentialNumbers = parentSubtasks
|
|
.map((id) => {
|
|
const parts = id.split('.');
|
|
const lastPart = parts[parts.length - 1];
|
|
return Number.parseInt(lastPart, 10);
|
|
})
|
|
.filter((num) => !Number.isNaN(num))
|
|
.sort((a, b) => a - b);
|
|
|
|
// Determine the next sequential number
|
|
const nextSequential =
|
|
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
|
|
|
|
return `${parentId}.${nextSequential}`;
|
|
}
|
|
|
|
/**
|
|
* Generates a random alphanumeric string of specified length
|
|
* Uses crypto.randomBytes for cryptographically secure randomness
|
|
*
|
|
* @param length - The desired length of the random string
|
|
* @returns A random alphanumeric string
|
|
* @internal
|
|
*/
|
|
function generateRandomString(length: number): string {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
const bytes = randomBytes(length);
|
|
let result = '';
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
result += chars[bytes[i] % chars.length];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Validates a task ID format
|
|
*
|
|
* @param id - The ID to validate
|
|
* @returns True if the ID matches the expected task ID format
|
|
* @example
|
|
* ```typescript
|
|
* isValidTaskId("TASK-1704067200000-A7B3"); // true
|
|
* isValidTaskId("invalid-id"); // false
|
|
* ```
|
|
*/
|
|
export function isValidTaskId(id: string): boolean {
|
|
const taskIdRegex = /^TASK-\d{13}-[A-Z0-9]{4}$/;
|
|
return taskIdRegex.test(id);
|
|
}
|
|
|
|
/**
|
|
* Validates a subtask ID format
|
|
*
|
|
* @param id - The ID to validate
|
|
* @returns True if the ID matches the expected subtask ID format
|
|
* @example
|
|
* ```typescript
|
|
* isValidSubtaskId("TASK-1704067200000-A7B3.1"); // true
|
|
* isValidSubtaskId("TASK-1704067200000-A7B3.1.2"); // true (nested subtask)
|
|
* isValidSubtaskId("invalid.id"); // false
|
|
* ```
|
|
*/
|
|
export function isValidSubtaskId(id: string): boolean {
|
|
const parts = id.split('.');
|
|
if (parts.length < 2) return false;
|
|
|
|
// First part should be a valid task ID
|
|
const taskIdPart = parts[0];
|
|
if (!isValidTaskId(taskIdPart)) return false;
|
|
|
|
// Remaining parts should be positive integers
|
|
const sequentialParts = parts.slice(1);
|
|
return sequentialParts.every((part) => {
|
|
const num = Number.parseInt(part, 10);
|
|
return !Number.isNaN(num) && num > 0 && part === num.toString();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extracts the parent task ID from a subtask ID
|
|
*
|
|
* @param subtaskId - The subtask ID
|
|
* @returns The parent task ID, or null if the input is not a valid subtask ID
|
|
* @example
|
|
* ```typescript
|
|
* getParentTaskId("TASK-1704067200000-A7B3.1.2"); // "TASK-1704067200000-A7B3"
|
|
* getParentTaskId("TASK-1704067200000-A7B3"); // null (not a subtask)
|
|
* ```
|
|
*/
|
|
export function getParentTaskId(subtaskId: string): string | null {
|
|
if (!isValidSubtaskId(subtaskId)) return null;
|
|
|
|
const parts = subtaskId.split('.');
|
|
return parts[0];
|
|
}
|