feat: add unknown status and priority, change todo status to open

This commit is contained in:
2026-07-18 07:48:11 +05:00
parent 526a001d06
commit 148ed75841
28 changed files with 162 additions and 111 deletions
+10 -8
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { changeStatus } from "./changeStatus"; import { changeStatus } from "./changeStatus";
@@ -18,8 +20,8 @@ describe("changeStatus", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "Work item", title: "Work item",
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
@@ -27,23 +29,23 @@ describe("changeStatus", () => {
const result = await changeStatus(repo, { const result = await changeStatus(repo, {
folderPath: folder, folderPath: folder,
id: created.id, id: created.id,
status: "in-progress", status: TaskStatus.IN_PROGRESS,
}); });
expect(result.isOk()).toBe(true); expect(result.isOk()).toBe(true);
if (result.isErr()) return; if (result.isErr()) return;
expect(result.value.status).toBe("in-progress"); expect(result.value.status).toBe(TaskStatus.IN_PROGRESS);
expect(result.value.id).toBe(created.id); expect(result.value.id).toBe(created.id);
const stored = await repo.getById(folder, created.id); const stored = await repo.getById(folder, created.id);
expect(stored?.status).toBe("in-progress"); expect(stored?.status).toBe(TaskStatus.IN_PROGRESS);
}); });
it("returns not-found for unknown id", async () => { it("returns not-found for unknown id", async () => {
const result = await changeStatus(repo, { const result = await changeStatus(repo, {
folderPath: folder, folderPath: folder,
id: "missing", id: "missing",
status: "done", status: TaskStatus.DONE,
}); });
expect(result).toEqual( expect(result).toEqual(
err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })), err(appError(AppErrorVariant.NOT_FOUND, { id: "missing" })),
@@ -54,7 +56,7 @@ describe("changeStatus", () => {
const result = await changeStatus(repo, { const result = await changeStatus(repo, {
folderPath: undefined, folderPath: undefined,
id: "x", id: "x",
status: "done", status: TaskStatus.DONE,
}); });
expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER))); expect(result).toEqual(err(appError(AppErrorVariant.NO_FOLDER)));
}); });
@@ -63,7 +65,7 @@ describe("changeStatus", () => {
const result = await changeStatus(repo, { const result = await changeStatus(repo, {
folderPath: folder, folderPath: folder,
id: undefined, id: undefined,
status: "done", status: TaskStatus.DONE,
}); });
expect(result).toEqual(err(appError(AppErrorVariant.NO_ID))); expect(result).toEqual(err(appError(AppErrorVariant.NO_ID)));
}); });
+4 -3
View File
@@ -3,6 +3,7 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus"; import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { createTask } from "./createTask"; import { createTask } from "./createTask";
@@ -15,7 +16,7 @@ describe("createTask", () => {
repo = new FsTaskRepository(new InMemoryFileSystem(), TASK_FILE_EXTENSION); repo = new FsTaskRepository(new InMemoryFileSystem(), TASK_FILE_EXTENSION);
}); });
it("creates a todo/medium task from title", async () => { it("creates an open/medium task from title", async () => {
const result = await createTask(repo, { const result = await createTask(repo, {
folderPath: folder, folderPath: folder,
title: " New feature ", title: " New feature ",
@@ -25,8 +26,8 @@ describe("createTask", () => {
if (result.isErr()) return; if (result.isErr()) return;
expect(result.value.title).toBe("New feature"); expect(result.value.title).toBe("New feature");
expect(result.value.status).toBe("todo"); expect(result.value.status).toBe(TaskStatus.OPEN);
expect(result.value.priority).toBe("medium"); expect(result.value.priority).toBe(TaskPriority.MEDIUM);
expect(result.value.description).toBe(""); expect(result.value.description).toBe("");
expect(result.value.tags).toEqual([]); expect(result.value.tags).toEqual([]);
expect(result.value.assignee).toBe(""); expect(result.value.assignee).toBe("");
+3 -3
View File
@@ -8,7 +8,7 @@ import { ITaskRepository } from "@/ports";
export type CreateTaskInput = { export type CreateTaskInput = {
folderPath?: string; folderPath?: string;
title?: string; title?: string;
/** Defaults to TODO when omitted. */ /** Defaults to OPEN when omitted. */
status?: TaskStatus; status?: TaskStatus;
}; };
@@ -27,10 +27,10 @@ export function createTask(
const status = const status =
input.status === undefined input.status === undefined
? TaskStatus.TODO ? TaskStatus.OPEN
: isTaskStatus(input.status) : isTaskStatus(input.status)
? input.status ? input.status
: TaskStatus.TODO; : TaskStatus.OPEN;
return ResultAsync.fromSafePromise( return ResultAsync.fromSafePromise(
repo.create(input.folderPath, { repo.create(input.folderPath, {
+4 -2
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err, ok } from "neverthrow"; import { err, ok } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { deleteTask } from "./deleteTask"; import { deleteTask } from "./deleteTask";
@@ -18,8 +20,8 @@ describe("deleteTask", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "To delete", title: "To delete",
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
+6 -4
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { listLocatedTasks } from "./listLocatedTasks"; import { listLocatedTasks } from "./listLocatedTasks";
@@ -19,16 +21,16 @@ describe("listLocatedTasks", () => {
const p = await repo.create(project, { const p = await repo.create(project, {
title: "Project task", title: "Project task",
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
const g = await repo.create(global, { const g = await repo.create(global, {
title: "Global task", title: "Global task",
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "high", priority: TaskPriority.HIGH,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
+6 -4
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { loadDashboard } from "./loadDashboard"; import { loadDashboard } from "./loadDashboard";
@@ -18,16 +20,16 @@ describe("loadDashboard", () => {
const a = await repo.create(folder, { const a = await repo.create(folder, {
title: "First", title: "First",
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
const b = await repo.create(folder, { const b = await repo.create(folder, {
title: "Second", title: "Second",
description: "body", description: "body",
status: "done", status: TaskStatus.DONE,
priority: "high", priority: TaskPriority.HIGH,
tags: ["x"], tags: ["x"],
assignee: "", assignee: "",
}); });
+4 -2
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { openTask } from "./openTask"; import { openTask } from "./openTask";
@@ -18,8 +20,8 @@ describe("openTask", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "Open me", title: "Open me",
description: "body", description: "body",
status: "todo", status: TaskStatus.OPEN,
priority: "high", priority: TaskPriority.HIGH,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
+8 -6
View File
@@ -3,6 +3,8 @@ import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { saveTaskDescription } from "./saveTaskDescription"; import { saveTaskDescription } from "./saveTaskDescription";
@@ -18,8 +20,8 @@ describe("saveTaskDescription", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "Editable", title: "Editable",
description: "old body", description: "old body",
status: "todo", status: TaskStatus.OPEN,
priority: "high", priority: TaskPriority.HIGH,
tags: ["keep"], tags: ["keep"],
assignee: "alice", assignee: "alice",
}); });
@@ -36,8 +38,8 @@ describe("saveTaskDescription", () => {
expect(result.value.id).toBe(created.id); expect(result.value.id).toBe(created.id);
expect(result.value.description).toBe("new markdown body"); expect(result.value.description).toBe("new markdown body");
expect(result.value.title).toBe("Editable"); expect(result.value.title).toBe("Editable");
expect(result.value.status).toBe("todo"); expect(result.value.status).toBe(TaskStatus.OPEN);
expect(result.value.priority).toBe("high"); expect(result.value.priority).toBe(TaskPriority.HIGH);
expect(result.value.tags).toEqual(["keep"]); expect(result.value.tags).toEqual(["keep"]);
expect(result.value.assignee).toBe("alice"); expect(result.value.assignee).toBe("alice");
@@ -49,8 +51,8 @@ describe("saveTaskDescription", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "Clear me", title: "Clear me",
description: "something", description: "something",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
+3 -2
View File
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus"; import { TaskStatus } from "@/model/taskStatus";
import { FsTaskRepository } from "@/storage/FsTaskRepository"; import { FsTaskRepository } from "@/storage/FsTaskRepository";
import { serializeTask } from "@/utils/markdown"; import { serializeTask } from "@/utils/markdown";
@@ -20,8 +21,8 @@ describe("saveTaskRaw", () => {
const created = await repo.create(folder, { const created = await repo.create(folder, {
title: "Raw me", title: "Raw me",
description: "old", description: "old",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
}); });
+5 -4
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { err } from "neverthrow"; import { err } from "neverthrow";
import { appError, AppErrorVariant } from "@/error"; import { appError, AppErrorVariant } from "@/error";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus"; import { TaskStatus } from "@/model/taskStatus";
import { serializeTask } from "@/utils/markdown"; import { serializeTask } from "@/utils/markdown";
import { applyRawTaskContent } from "./applyRawTaskContent"; import { applyRawTaskContent } from "./applyRawTaskContent";
@@ -10,8 +11,8 @@ const previous: Task = {
id: "keep-me", id: "keep-me",
title: "Old title", title: "Old title",
description: "old body", description: "old body",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
created: "2026-01-01T00:00:00.000Z", created: "2026-01-01T00:00:00.000Z",
@@ -26,7 +27,7 @@ describe("applyRawTaskContent", () => {
title: "New title", title: "New title",
description: "new body", description: "new body",
status: TaskStatus.DONE, status: TaskStatus.DONE,
priority: "high", priority: TaskPriority.HIGH,
tags: ["a"], tags: ["a"],
assignee: "bob", assignee: "bob",
created: "2099-01-01T00:00:00.000Z", created: "2099-01-01T00:00:00.000Z",
@@ -41,7 +42,7 @@ describe("applyRawTaskContent", () => {
expect(result.value.title).toBe("New title"); expect(result.value.title).toBe("New title");
expect(result.value.description).toBe("new body"); expect(result.value.description).toBe("new body");
expect(result.value.status).toBe(TaskStatus.DONE); expect(result.value.status).toBe(TaskStatus.DONE);
expect(result.value.priority).toBe("high"); expect(result.value.priority).toBe(TaskPriority.HIGH);
expect(result.value.tags).toEqual(["a"]); expect(result.value.tags).toEqual(["a"]);
expect(result.value.assignee).toBe("bob"); expect(result.value.assignee).toBe("bob");
}); });
+13 -11
View File
@@ -1,13 +1,15 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { GroupBy } from "./groupBy"; import { GroupBy } from "./groupBy";
import { buildDashboardView } from "./buildDashboardView"; import { buildDashboardView } from "./buildDashboardView";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task { function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
return { return {
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
created: "2026-01-01T00:00:00.000Z", created: "2026-01-01T00:00:00.000Z",
@@ -21,22 +23,22 @@ describe("buildDashboardView", () => {
task({ task({
id: "1", id: "1",
title: "Alpha", title: "Alpha",
status: "todo", status: TaskStatus.OPEN,
priority: "high", priority: TaskPriority.HIGH,
description: "first", description: "first",
}), }),
task({ task({
id: "2", id: "2",
title: "Beta", title: "Beta",
status: "in-progress", status: TaskStatus.IN_PROGRESS,
priority: "low", priority: TaskPriority.LOW,
description: "second", description: "second",
}), }),
task({ task({
id: "3", id: "3",
title: "Gamma", title: "Gamma",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
description: "third", description: "third",
}), }),
]; ];
@@ -52,12 +54,12 @@ describe("buildDashboardView", () => {
it("applies filter then groups", () => { it("applies filter then groups", () => {
const view = buildDashboardView(tasks, { const view = buildDashboardView(tasks, {
filter: { statuses: ["todo"] }, filter: { statuses: [TaskStatus.OPEN] },
groupBy: GroupBy.PRIORITY, groupBy: GroupBy.PRIORITY,
}); });
expect(view.visibleTasks.map((t) => t.id)).toEqual(["1", "3"]); expect(view.visibleTasks.map((t) => t.id)).toEqual(["1", "3"]);
expect(view.groups.map((g) => g.key)).toEqual(["high", "medium"]); expect(view.groups.map((g) => g.key)).toEqual([TaskPriority.HIGH, TaskPriority.MEDIUM]);
}); });
it("resolves selected task by id from the full task list", () => { it("resolves selected task by id from the full task list", () => {
@@ -68,7 +70,7 @@ describe("buildDashboardView", () => {
it("keeps selection even when the task is filtered out of the list", () => { it("keeps selection even when the task is filtered out of the list", () => {
const view = buildDashboardView(tasks, { const view = buildDashboardView(tasks, {
filter: { statuses: ["todo"] }, filter: { statuses: [TaskStatus.OPEN] },
selectedId: "2", selectedId: "2",
}); });
+3 -2
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { LocatedTask, TaskScope } from "@/model/taskLocation"; import { LocatedTask, TaskScope } from "@/model/taskLocation";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus"; import { TaskStatus } from "@/model/taskStatus";
import { filterLocatedByScope } from "./filterLocatedByScope"; import { filterLocatedByScope } from "./filterLocatedByScope";
@@ -13,8 +14,8 @@ function located(
id, id,
title: id, title: id,
description: "", description: "",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
created: "2026-01-01T00:00:00.000Z", created: "2026-01-01T00:00:00.000Z",
+18 -16
View File
@@ -1,12 +1,14 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { filterTasks } from "./filterTasks"; import { filterTasks } from "./filterTasks";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task { function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
return { return {
description: "", description: "",
status: "todo", status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
created: "2026-01-01T00:00:00.000Z", created: "2026-01-01T00:00:00.000Z",
@@ -21,32 +23,32 @@ describe("filterTasks", () => {
id: "1", id: "1",
title: "Fix login", title: "Fix login",
description: "Auth token expired", description: "Auth token expired",
status: "todo", status: TaskStatus.OPEN,
priority: "high", priority: TaskPriority.HIGH,
tags: ["bug", "frontend"], tags: ["bug", "frontend"],
}), }),
task({ task({
id: "2", id: "2",
title: "Add dark mode", title: "Add dark mode",
description: "Theme toggle", description: "Theme toggle",
status: "in-progress", status: TaskStatus.IN_PROGRESS,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: ["feature", "frontend"], tags: ["feature", "frontend"],
}), }),
task({ task({
id: "3", id: "3",
title: "Write docs", title: "Write docs",
description: "API reference", description: "API reference",
status: "done", status: TaskStatus.DONE,
priority: "low", priority: TaskPriority.LOW,
tags: ["docs"], tags: ["docs"],
}), }),
task({ task({
id: "4", id: "4",
title: "Deploy hotfix", title: "Deploy hotfix",
description: "Production login outage", description: "Production login outage",
status: "todo", status: TaskStatus.OPEN,
priority: "critical", priority: TaskPriority.CRITICAL,
tags: ["bug", "ops"], tags: ["bug", "ops"],
}), }),
]; ];
@@ -56,17 +58,17 @@ describe("filterTasks", () => {
}); });
it("filters by a single status", () => { it("filters by a single status", () => {
const result = filterTasks(tasks, { statuses: ["todo"] }); const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN] });
expect(result.map((t) => t.id)).toEqual(["1", "4"]); expect(result.map((t) => t.id)).toEqual(["1", "4"]);
}); });
it("filters by multiple statuses", () => { it("filters by multiple statuses", () => {
const result = filterTasks(tasks, { statuses: ["todo", "done"] }); const result = filterTasks(tasks, { statuses: [TaskStatus.OPEN, TaskStatus.DONE] });
expect(result.map((t) => t.id)).toEqual(["1", "3", "4"]); expect(result.map((t) => t.id)).toEqual(["1", "3", "4"]);
}); });
it("filters by priority", () => { it("filters by priority", () => {
const result = filterTasks(tasks, { priorities: ["high", "critical"] }); const result = filterTasks(tasks, { priorities: [TaskPriority.HIGH, TaskPriority.CRITICAL] });
expect(result.map((t) => t.id)).toEqual(["1", "4"]); expect(result.map((t) => t.id)).toEqual(["1", "4"]);
}); });
@@ -92,8 +94,8 @@ describe("filterTasks", () => {
it("combines status, priority and query with AND", () => { it("combines status, priority and query with AND", () => {
const result = filterTasks(tasks, { const result = filterTasks(tasks, {
statuses: ["todo"], statuses: [TaskStatus.OPEN],
priorities: ["critical"], priorities: [TaskPriority.CRITICAL],
query: "login", query: "login",
}); });
expect(result.map((t) => t.id)).toEqual(["4"]); expect(result.map((t) => t.id)).toEqual(["4"]);
@@ -101,6 +103,6 @@ describe("filterTasks", () => {
it("returns empty array for empty input", () => { it("returns empty array for empty input", () => {
expect(filterTasks([], {})).toEqual([]); expect(filterTasks([], {})).toEqual([]);
expect(filterTasks([], { statuses: ["todo"] })).toEqual([]); expect(filterTasks([], { statuses: [TaskStatus.OPEN] })).toEqual([]);
}); });
}); });
+4 -4
View File
@@ -16,7 +16,7 @@ import { groupTasks } from "./groupTasks";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task { function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
return { return {
description: "", description: "",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: TaskPriority.MEDIUM, priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
@@ -37,13 +37,13 @@ describe("groupTasks", () => {
task({ task({
id: "b", id: "b",
title: "B", title: "B",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: TaskPriority.CRITICAL, priority: TaskPriority.CRITICAL,
}), }),
task({ task({
id: "c", id: "c",
title: "C", title: "C",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: TaskPriority.HIGH, priority: TaskPriority.HIGH,
}), }),
task({ task({
@@ -97,7 +97,7 @@ describe("groupTasks", () => {
it("silently drops tasks with unknown status from groups", () => { it("silently drops tasks with unknown status from groups", () => {
const tasks: Task[] = [ const tasks: Task[] = [
task({ id: "a", title: "A", status: "__garbage__" as TaskStatus }), task({ id: "a", title: "A", status: "__garbage__" as TaskStatus }),
task({ id: "b", title: "B", status: TaskStatus.TODO }), task({ id: "b", title: "B", status: TaskStatus.OPEN }),
]; ];
const groups = groupTasks(tasks, GroupBy.STATUS); const groups = groupTasks(tasks, GroupBy.STATUS);
const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id)); const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id));
+3 -2
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus"; import { TaskStatus } from "@/model/taskStatus";
import { sortTasks, TaskSortDirection } from "./sortTasks"; import { sortTasks, TaskSortDirection } from "./sortTasks";
@@ -8,8 +9,8 @@ function task(
): Task { ): Task {
return { return {
description: "", description: "",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
updated: "2026-01-01T00:00:00.000Z", updated: "2026-01-01T00:00:00.000Z",
+4 -2
View File
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { import {
extractFrontmatterBlock, extractFrontmatterBlock,
findFieldRange, findFieldRange,
@@ -20,7 +22,7 @@ Body
describe("replaceFrontmatterField", () => { describe("replaceFrontmatterField", () => {
it("replaces status value", () => { it("replaces status value", () => {
const result = replaceFrontmatterField(sample, "status", "done"); const result = replaceFrontmatterField(sample, "status", TaskStatus.DONE);
expect(result.ok).toBe(true); expect(result.ok).toBe(true);
if (!result.ok) return; if (!result.ok) return;
expect(result.content).toContain("status: done"); expect(result.content).toContain("status: done");
@@ -29,7 +31,7 @@ describe("replaceFrontmatterField", () => {
}); });
it("replaces priority value", () => { it("replaces priority value", () => {
const result = replaceFrontmatterField(sample, "priority", "critical"); const result = replaceFrontmatterField(sample, "priority", TaskPriority.CRITICAL);
expect(result.ok).toBe(true); expect(result.ok).toBe(true);
if (!result.ok) return; if (!result.ok) return;
expect(result.content).toContain("priority: critical"); expect(result.content).toContain("priority: critical");
+2 -2
View File
@@ -5,7 +5,7 @@ import { validateFrontmatter } from "./validateFrontmatter";
const valid = `--- const valid = `---
id: "550e8400-e29b-41d4-a716-446655440000" id: "550e8400-e29b-41d4-a716-446655440000"
title: Fix login title: Fix login
status: todo status: open
priority: high priority: high
tags: [bug] tags: [bug]
assignee: "" assignee: ""
@@ -34,7 +34,7 @@ describe("validateFrontmatter", () => {
}); });
it("errors on invalid status", () => { it("errors on invalid status", () => {
const content = valid.replace("status: todo", "status: nope"); const content = valid.replace("status: open", "status: nope");
const issues = validateFrontmatter(content); const issues = validateFrontmatter(content);
const statusIssue = issues.find((i) => i.field === "status"); const statusIssue = issues.find((i) => i.field === "status");
expect(statusIssue).toBeDefined(); expect(statusIssue).toBeDefined();
+11 -10
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { import {
TASK_STATUS_META, TASK_STATUS_META,
TASK_STATUS_ORDER, TASK_STATUS_ORDER,
@@ -10,8 +11,8 @@ import { buildKanbanBoard } from "./buildKanbanBoard";
function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task { function task(partial: Partial<Task> & Pick<Task, "id" | "title">): Task {
return { return {
description: "", description: "",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
priority: "medium", priority: TaskPriority.MEDIUM,
tags: [], tags: [],
assignee: "", assignee: "",
created: "2026-01-01T00:00:00.000Z", created: "2026-01-01T00:00:00.000Z",
@@ -35,13 +36,13 @@ describe("buildKanbanBoard", () => {
task({ task({
id: "b", id: "b",
title: "Todo older", title: "Todo older",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
updated: "2026-01-01T00:00:01.000Z", updated: "2026-01-01T00:00:01.000Z",
}), }),
task({ task({
id: "c", id: "c",
title: "Todo newer", title: "Todo newer",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
updated: "2026-01-01T00:00:03.000Z", updated: "2026-01-01T00:00:03.000Z",
}), }),
task({ task({
@@ -96,7 +97,7 @@ describe("buildKanbanBoard", () => {
const byStatus = Object.fromEntries( const byStatus = Object.fromEntries(
board.columns.map((col) => [col.status, col.tasks.map((t) => t.id)]), board.columns.map((col) => [col.status, col.tasks.map((t) => t.id)]),
); );
expect(byStatus[TaskStatus.TODO]).toEqual(["c", "b"]); expect(byStatus[TaskStatus.OPEN]).toEqual(["c", "b"]);
expect(byStatus[TaskStatus.IN_PROGRESS]).toEqual(["d"]); expect(byStatus[TaskStatus.IN_PROGRESS]).toEqual(["d"]);
expect(byStatus[TaskStatus.DONE]).toEqual(["a"]); expect(byStatus[TaskStatus.DONE]).toEqual(["a"]);
expect(byStatus[TaskStatus.BACKLOG]).toEqual(["e"]); expect(byStatus[TaskStatus.BACKLOG]).toEqual(["e"]);
@@ -105,7 +106,7 @@ describe("buildKanbanBoard", () => {
it("sorts tasks within a column by updated desc", () => { it("sorts tasks within a column by updated desc", () => {
const board = buildKanbanBoard(tasks); const board = buildKanbanBoard(tasks);
const todo = board.columns.find((col) => col.status === TaskStatus.TODO); const todo = board.columns.find((col) => col.status === TaskStatus.OPEN);
expect(todo?.tasks.map((t) => t.id)).toEqual(["c", "b"]); expect(todo?.tasks.map((t) => t.id)).toEqual(["c", "b"]);
}); });
@@ -124,24 +125,24 @@ describe("buildKanbanBoard", () => {
task({ task({
id: "a", id: "a",
title: "A", title: "A",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
updated: sameUpdated, updated: sameUpdated,
}), }),
task({ task({
id: "b", id: "b",
title: "B", title: "B",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
updated: sameUpdated, updated: sameUpdated,
}), }),
task({ task({
id: "c", id: "c",
title: "C", title: "C",
status: TaskStatus.TODO, status: TaskStatus.OPEN,
updated: sameUpdated, updated: sameUpdated,
}), }),
]; ];
const board = buildKanbanBoard(tasks); const board = buildKanbanBoard(tasks);
const todo = board.columns.find((col) => col.status === TaskStatus.TODO); const todo = board.columns.find((col) => col.status === TaskStatus.OPEN);
expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]); expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]);
}); });
}); });
@@ -6,5 +6,6 @@ exports[`TaskPriority > exposes wire values on const keys 1`] = `
"HIGH": "high", "HIGH": "high",
"LOW": "low", "LOW": "low",
"MEDIUM": "medium", "MEDIUM": "medium",
"UNKNOWN": "unknown",
} }
`; `;
@@ -6,6 +6,7 @@ exports[`TaskStatus > exposes wire values on const keys 1`] = `
"CANCELLED": "cancelled", "CANCELLED": "cancelled",
"DONE": "done", "DONE": "done",
"IN_PROGRESS": "in-progress", "IN_PROGRESS": "in-progress",
"TODO": "todo", "OPEN": "open",
"UNKNOWN": "unknown",
} }
`; `;
+1
View File
@@ -23,6 +23,7 @@ describe("TaskPriority", () => {
it("isTaskPriority accepts known values only", () => { it("isTaskPriority accepts known values only", () => {
expect(isTaskPriority(TaskPriority.HIGH)).toBe(true); expect(isTaskPriority(TaskPriority.HIGH)).toBe(true);
expect(isTaskPriority(TaskPriority.UNKNOWN)).toBe(true);
expect(isTaskPriority("__not_a_priority__")).toBe(false); expect(isTaskPriority("__not_a_priority__")).toBe(false);
}); });
}); });
+4 -1
View File
@@ -3,6 +3,7 @@ export const TaskPriority = {
HIGH: "high", HIGH: "high",
MEDIUM: "medium", MEDIUM: "medium",
LOW: "low", LOW: "low",
UNKNOWN: "unknown",
} as const; } as const;
export type TaskPriority = (typeof TaskPriority)[keyof typeof TaskPriority]; export type TaskPriority = (typeof TaskPriority)[keyof typeof TaskPriority];
@@ -11,12 +12,13 @@ export type TaskPriorityMeta = {
label: string; label: string;
}; };
/** Explicit order for filters / grouping (critical → low). */ /** Explicit order for filters / grouping (critical → low → unknown). */
export const TASK_PRIORITY_ORDER: TaskPriority[] = [ export const TASK_PRIORITY_ORDER: TaskPriority[] = [
TaskPriority.CRITICAL, TaskPriority.CRITICAL,
TaskPriority.HIGH, TaskPriority.HIGH,
TaskPriority.MEDIUM, TaskPriority.MEDIUM,
TaskPriority.LOW, TaskPriority.LOW,
TaskPriority.UNKNOWN,
]; ];
export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = { export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = {
@@ -24,6 +26,7 @@ export const TASK_PRIORITY_META: Record<TaskPriority, TaskPriorityMeta> = {
[TaskPriority.HIGH]: { label: "High" }, [TaskPriority.HIGH]: { label: "High" },
[TaskPriority.MEDIUM]: { label: "Medium" }, [TaskPriority.MEDIUM]: { label: "Medium" },
[TaskPriority.LOW]: { label: "Low" }, [TaskPriority.LOW]: { label: "Low" },
[TaskPriority.UNKNOWN]: { label: "Unknown" },
}; };
export function isTaskPriority(value: unknown): value is TaskPriority { export function isTaskPriority(value: unknown): value is TaskPriority {
+3 -2
View File
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
isTaskStatus, isTaskStatus,
TaskStatus,
TASK_STATUS_META, TASK_STATUS_META,
TASK_STATUS_ORDER, TASK_STATUS_ORDER,
TaskStatus,
} from "./taskStatus"; } from "./taskStatus";
describe("TaskStatus", () => { describe("TaskStatus", () => {
@@ -23,8 +23,9 @@ describe("TaskStatus", () => {
}); });
it("isTaskStatus accepts known values only", () => { it("isTaskStatus accepts known values only", () => {
expect(isTaskStatus(TaskStatus.TODO)).toBe(true); expect(isTaskStatus(TaskStatus.OPEN)).toBe(true);
expect(isTaskStatus(TaskStatus.BACKLOG)).toBe(true); expect(isTaskStatus(TaskStatus.BACKLOG)).toBe(true);
expect(isTaskStatus(TaskStatus.UNKNOWN)).toBe(true);
expect(isTaskStatus("__not_a_status__")).toBe(false); expect(isTaskStatus("__not_a_status__")).toBe(false);
expect(isTaskStatus(null)).toBe(false); expect(isTaskStatus(null)).toBe(false);
}); });
+11 -4
View File
@@ -1,9 +1,10 @@
export const TaskStatus = { export const TaskStatus = {
BACKLOG: "backlog", BACKLOG: "backlog",
TODO: "todo", OPEN: "open",
IN_PROGRESS: "in-progress", IN_PROGRESS: "in-progress",
DONE: "done", DONE: "done",
CANCELLED: "cancelled", CANCELLED: "cancelled",
UNKNOWN: "unknown",
} as const; } as const;
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus]; export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
@@ -19,10 +20,11 @@ export type TaskStatusMeta = {
/** Explicit display / column order (not Object.values). */ /** Explicit display / column order (not Object.values). */
export const TASK_STATUS_ORDER: TaskStatus[] = [ export const TASK_STATUS_ORDER: TaskStatus[] = [
TaskStatus.BACKLOG, TaskStatus.BACKLOG,
TaskStatus.TODO, TaskStatus.OPEN,
TaskStatus.IN_PROGRESS, TaskStatus.IN_PROGRESS,
TaskStatus.DONE, TaskStatus.DONE,
TaskStatus.CANCELLED, TaskStatus.CANCELLED,
TaskStatus.UNKNOWN,
]; ];
export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = { export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
@@ -31,8 +33,8 @@ export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
defaultHidden: true, defaultHidden: true,
icon: "inbox", icon: "inbox",
}, },
[TaskStatus.TODO]: { [TaskStatus.OPEN]: {
label: "To Do", label: "Open",
defaultHidden: false, defaultHidden: false,
icon: "circle-outline", icon: "circle-outline",
}, },
@@ -51,6 +53,11 @@ export const TASK_STATUS_META: Record<TaskStatus, TaskStatusMeta> = {
defaultHidden: true, defaultHidden: true,
icon: "close", icon: "close",
}, },
[TaskStatus.UNKNOWN]: {
label: "Unknown",
defaultHidden: false,
icon: "question",
},
}; };
export function isTaskStatus(value: unknown): value is TaskStatus { export function isTaskStatus(value: unknown): value is TaskStatus {
+9 -7
View File
@@ -1,6 +1,8 @@
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { ITaskRepository } from "@/ports"; import { ITaskRepository } from "@/ports";
import { TASK_FILE_EXTENSION } from "@/model/taskFile"; import { TASK_FILE_EXTENSION } from "@/model/taskFile";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { beforeEach, describe, expect, it } from "vitest"; import { beforeEach, describe, expect, it } from "vitest";
import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem"; import { InMemoryFileSystem } from "../../tests/helpers/InMemoryFileSystem";
import { FsTaskRepository } from "./FsTaskRepository"; import { FsTaskRepository } from "./FsTaskRepository";
@@ -21,8 +23,8 @@ describe("FsTaskRepository", () => {
const sampleTask = { const sampleTask = {
title: "Fix login bug", title: "Fix login bug",
description: "Token validation error", description: "Token validation error",
status: "todo" as const, status: TaskStatus.OPEN,
priority: "high" as const, priority: TaskPriority.HIGH,
tags: ["bug", "frontend"], tags: ["bug", "frontend"],
assignee: "ivanov", assignee: "ivanov",
}; };
@@ -53,7 +55,7 @@ describe("FsTaskRepository", () => {
`${folder}/fix-login-bug${TASK_FILE_EXTENSION}`, `${folder}/fix-login-bug${TASK_FILE_EXTENSION}`,
); );
expect(content).toContain("Fix login bug"); expect(content).toContain("Fix login bug");
expect(content).toContain("status: todo"); expect(content).toContain("status: open");
}); });
it("list returns created tasks sorted by updated desc", async () => { it("list returns created tasks sorted by updated desc", async () => {
@@ -81,12 +83,12 @@ describe("FsTaskRepository", () => {
it("update modifies fields and bumps updated timestamp", async () => { it("update modifies fields and bumps updated timestamp", async () => {
const task = await repo.create(folder, sampleTask); const task = await repo.create(folder, sampleTask);
const updated: Task = { ...task, status: "done" }; const updated: Task = { ...task, status: TaskStatus.DONE };
await repo.update(folder, updated); await repo.update(folder, updated);
const found = await repo.getById(folder, task.id); const found = await repo.getById(folder, task.id);
expect(found!.status).toBe("done"); expect(found!.status).toBe(TaskStatus.DONE);
expect(found!.title).toBe(task.title); expect(found!.title).toBe(task.title);
expect(found!.updated).toBe(t1); expect(found!.updated).toBe(t1);
expect(found!.created).toBe(t0); expect(found!.created).toBe(t0);
@@ -220,7 +222,7 @@ describe("FsTaskRepository", () => {
}); });
it("creates task with tags containing special characters", async () => { it("creates task with tags containing special characters", async () => {
const task = await repo.create(folder, { await repo.create(folder, {
...sampleTask, ...sampleTask,
tags: ["c#", "c++", "tag name", "tag:value"], tags: ["c#", "c++", "tag name", "tag:value"],
}); });
@@ -236,7 +238,7 @@ describe("FsTaskRepository", () => {
}); });
it("getFilePath produces consistent slug regardless of special characters", async () => { it("getFilePath produces consistent slug regardless of special characters", async () => {
const task = await repo.create(folder, { await repo.create(folder, {
...sampleTask, ...sampleTask,
title: "Fix___bug...", title: "Fix___bug...",
}); });
+2 -1
View File
@@ -11,7 +11,8 @@ export function resolveTaskFolderForPath(
): string | undefined { ): string | undefined {
const normalized = path.resolve(filePath).replace(/\\/g, "/"); const normalized = path.resolve(filePath).replace(/\\/g, "/");
let best: string | undefined; let best: string | undefined;
let bestLen = -1; const NO_MATCH = -1;
let bestLen = NO_MATCH;
for (const location of locations) { for (const location of locations) {
const folder = path.resolve(location.folderPath).replace(/\\/g, "/"); const folder = path.resolve(location.folderPath).replace(/\\/g, "/");
+6 -4
View File
@@ -1,4 +1,6 @@
import { Task } from "@/model/task"; import { Task } from "@/model/task";
import { TaskPriority } from "@/model/taskPriority";
import { TaskStatus } from "@/model/taskStatus";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { parseTaskFile, serializeTask } from "./markdown"; import { parseTaskFile, serializeTask } from "./markdown";
@@ -8,8 +10,8 @@ const fullTask: Task = {
id: "550e8400-e29b-41d4-a716-446655440000", id: "550e8400-e29b-41d4-a716-446655440000",
title: "Пофиксить баг логина", title: "Пофиксить баг логина",
description: "Ошибка валидации токена", description: "Ошибка валидации токена",
status: "in-progress", status: TaskStatus.IN_PROGRESS,
priority: "high", priority: TaskPriority.HIGH,
tags: ["bug", "frontend"], tags: ["bug", "frontend"],
assignee: "ivanov", assignee: "ivanov",
created: "2026-07-12T10:00:00.000Z", created: "2026-07-12T10:00:00.000Z",
@@ -45,8 +47,8 @@ describe("markdown", () => {
expect(task.id).toBe("abc"); expect(task.id).toBe("abc");
expect(task.title).toBe("Minimal"); expect(task.title).toBe("Minimal");
expect(task.description).toBe(""); expect(task.description).toBe("");
expect(task.status).toBe("todo"); expect(task.status).toBe(TaskStatus.UNKNOWN);
expect(task.priority).toBe("medium"); expect(task.priority).toBe(TaskPriority.UNKNOWN);
expect(task.tags).toEqual([]); expect(task.tags).toEqual([]);
expect(task.assignee).toBe(""); expect(task.assignee).toBe("");
expect(task.created).toMatch(ISO_RE); expect(task.created).toMatch(ISO_RE);
+12 -4
View File
@@ -10,6 +10,16 @@ function toISO(value: unknown): string {
return new Date().toISOString(); return new Date().toISOString();
} }
function normalizeTaskStatus(value: unknown): TaskStatus {
if (isTaskStatus(value)) return value;
return TaskStatus.UNKNOWN;
}
function normalizeTaskPriority(value: unknown): TaskPriority {
if (isTaskPriority(value)) return value;
return TaskPriority.UNKNOWN;
}
export function parseTaskFile(content: string): Task { export function parseTaskFile(content: string): Task {
const parsed = fm<Record<string, unknown>>(content); const parsed = fm<Record<string, unknown>>(content);
const data = parsed.attributes; const data = parsed.attributes;
@@ -17,10 +27,8 @@ export function parseTaskFile(content: string): Task {
id: data.id as string, id: data.id as string,
title: data.title as string, title: data.title as string,
description: parsed.body.trim(), description: parsed.body.trim(),
status: isTaskStatus(data.status) ? data.status : TaskStatus.TODO, status: normalizeTaskStatus(data.status),
priority: isTaskPriority(data.priority) priority: normalizeTaskPriority(data.priority),
? data.priority
: TaskPriority.MEDIUM,
tags: (data.tags as string[]) ?? [], tags: (data.tags as string[]) ?? [],
assignee: (data.assignee as string) ?? "", assignee: (data.assignee as string) ?? "",
created: toISO(data.created), created: toISO(data.created),