Skip to content

fix: normalize task ID types in tasks.json writes#1670

Open
withsivram wants to merge 2 commits intoeyaltoledano:mainfrom
withsivram:fix/inconsistent-task-id-quoting-issue-1583
Open

fix: normalize task ID types in tasks.json writes#1670
withsivram wants to merge 2 commits intoeyaltoledano:mainfrom
withsivram:fix/inconsistent-task-id-quoting-issue-1583

Conversation

@withsivram
Copy link
Copy Markdown

@withsivram withsivram commented Mar 22, 2026

Summary

  • Fixes inconsistent task ID types in tasks.json where set-task-status (CLI path) wrote numeric IDs while update-task (core file-storage path) wrote string IDs
  • Changed file-storage.ts and format-handler.ts normalization from String() to Number() for task IDs, subtask parent IDs
  • Updated TypeScript types (Task.id, Subtask.parentId, PlaceholderTask.id, UpdateTask.id) from string to number | string to support both file storage (numeric) and API storage (string display IDs)
  • Updated test fixture createTask helper to use Number() instead of String() for consistency

Test plan

  • TypeScript type checker passes (npm run turbo:typecheck)
  • All 923 existing tests pass (npm run test -w packages/tm-core)
  • Manual: Run set-task-status then update-task on same task, verify tasks.json has consistent numeric IDs
  • Manual: Verify subtask parent IDs are also numeric after both operations

Fixes #1583

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automatic renumbering when moving tasks between tags with ID conflicts, replacing previous failures.
    • Enhanced response messages include renumbering details (original → new ID mappings).
    • Task identifiers now support both numeric and string formats.
  • Bug Fixes

    • Improved dependency integrity handling during cross-tag task movements with renumbering.

withsivram and others added 2 commits March 20, 2026 12:47
…ixes eyaltoledano#1647)

When moving tasks between tags, if a task's ID already exists in the
target tag, the task is now automatically renumbered to the next
available ID instead of throwing an error. Dependencies within the
moved batch are updated to reflect the new IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…#1583)

The file-storage adapter and format-handler were converting task IDs to
strings via String(), while the CLI scripts (utils.js) normalized them
to numbers via parseInt(). This caused inconsistent ID types in
tasks.json depending on which command last updated a task.

Changes:
- file-storage.ts normalizeTaskIds: String(task.id) -> Number(task.id)
- file-storage.ts updateTask: String(taskId) -> Number(taskId)
- format-handler.ts normalizeTasks: String(task.id) -> Number(task.id)
- Both: String(subtask.parentId) -> Number(subtask.parentId)
- TypeScript types: Task.id, Subtask.parentId, PlaceholderTask.id,
  UpdateTask.id updated from string to number | string
- task-fixtures.ts: String(overrides.id) -> Number(overrides.id)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 22, 2026 06:53
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 22, 2026

⚠️ No Changeset found

Latest commit: 3bfaeaf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

This PR implements auto-renumbering for task IDs during cross-tag moves when conflicts occur. It normalizes task identifiers as numeric values in storage, updates type definitions to support both numeric and string IDs, and enhances the move-task logic to track renumbering via ID mapping while updating internal dependencies accordingly.

Changes

Cohort / File(s) Summary
Type Definition Updates
packages/tm-core/src/common/types/index.ts
Updated PlaceholderTask.id, Task.id, Subtask.parentId, and UpdateTask.id to support both numeric and string types (number | string).
Storage Normalization
packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts, packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts, packages/tm-core/src/testing/task-fixtures.ts
Normalized task and subtask identifiers to numeric values (using Number() instead of String()); affects how tasks are persisted and loaded from storage.
Cross-Tag Move Logic
scripts/modules/task-manager/move-task.js, mcp-server/src/core/direct-functions/move-task-cross-tag.js
Introduced ID conflict resolution via auto-renumbering using idRemapping map; computes next available ID and updates internal subtask dependency strings; enriched success response messages with renumbering summaries (original ID → new ID).
Move-Task Tests
tests/integration/move-task-cross-tag.integration.test.js
Updated "ID conflicts in target tag" test to validate auto-renumbering behavior instead of error rejection; verifies renumbered entries and dependency persistence.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant MoveTask as Move Logic
    participant Storage
    participant FileSystem as File System

    User->>CLI: Move task across tags (ID collision)
    CLI->>MoveTask: executeMoveOperation()
    
    MoveTask->>MoveTask: Check ID collision in target tag
    alt ID Conflict
        MoveTask->>MoveTask: getNextAvailableId() → newId
        MoveTask->>MoveTask: Track in idRemapping: oldId → newId
        MoveTask->>MoveTask: Update task.id to newId
    end
    
    MoveTask->>MoveTask: Update internal subtask dependencies<br/>(parent.subId refs)
    MoveTask->>MoveTask: Remap numeric dependencies<br/>in moved task batch
    
    MoveTask->>Storage: Persist moved tasks (with newId)
    Storage->>FileSystem: writeJSON(target-tag tasks)
    
    MoveTask->>MoveTask: Build success message<br/>with renumbering summary
    MoveTask->>CLI: Return response + movedTasks<br/>(originalId, newId fields)
    CLI->>User: Display success message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • Crunchyman-ralph
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes task auto-renumbering functionality in move-task.js that extends beyond the ID normalization scope of issue #1583, though it appears related to preventing ID collisions during migrations. Clarify whether the move-task.js auto-renumbering changes are required by issue #1583 or should be addressed in a separate PR. If out-of-scope, move those changes to a dedicated PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: normalizing task ID types (from strings to numbers) in tasks.json writes, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #1583: changed String() to Number() in file-storage.ts and format-handler.ts [#1583], updated TypeScript types to number | string [#1583], updated test fixtures [#1583], and includes integration test updates [#1583].
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to make tasks.json writes consistent by normalizing task/subtask IDs to numeric form across file-storage code paths, and updates shared TypeScript types/fixtures accordingly. It also changes cross-tag move behavior to auto-renumber tasks on ID collisions and surfaces renumbering details in responses/messages.

Changes:

  • Normalize Task.id and Subtask.parentId to numbers when writing via tm-core file-storage (file-storage.ts, format-handler.ts).
  • Broaden ID-related TypeScript types to number | string and update tm-core test fixtures to align with numeric file-storage IDs.
  • Update cross-tag move logic/tests to auto-renumber on target-tag ID conflicts and include renumbering details in messages/results.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/integration/move-task-cross-tag.integration.test.js Updates expectations to assert auto-renumbering behavior on ID collisions.
scripts/modules/task-manager/move-task.js Adds auto-renumbering on cross-tag ID conflicts plus dependency remapping & messaging.
packages/tm-core/src/testing/task-fixtures.ts Updates fixture task ID coercion to numeric.
packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts Normalizes task/subtask IDs to numbers for file-storage save formatting.
packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts Normalizes task/subtask IDs to numbers on save and update operations.
packages/tm-core/src/common/types/index.ts Broadens ID types to `number
mcp-server/src/core/direct-functions/move-task-cross-tag.js Adjusts returned message to include renumber details.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +968 to +981
if (movedTask && Array.isArray(movedTask.subtasks)) {
movedTask.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
const normalizedDep = normalizeDependency(dep);
if (
Number.isFinite(normalizedDep) &&
idRemapping.has(normalizedDep)
) {
return idRemapping.get(normalizedDep);
}
return dep;
});
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The post-move remapping of subtask dependencies uses normalizeDependency(), which strips dotted refs like "5.2" down to the parent ID (5). If a moved/renumbered task’s subtasks depend on another moved task’s specific subtask (e.g., "1.2"), this code will rewrite that to a bare number (e.g., 2) and lose the subtask portion, corrupting the dependency reference. Update the remapping logic to preserve dotted dependencies by only rewriting the parent portion when the dependency is in "parent.subId" form.

Copilot uses AI. Check for mistakes.
Comment on lines +895 to +913
// Apply the new ID to the task
taskToMove.id = assignedId;

// Update internal subtask dependency references that pointed to the old parent ID
if (Array.isArray(taskToMove.subtasks)) {
taskToMove.subtasks.forEach((subtask) => {
if (Array.isArray(subtask.dependencies)) {
subtask.dependencies = subtask.dependencies.map((dep) => {
if (typeof dep === 'string' && dep.includes('.')) {
const [depParent, depSub] = dep.split('.');
if (parseInt(depParent, 10) === normalizedTaskId) {
return `${assignedId}.${depSub}`;
}
}
return dep;
});
}
});
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an ID collision is auto-renumbered, the parent task’s id is updated but subtasks’ stored parent pointer (scripts use parentTaskId) is not updated. This can leave subtasks claiming they belong to the old parent ID after the move/renumber, which will break helpers that rely on parentTaskId. When assignedId differs from the original, update each subtask.parentTaskId (if present) to assignedId as part of the renumbering step.

Copilot uses AI. Check for mistakes.
Comment on lines +883 to 893
let assignedId = normalizedTaskId;

if (existingTaskIndex !== -1) {
throw new MoveTaskError(
MOVE_ERROR_CODES.TASK_ALREADY_EXISTS,
`Task ${taskId} already exists in target tag "${targetTag}"`,
{
conflictingId: normalizedTaskId,
targetTag,
suggestions: [
'Choose a different target tag without conflicting IDs',
'Move a different set of IDs (avoid existing ones)',
'If needed, move within-tag to a new ID first, then cross-tag move'
]
}
// ID collision detected — auto-renumber to the next available ID
assignedId = getNextAvailableId(rawData, targetTag, idRemapping);
idRemapping.set(normalizedTaskId, assignedId);
log(
'info',
`Task ${normalizedTaskId} conflicts with existing ID in "${targetTag}", renumbered to ${assignedId}`
);
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR’s description is about normalizing task ID types when writing tasks.json, but this change also alters cross-tag move semantics: instead of throwing TASK_ALREADY_EXISTS on ID collisions, it silently auto-renumbers and proceeds. That’s a user-visible behavior change and should be called out explicitly in the PR description (or split into a separate PR) so reviewers/users understand the new contract.

Copilot uses AI. Check for mistakes.
Comment on lines 288 to 299
private normalizeTaskIds(tasks: Task[]): Task[] {
return tasks.map((task) => ({
...task,
id: String(task.id), // Task IDs are strings
id: Number(task.id), // Task IDs are numbers
dependencies: task.dependencies?.map((dep) => String(dep)) || [],
subtasks:
task.subtasks?.map((subtask) => ({
...subtask,
id: Number(subtask.id), // Subtask IDs are numbers
parentId: String(subtask.parentId) // Parent ID is string (Task ID)
parentId: Number(subtask.parentId) // Parent ID is number (Task ID)
})) || []
}));
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coercing IDs with Number(...) can yield NaN for unexpected inputs (e.g., non-numeric string IDs). Since Task/Subtask IDs are now typed as number|string, this can silently write invalid NaN IDs into tasks.json. Consider validating that coerced IDs are finite (and ideally positive integers) and throwing a clear error if not, rather than persisting NaN.

Copilot uses AI. Check for mistakes.
Comment on lines 403 to 408
tasks[taskIndex] = {
...existingTask,
...updates,
...(mergedSubtasks && { subtasks: mergedSubtasks }),
id: String(taskId) // Keep consistent with normalizeTaskIds
id: Number(taskId) // Keep consistent with normalizeTaskIds
};
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateTask() now forces id: Number(taskId). If taskId is ever a non-numeric string, this will persist NaN as the task ID. Please add validation (finite / integer / >0) before assigning, and surface a helpful error when the ID cannot be represented as a numeric file-storage ID.

Copilot uses AI. Check for mistakes.
Comment on lines 222 to 233
private normalizeTasks(tasks: Task[]): Task[] {
return tasks.map((task) => ({
...task,
id: String(task.id), // Task IDs are strings
id: Number(task.id), // Task IDs are numbers
dependencies: task.dependencies?.map((dep) => String(dep)) || [],
subtasks:
task.subtasks?.map((subtask) => ({
...subtask,
id: Number(subtask.id), // Subtask IDs are numbers
parentId: String(subtask.parentId) // Parent ID is string (Task ID)
parentId: Number(subtask.parentId) // Parent ID is number (Task ID)
})) || []
}));
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coercing IDs with Number(...) here can produce NaN for non-numeric string IDs, and convert "" to 0. Since Task/Subtask IDs are now number|string, it would be safer to validate (Number.isFinite, integer, >0) and throw (or otherwise handle) invalid IDs rather than writing invalid values to disk.

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 58
export function createTask(
overrides: Partial<Omit<Task, 'id'>> & { id: number | string; title: string }
): Task {
return {
id: String(overrides.id),
id: Number(overrides.id),
title: overrides.title,
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createTask fixture now coerces id with Number(...), but the doc comment immediately above still says the id is converted to a string. Update the fixture documentation to match the new numeric-ID behavior (and consider guarding against NaN in fixtures to fail fast in tests).

Copilot uses AI. Check for mistakes.
Comment on lines +120 to 137
// Check if any tasks were renumbered during the move
const renumberedTasks = (result.movedTasks || []).filter(
(t) => t.newId !== undefined
);
let message = `Successfully moved ${sourceIds.length} task(s) from "${args.sourceTag}" to "${args.targetTag}"`;
if (renumberedTasks.length > 0) {
const renumberDetails = renumberedTasks
.map((t) => `${t.originalId} → ${t.newId}`)
.join(', ');
message += `. Renumbered to avoid ID collisions: ${renumberDetails}`;
}

return {
success: true,
data: {
...result,
message: `Successfully moved ${sourceIds.length} task(s) from "${args.sourceTag}" to "${args.targetTag}"`,
message,
moveOptions,
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message count here is based on sourceIds.length, but moveTasksBetweenTags() can move more tasks than that when withDependencies=true (and it returns movedTasks). Use result.movedTasks.length (or fall back safely) so the message accurately reflects what happened.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/tm-core/src/common/types/index.ts (1)

296-329: ⚠️ Potential issue | 🟠 Major

Type guards isTask and isSubtask are now inconsistent with the widened id types.

The type definitions were updated to allow number | string for task IDs, but the type guards still check for specific types:

  • isTask (line 301): checks typeof task.id === 'string' — rejects numeric IDs
  • isSubtask (lines 321-322): checks typeof subtask.id === 'number' and typeof subtask.parentId === 'string' — rejects string IDs and numeric parentIds

These guards will now incorrectly return false for valid objects, potentially breaking runtime validation.

🐛 Proposed fix to support both types
 export function isTask(obj: unknown): obj is Task {
 	if (!obj || typeof obj !== 'object') return false;
 	const task = obj as Record<string, unknown>;
 
 	return (
-		typeof task.id === 'string' &&
+		(typeof task.id === 'string' || typeof task.id === 'number') &&
 		typeof task.title === 'string' &&
 		typeof task.description === 'string' &&
 		isTaskStatus(task.status) &&
 		isTaskPriority(task.priority) &&
 		Array.isArray(task.dependencies) &&
 		typeof task.details === 'string' &&
 		typeof task.testStrategy === 'string' &&
 		Array.isArray(task.subtasks)
 	);
 }

 export function isSubtask(obj: unknown): obj is Subtask {
 	if (!obj || typeof obj !== 'object') return false;
 	const subtask = obj as Record<string, unknown>;
 
 	return (
-		typeof subtask.id === 'number' &&
-		typeof subtask.parentId === 'string' &&
+		(typeof subtask.id === 'number' || typeof subtask.id === 'string') &&
+		(typeof subtask.parentId === 'number' || typeof subtask.parentId === 'string') &&
 		typeof subtask.title === 'string' &&
 		typeof subtask.description === 'string' &&
 		isTaskStatus(subtask.status) &&
 		isTaskPriority(subtask.priority) &&
 		!('subtasks' in subtask)
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tm-core/src/common/types/index.ts` around lines 296 - 329, The type
guards isTask and isSubtask currently only accept a single primitive for id
fields; update their checks to accept both string and number per the widened ID
types: in isTask allow task.id to be typeof 'string' OR 'number' (and if any
other id-like fields exist on Task, apply same change), and in isSubtask allow
subtask.id and subtask.parentId to be typeof 'string' OR 'number' instead of
forcing only 'number'/'string'; keep the rest of each guard
(status/priority/subtasks checks) unchanged.
🧹 Nitpick comments (3)
packages/tm-core/src/testing/task-fixtures.ts (2)

44-44: Stale documentation: comment says "Converted to string" but code now converts to number.

The comment at line 44 states id: Converted to string if number is provided, but the implementation at line 57 now uses Number(overrides.id). Update the comment to reflect the new behavior.

📝 Suggested fix
 * DEFAULTS:
- * - id: Converted to string if number is provided
+ * - id: Converted to number if string is provided
 * - status: 'pending'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tm-core/src/testing/task-fixtures.ts` at line 44, Update the stale
JSDoc/comment in packages/tm-core/src/testing/task-fixtures.ts describing the id
behavior: change the line that reads "id: Converted to string if number is
provided" to reflect the current implementation which uses Number(overrides.id)
— e.g., "id: Converted to number if string is provided (uses Number(...))" — so
the comment matches the actual conversion performed by the task fixture.

122-134: Inconsistent ID normalization: createSubtask doesn't normalize IDs to numbers like createTask.

createTask now coerces id to Number() (line 57), but createSubtask keeps the raw overrides.id and derives parentId as a string. For consistent test fixtures matching the PR's goal of numeric IDs in file storage, consider normalizing both here as well.

♻️ Suggested fix for consistency
 export function createSubtask(
 	overrides: Partial<Omit<Subtask, 'id' | 'parentId'>> & {
 		id: number | string;
 		title: string;
-		parentId?: string;
+		parentId?: number | string;
 	}
 ): Subtask {
 	const idStr = String(overrides.id);
-	const defaultParentId = idStr.includes('.') ? idStr.split('.')[0] : '1';
+	const defaultParentId = idStr.includes('.') ? Number(idStr.split('.')[0]) : 1;
 
 	return {
-		id: overrides.id,
-		parentId: overrides.parentId ?? defaultParentId,
+		id: Number(overrides.id),
+		parentId: Number(overrides.parentId ?? defaultParentId),

Based on learnings: "Use numeric IDs for direct task references" and the PR objective to normalize task IDs to numbers everywhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tm-core/src/testing/task-fixtures.ts` around lines 122 - 134,
createSubtask currently returns overrides.id and computes parentId as a string
(using idStr), causing inconsistency with createTask which coerces ids to
numbers; update createSubtask to normalize both the subtask id and parentId to
numbers (use Number(overrides.id) for id and derive defaultParentId as a numeric
value instead of a string), and ensure the returned Subtask properties use these
numeric values so fixtures match the numeric-ID convention used by createTask.
scripts/modules/task-manager/move-task.js (1)

819-834: Consider filtering non-finite IDs in getNextAvailableId.

If any existing task has a malformed ID that becomes NaN after normalization, Math.max(...allIds) will return NaN, and the function will return NaN + 1 = NaN.

🛡️ Suggested defensive improvement
 function getNextAvailableId(rawData, targetTag, idRemapping) {
-	const existingIds = rawData[targetTag].tasks.map((t) => t.id);
+	const existingIds = rawData[targetTag].tasks
+		.map((t) => t.id)
+		.filter((id) => Number.isFinite(id));
 	const remappedIds = Array.from(idRemapping.values());
-	const allIds = [...existingIds, ...remappedIds];
+	const allIds = [...existingIds, ...remappedIds].filter((id) => Number.isFinite(id));
 	if (allIds.length === 0) return 1;
 	return Math.max(...allIds) + 1;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/modules/task-manager/move-task.js` around lines 819 - 834, The
getNextAvailableId function should defensively filter and coerce IDs so
malformed or NaN values don't poison Math.max; when building existingIds and
remappedIds (from rawData[targetTag].tasks and idRemapping.values()),
map/convert entries to Number and then filter with Number.isFinite (or isFinite)
to produce valid numeric IDs in allIds, and if after filtering allIds is empty
return 1, otherwise return Math.max(...allIds) + 1; update references to
existingIds, remappedIds and allIds in getNextAvailableId accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts`:
- Around line 222-234: normalizeTasks currently uses Number(...) directly which
can produce NaN for malformed ids; update normalizeTasks to validate numeric
conversion for task.id, subtask.id, and subtask.parentId using
Number.isFinite(Number(value)) (or equivalent) and handle failures explicitly:
either throw a descriptive error (including the task/subtask context) or set a
safe fallback (e.g., null) and log the issue so callers don't get silent NaNs.
Ensure you apply this check inside the normalizeTasks function for the Task and
subtask mappings and include clear context (task id or index and subtask id) in
any error/log to aid debugging.

---

Outside diff comments:
In `@packages/tm-core/src/common/types/index.ts`:
- Around line 296-329: The type guards isTask and isSubtask currently only
accept a single primitive for id fields; update their checks to accept both
string and number per the widened ID types: in isTask allow task.id to be typeof
'string' OR 'number' (and if any other id-like fields exist on Task, apply same
change), and in isSubtask allow subtask.id and subtask.parentId to be typeof
'string' OR 'number' instead of forcing only 'number'/'string'; keep the rest of
each guard (status/priority/subtasks checks) unchanged.

---

Nitpick comments:
In `@packages/tm-core/src/testing/task-fixtures.ts`:
- Line 44: Update the stale JSDoc/comment in
packages/tm-core/src/testing/task-fixtures.ts describing the id behavior: change
the line that reads "id: Converted to string if number is provided" to reflect
the current implementation which uses Number(overrides.id) — e.g., "id:
Converted to number if string is provided (uses Number(...))" — so the comment
matches the actual conversion performed by the task fixture.
- Around line 122-134: createSubtask currently returns overrides.id and computes
parentId as a string (using idStr), causing inconsistency with createTask which
coerces ids to numbers; update createSubtask to normalize both the subtask id
and parentId to numbers (use Number(overrides.id) for id and derive
defaultParentId as a numeric value instead of a string), and ensure the returned
Subtask properties use these numeric values so fixtures match the numeric-ID
convention used by createTask.

In `@scripts/modules/task-manager/move-task.js`:
- Around line 819-834: The getNextAvailableId function should defensively filter
and coerce IDs so malformed or NaN values don't poison Math.max; when building
existingIds and remappedIds (from rawData[targetTag].tasks and
idRemapping.values()), map/convert entries to Number and then filter with
Number.isFinite (or isFinite) to produce valid numeric IDs in allIds, and if
after filtering allIds is empty return 1, otherwise return Math.max(...allIds) +
1; update references to existingIds, remappedIds and allIds in
getNextAvailableId accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb2f455d-bc65-4a46-8559-fbe69a866f96

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1211b and 3bfaeaf.

📒 Files selected for processing (7)
  • mcp-server/src/core/direct-functions/move-task-cross-tag.js
  • packages/tm-core/src/common/types/index.ts
  • packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts
  • packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts
  • packages/tm-core/src/testing/task-fixtures.ts
  • scripts/modules/task-manager/move-task.js
  • tests/integration/move-task-cross-tag.integration.test.js

Comment on lines 222 to 234
private normalizeTasks(tasks: Task[]): Task[] {
return tasks.map((task) => ({
...task,
id: String(task.id), // Task IDs are strings
id: Number(task.id), // Task IDs are numbers
dependencies: task.dependencies?.map((dep) => String(dep)) || [],
subtasks:
task.subtasks?.map((subtask) => ({
...subtask,
id: Number(subtask.id), // Subtask IDs are numbers
parentId: String(subtask.parentId) // Parent ID is string (Task ID)
parentId: Number(subtask.parentId) // Parent ID is number (Task ID)
})) || []
}));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider validating numeric conversion to avoid silent NaN values.

Number() returns NaN for undefined, null, or non-numeric strings. If task data is malformed (e.g., task.id is undefined or "abc"), the normalized task will have id: NaN, which could cause subtle bugs in lookups and comparisons.

Consider adding validation or using a safer conversion:

🛡️ Suggested defensive approach
 private normalizeTasks(tasks: Task[]): Task[] {
 	return tasks.map((task) => ({
 		...task,
-		id: Number(task.id), // Task IDs are numbers
+		id: this.toNumericId(task.id), // Task IDs are numbers
 		dependencies: task.dependencies?.map((dep) => String(dep)) || [],
 		subtasks:
 			task.subtasks?.map((subtask) => ({
 				...subtask,
-				id: Number(subtask.id), // Subtask IDs are numbers
-				parentId: Number(subtask.parentId) // Parent ID is number (Task ID)
+				id: this.toNumericId(subtask.id), // Subtask IDs are numbers
+				parentId: this.toNumericId(subtask.parentId) // Parent ID is number (Task ID)
 			})) || []
 	}));
 }

+/**
+ * Safely convert a value to a numeric ID, throwing if invalid
+ */
+private toNumericId(value: unknown): number {
+	const num = Number(value);
+	if (!Number.isFinite(num)) {
+		throw new Error(`Invalid task ID: ${value}`);
+	}
+	return num;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private normalizeTasks(tasks: Task[]): Task[] {
return tasks.map((task) => ({
...task,
id: String(task.id), // Task IDs are strings
id: Number(task.id), // Task IDs are numbers
dependencies: task.dependencies?.map((dep) => String(dep)) || [],
subtasks:
task.subtasks?.map((subtask) => ({
...subtask,
id: Number(subtask.id), // Subtask IDs are numbers
parentId: String(subtask.parentId) // Parent ID is string (Task ID)
parentId: Number(subtask.parentId) // Parent ID is number (Task ID)
})) || []
}));
}
private normalizeTasks(tasks: Task[]): Task[] {
return tasks.map((task) => ({
...task,
id: this.toNumericId(task.id), // Task IDs are numbers
dependencies: task.dependencies?.map((dep) => String(dep)) || [],
subtasks:
task.subtasks?.map((subtask) => ({
...subtask,
id: this.toNumericId(subtask.id), // Subtask IDs are numbers
parentId: this.toNumericId(subtask.parentId) // Parent ID is number (Task ID)
})) || []
}));
}
/**
* Safely convert a value to a numeric ID, throwing if invalid
*/
private toNumericId(value: unknown): number {
const num = Number(value);
if (!Number.isFinite(num)) {
throw new Error(`Invalid task ID: ${value}`);
}
return num;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts`
around lines 222 - 234, normalizeTasks currently uses Number(...) directly which
can produce NaN for malformed ids; update normalizeTasks to validate numeric
conversion for task.id, subtask.id, and subtask.parentId using
Number.isFinite(Number(value)) (or equivalent) and handle failures explicitly:
either throw a descriptive error (including the task/subtask context) or set a
safe fallback (e.g., null) and log the issue so callers don't get silent NaNs.
Ensure you apply this check inside the normalizeTasks function for the Task and
subtask mappings and include clear context (task id or index and subtask id) in
any error/log to aid debugging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Inconsistent task ID quoting in tasks.json between 'set task' and 'update task' commands

2 participants