feat: strengthen task workflow and compaction recovery
This commit is contained in:
+30
-5
@@ -10,13 +10,13 @@ import type {
|
||||
} from "./types.ts";
|
||||
import { TaskValidationError } from "./types.ts";
|
||||
|
||||
const MUTATION_TOOLS = new Set(["add_task", "update_task", "rm_task"]);
|
||||
const MUTATION_TOOLS = new Set(["add_task", "set_tasks", "update_task", "rm_task"]);
|
||||
const VALID_STATUSES = new Set<TaskStatus>(["pending", "in-progress", "blocked", "done"]);
|
||||
|
||||
interface TaskRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
status: TaskStatus;
|
||||
parentId: number | null;
|
||||
}
|
||||
@@ -63,7 +63,9 @@ function validateGraph(records: TaskRecord[]): string[] {
|
||||
ids.add(task.id);
|
||||
if (!task.name.trim()) errors.push(`task ${task.id}: name must not be empty`);
|
||||
if (/[\r\n]/.test(task.name)) errors.push(`task ${task.id}: name must be a single line`);
|
||||
if (!task.description.trim()) errors.push(`task ${task.id}: description must not be empty`);
|
||||
if (task.description !== undefined && !task.description.trim()) {
|
||||
errors.push(`task ${task.id}: description must not be empty`);
|
||||
}
|
||||
if (!VALID_STATUSES.has(task.status)) errors.push(`task ${task.id}: invalid status '${task.status}'`);
|
||||
if (task.parentId !== null && !byId.has(task.parentId)) {
|
||||
errors.push(`task ${task.id}: parent ${task.parentId} does not exist`);
|
||||
@@ -191,7 +193,7 @@ export class TaskState {
|
||||
}
|
||||
if (typeof draft.name !== "string" || !draft.name.trim()) errors.push(`${path}.name must not be empty`);
|
||||
if (typeof draft.name === "string" && /[\r\n]/.test(draft.name)) errors.push(`${path}.name must be a single line`);
|
||||
if (typeof draft.description !== "string" || !draft.description.trim()) {
|
||||
if (draft.description !== undefined && (typeof draft.description !== "string" || !draft.description.trim())) {
|
||||
errors.push(`${path}.description must not be empty`);
|
||||
}
|
||||
if (draft.status !== undefined && !VALID_STATUSES.has(draft.status)) {
|
||||
@@ -214,7 +216,7 @@ export class TaskState {
|
||||
working.push({
|
||||
id,
|
||||
name: typeof draft.name === "string" ? draft.name.trim() : "",
|
||||
description: typeof draft.description === "string" ? draft.description.trim() : "",
|
||||
...(typeof draft.description === "string" ? { description: draft.description.trim() } : {}),
|
||||
status: draft.status ?? "pending",
|
||||
parentId,
|
||||
});
|
||||
@@ -233,6 +235,29 @@ export class TaskState {
|
||||
return this.get(addedIds);
|
||||
}
|
||||
|
||||
set(mode: "append" | "replace", drafts: AddTaskDraft[], force?: boolean): Task[] {
|
||||
if (mode === "append") {
|
||||
if (force !== undefined) throw new TaskValidationError(["force is allowed only in replace mode"]);
|
||||
return this.add(drafts);
|
||||
}
|
||||
if (mode !== "replace") throw new TaskValidationError(["mode must be append or replace"]);
|
||||
if (!Array.isArray(drafts)) throw new TaskValidationError(["tasks must be an array"]);
|
||||
|
||||
const errors: string[] = [];
|
||||
if (this.records.some((task) => task.status !== "done") && force !== true) {
|
||||
errors.push("replace requires force when unfinished tasks exist");
|
||||
}
|
||||
drafts.forEach((draft, index) => {
|
||||
if (draft?.parentId !== undefined) errors.push(`tasks[${index}].parentId is not allowed in replace mode`);
|
||||
});
|
||||
if (errors.length > 0) throw new TaskValidationError(errors);
|
||||
|
||||
const replacement = new TaskState();
|
||||
const affected = drafts.length === 0 ? [] : replacement.add(drafts);
|
||||
this.restore(replacement.snapshot());
|
||||
return affected;
|
||||
}
|
||||
|
||||
update(patches: TaskPatch[]): Task[] {
|
||||
const errors: string[] = [];
|
||||
if (!Array.isArray(patches) || patches.length === 0) throw new TaskValidationError(["tasks must not be empty"]);
|
||||
|
||||
Reference in New Issue
Block a user