@tomkapa/tayto 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6NQOFUIQ.js → chunk-FUNYPBWJ.js} +23 -1
- package/dist/chunk-FUNYPBWJ.js.map +1 -0
- package/dist/index.js +252 -33
- package/dist/index.js.map +1 -1
- package/dist/migrations/004_remove_check_constraints.sql +63 -0
- package/dist/{tui-JNZRBEIQ.js → tui-5JJH67YY.js} +888 -294
- package/dist/tui-5JJH67YY.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-6NQOFUIQ.js.map +0 -1
- package/dist/tui-JNZRBEIQ.js.map +0 -1
|
@@ -98,10 +98,29 @@ var TaskStatus = {
|
|
|
98
98
|
Cancelled: "cancelled"
|
|
99
99
|
};
|
|
100
100
|
var TaskType = {
|
|
101
|
+
Epic: "epic",
|
|
101
102
|
Story: "story",
|
|
102
103
|
TechDebt: "tech-debt",
|
|
103
104
|
Bug: "bug"
|
|
104
105
|
};
|
|
106
|
+
var TaskLevel = {
|
|
107
|
+
Epic: 1,
|
|
108
|
+
Work: 2
|
|
109
|
+
};
|
|
110
|
+
var TYPE_TO_LEVEL = {
|
|
111
|
+
[TaskType.Epic]: TaskLevel.Epic,
|
|
112
|
+
[TaskType.Story]: TaskLevel.Work,
|
|
113
|
+
[TaskType.TechDebt]: TaskLevel.Work,
|
|
114
|
+
[TaskType.Bug]: TaskLevel.Work
|
|
115
|
+
};
|
|
116
|
+
function getTaskLevel(type) {
|
|
117
|
+
return TYPE_TO_LEVEL[type] ?? TaskLevel.Work;
|
|
118
|
+
}
|
|
119
|
+
var WORK_TYPES = /* @__PURE__ */ new Set([
|
|
120
|
+
TaskType.Story,
|
|
121
|
+
TaskType.TechDebt,
|
|
122
|
+
TaskType.Bug
|
|
123
|
+
]);
|
|
105
124
|
var DependencyType = {
|
|
106
125
|
Blocks: "blocks",
|
|
107
126
|
RelatesTo: "relates-to",
|
|
@@ -124,10 +143,13 @@ export {
|
|
|
124
143
|
logger,
|
|
125
144
|
TaskStatus,
|
|
126
145
|
TaskType,
|
|
146
|
+
TaskLevel,
|
|
147
|
+
getTaskLevel,
|
|
148
|
+
WORK_TYPES,
|
|
127
149
|
DependencyType,
|
|
128
150
|
UIDependencyType,
|
|
129
151
|
RANK_GAP,
|
|
130
152
|
TERMINAL_STATUSES,
|
|
131
153
|
isTerminalStatus
|
|
132
154
|
};
|
|
133
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-FUNYPBWJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/logging/logger.ts","../src/types/enums.ts"],"sourcesContent":["import { appendFileSync, readdirSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { trace, type Span, SpanStatusCode } from '@opentelemetry/api';\n\nconst tracer = trace.getTracer('task');\nconst LOG_RETENTION_DAYS = 7;\n\nexport interface LogAttributes {\n [key: string]: string | number | boolean;\n}\n\ntype LogLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction formatTimestamp(): string {\n return new Date().toISOString();\n}\n\nfunction formatAttrs(attrs?: LogAttributes): string {\n if (!attrs || Object.keys(attrs).length === 0) return '';\n return ' ' + JSON.stringify(attrs);\n}\n\nclass Logger {\n private logFilePath: string | null = null;\n\n init(logDir: string): void {\n const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD\n this.logFilePath = join(logDir, `task-${date}.log`);\n this.pruneOldLogs(logDir);\n }\n\n info(message: string, attrs?: LogAttributes): void {\n this.write('INFO', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(message, attrs);\n }\n }\n\n warn(message: string, attrs?: LogAttributes): void {\n this.write('WARN', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`WARN: ${message}`, attrs);\n }\n }\n\n error(message: string, error?: unknown, attrs?: LogAttributes): void {\n const errorDetail = error instanceof Error ? ` | ${error.stack ?? error.message}` : '';\n this.write('ERROR', `${message}${errorDetail}`, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`ERROR: ${message}`, attrs);\n if (error instanceof Error) {\n span.recordException(error);\n }\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n }\n }\n\n startSpan<T>(name: string, fn: (span: Span) => T): T {\n return tracer.startActiveSpan(name, (span) => {\n try {\n const result = fn(span);\n span.end();\n return result;\n } catch (e) {\n if (e instanceof Error) {\n span.recordException(e);\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.end();\n throw e;\n }\n });\n }\n\n private write(level: LogLevel, message: string, attrs?: LogAttributes): void {\n if (!this.logFilePath) return;\n const line = `${formatTimestamp()} [${level}] ${message}${formatAttrs(attrs)}\\n`;\n try {\n appendFileSync(this.logFilePath, line);\n } catch {\n // Swallowing here is intentional: logging must never crash the app.\n // If the log file is unwritable, the OTel span still captures the event.\n }\n }\n\n private pruneOldLogs(logDir: string): void {\n try {\n const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const files = readdirSync(logDir).filter((f) => f.startsWith('task-') && f.endsWith('.log'));\n for (const file of files) {\n const dateStr = file.slice('task-'.length, -'.log'.length);\n const fileDate = new Date(dateStr).getTime();\n if (!isNaN(fileDate) && fileDate < cutoff) {\n unlinkSync(join(logDir, file));\n }\n }\n } catch {\n // Best-effort cleanup — don't crash if pruning fails\n }\n }\n}\n\nexport const logger = new Logger();\n","export const TaskStatus = {\n Backlog: 'backlog',\n Todo: 'todo',\n InProgress: 'in-progress',\n Review: 'review',\n Done: 'done',\n Cancelled: 'cancelled',\n} as const;\nexport type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];\n\nexport const TaskType = {\n Epic: 'epic',\n Story: 'story',\n TechDebt: 'tech-debt',\n Bug: 'bug',\n} as const;\nexport type TaskType = (typeof TaskType)[keyof typeof TaskType];\n\n/**\n * Task level derived from type.\n * Level 1: epics (grouping/planning layer)\n * Level 2: stories, tech-debt, bugs (execution layer)\n */\nexport const TaskLevel = {\n Epic: 1,\n Work: 2,\n} as const;\nexport type TaskLevel = (typeof TaskLevel)[keyof typeof TaskLevel];\n\nconst TYPE_TO_LEVEL: Record<string, TaskLevel> = {\n [TaskType.Epic]: TaskLevel.Epic,\n [TaskType.Story]: TaskLevel.Work,\n [TaskType.TechDebt]: TaskLevel.Work,\n [TaskType.Bug]: TaskLevel.Work,\n};\n\nexport function getTaskLevel(type: string): TaskLevel {\n return TYPE_TO_LEVEL[type] ?? TaskLevel.Work;\n}\n\n/** Types that belong to the work (level 2) execution layer. */\nexport const WORK_TYPES: ReadonlySet<string> = new Set([\n TaskType.Story,\n TaskType.TechDebt,\n TaskType.Bug,\n]);\n\n/** Types stored in the database. */\nexport const DependencyType = {\n Blocks: 'blocks',\n RelatesTo: 'relates-to',\n Duplicates: 'duplicates',\n} as const;\nexport type DependencyType = (typeof DependencyType)[keyof typeof DependencyType];\n\n/**\n * UI-level dependency types — includes BlockedBy which is a reverse-Blocks\n * relationship resolved before persisting to the database.\n */\nexport const UIDependencyType = {\n ...DependencyType,\n BlockedBy: 'blocked-by',\n} as const;\nexport type UIDependencyType = (typeof UIDependencyType)[keyof typeof UIDependencyType];\n\n/** Gap between consecutive rank values, used for insertion between neighbors. */\nexport const RANK_GAP = 1000.0;\n\n/** Statuses that represent terminal/completed task states. */\nexport const TERMINAL_STATUSES: ReadonlySet<string> = new Set([\n TaskStatus.Done,\n TaskStatus.Cancelled,\n]);\n\nexport function isTerminalStatus(status: string): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
DependencyType,
|
|
4
4
|
RANK_GAP,
|
|
5
5
|
TERMINAL_STATUSES,
|
|
6
|
+
TaskLevel,
|
|
6
7
|
TaskStatus,
|
|
7
8
|
TaskType,
|
|
8
9
|
UIDependencyType,
|
|
10
|
+
WORK_TYPES,
|
|
11
|
+
getTaskLevel,
|
|
9
12
|
isTerminalStatus,
|
|
10
13
|
logger
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FUNYPBWJ.js";
|
|
12
15
|
|
|
13
16
|
// src/config/index.ts
|
|
14
17
|
import { mkdirSync } from "fs";
|
|
@@ -305,6 +308,17 @@ var SqliteProjectRepository = class {
|
|
|
305
308
|
}
|
|
306
309
|
};
|
|
307
310
|
|
|
311
|
+
// src/utils/search-parser.ts
|
|
312
|
+
var ID_PREFIX_RE = /^id:(.+)$/;
|
|
313
|
+
function parseSearchQuery(raw) {
|
|
314
|
+
const trimmed = raw.trim();
|
|
315
|
+
const match = ID_PREFIX_RE.exec(trimmed);
|
|
316
|
+
if (match?.[1]) {
|
|
317
|
+
return { kind: "id", value: match[1] };
|
|
318
|
+
}
|
|
319
|
+
return { kind: "fts", query: trimmed };
|
|
320
|
+
}
|
|
321
|
+
|
|
308
322
|
// src/repository/task.repository.ts
|
|
309
323
|
var TERMINAL_STATUS_ARRAY = [...TERMINAL_STATUSES];
|
|
310
324
|
var TERMINAL_PLACEHOLDERS = TERMINAL_STATUS_ARRAY.map(() => "?").join(", ");
|
|
@@ -317,10 +331,11 @@ var SqliteTaskRepository = class {
|
|
|
317
331
|
return logger.startSpan("TaskRepository.insert", () => {
|
|
318
332
|
try {
|
|
319
333
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
320
|
-
const
|
|
334
|
+
const level = getTaskLevel(input.type);
|
|
335
|
+
const maxActiveResult = this.getMaxActiveRankByLevel(input.projectId, level);
|
|
321
336
|
if (!maxActiveResult.ok) return maxActiveResult;
|
|
322
337
|
const maxActiveRank = maxActiveResult.value;
|
|
323
|
-
const minTerminalResult = this.
|
|
338
|
+
const minTerminalResult = this.getMinTerminalRankByLevel(input.projectId, level);
|
|
324
339
|
if (!minTerminalResult.ok) return minTerminalResult;
|
|
325
340
|
const minTerminalRank = minTerminalResult.value;
|
|
326
341
|
let rank;
|
|
@@ -380,14 +395,31 @@ var SqliteTaskRepository = class {
|
|
|
380
395
|
conditions.push("type = ?");
|
|
381
396
|
params.push(filter.type);
|
|
382
397
|
}
|
|
398
|
+
if (filter.level !== void 0) {
|
|
399
|
+
const typesForLevel = this.getTypesForLevel(filter.level);
|
|
400
|
+
const placeholders = typesForLevel.map(() => "?").join(", ");
|
|
401
|
+
conditions.push(`type IN (${placeholders})`);
|
|
402
|
+
params.push(...typesForLevel);
|
|
403
|
+
}
|
|
383
404
|
if (filter.parentId) {
|
|
384
405
|
conditions.push("parent_id = ?");
|
|
385
406
|
params.push(filter.parentId);
|
|
386
407
|
}
|
|
408
|
+
if (filter.parentIds && filter.parentIds.length > 0) {
|
|
409
|
+
const placeholders = filter.parentIds.map(() => "?").join(", ");
|
|
410
|
+
conditions.push(`parent_id IN (${placeholders})`);
|
|
411
|
+
params.push(...filter.parentIds);
|
|
412
|
+
}
|
|
387
413
|
if (filter.search) {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
414
|
+
const parsed = parseSearchQuery(filter.search);
|
|
415
|
+
if (parsed.kind === "id") {
|
|
416
|
+
conditions.push(`id LIKE ?`);
|
|
417
|
+
params.push(`%${parsed.value}%`);
|
|
418
|
+
} else {
|
|
419
|
+
const ftsQuery = parsed.query.split(/\s+/).map((term) => `"${term.replace(/"/g, '""')}"*`).join(" ");
|
|
420
|
+
conditions.push(`id IN (SELECT id FROM tasks_fts WHERE tasks_fts MATCH ?)`);
|
|
421
|
+
params.push(ftsQuery);
|
|
422
|
+
}
|
|
391
423
|
}
|
|
392
424
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
393
425
|
const sql = `SELECT * FROM tasks ${where} ORDER BY rank ASC`;
|
|
@@ -528,10 +560,92 @@ _${now}_
|
|
|
528
560
|
return err(new AppError("DB_ERROR", "Failed to get ranked tasks", e));
|
|
529
561
|
}
|
|
530
562
|
}
|
|
563
|
+
getTypesForLevel(level) {
|
|
564
|
+
if (level === 1) return ["epic"];
|
|
565
|
+
return [...WORK_TYPES];
|
|
566
|
+
}
|
|
567
|
+
getMaxRankByLevel(projectId, level) {
|
|
568
|
+
try {
|
|
569
|
+
const types = this.getTypesForLevel(level);
|
|
570
|
+
const placeholders = types.map(() => "?").join(", ");
|
|
571
|
+
const row = this.db.prepare(
|
|
572
|
+
`SELECT MAX(rank) as max_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${placeholders})`
|
|
573
|
+
).get(projectId, ...types);
|
|
574
|
+
return ok(row?.max_rank ?? 0);
|
|
575
|
+
} catch (e) {
|
|
576
|
+
return err(new AppError("DB_ERROR", "Failed to get max rank by level", e));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
getMaxActiveRankByLevel(projectId, level) {
|
|
580
|
+
try {
|
|
581
|
+
const types = this.getTypesForLevel(level);
|
|
582
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
583
|
+
const row = this.db.prepare(
|
|
584
|
+
`SELECT MAX(rank) as max_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders}) AND status NOT IN (${TERMINAL_PLACEHOLDERS})`
|
|
585
|
+
).get(projectId, ...types, ...TERMINAL_STATUS_ARRAY);
|
|
586
|
+
return ok(row?.max_rank ?? 0);
|
|
587
|
+
} catch (e) {
|
|
588
|
+
return err(new AppError("DB_ERROR", "Failed to get max active rank by level", e));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
getMinTerminalRankByLevel(projectId, level) {
|
|
592
|
+
try {
|
|
593
|
+
const types = this.getTypesForLevel(level);
|
|
594
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
595
|
+
const row = this.db.prepare(
|
|
596
|
+
`SELECT MIN(rank) as min_rank FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders}) AND status IN (${TERMINAL_PLACEHOLDERS})`
|
|
597
|
+
).get(projectId, ...types, ...TERMINAL_STATUS_ARRAY);
|
|
598
|
+
return ok(row?.min_rank ?? null);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
return err(new AppError("DB_ERROR", "Failed to get min terminal rank by level", e));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
getRankedTasksByLevel(projectId, level, status) {
|
|
604
|
+
try {
|
|
605
|
+
const types = this.getTypesForLevel(level);
|
|
606
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
607
|
+
let sql;
|
|
608
|
+
let params;
|
|
609
|
+
if (status) {
|
|
610
|
+
sql = `SELECT * FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders}) AND status = ? ORDER BY rank ASC`;
|
|
611
|
+
params = [projectId, ...types, status];
|
|
612
|
+
} else {
|
|
613
|
+
sql = `SELECT * FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders}) ORDER BY rank ASC`;
|
|
614
|
+
params = [projectId, ...types];
|
|
615
|
+
}
|
|
616
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
617
|
+
return ok(rows.map(rowToTask));
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return err(new AppError("DB_ERROR", "Failed to get ranked tasks by level", e));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
getRankedNonTerminalTasksByLevel(projectId, level) {
|
|
623
|
+
try {
|
|
624
|
+
const types = this.getTypesForLevel(level);
|
|
625
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
626
|
+
const sql = `SELECT * FROM tasks WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders}) AND status NOT IN (${TERMINAL_PLACEHOLDERS}) ORDER BY rank ASC`;
|
|
627
|
+
const rows = this.db.prepare(sql).all(projectId, ...types, ...TERMINAL_STATUS_ARRAY);
|
|
628
|
+
return ok(rows.map(rowToTask));
|
|
629
|
+
} catch (e) {
|
|
630
|
+
return err(new AppError("DB_ERROR", "Failed to get ranked non-terminal tasks by level", e));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
531
633
|
search(query, projectId) {
|
|
532
634
|
return logger.startSpan("TaskRepository.search", () => {
|
|
533
635
|
try {
|
|
534
|
-
const
|
|
636
|
+
const parsed = parseSearchQuery(query);
|
|
637
|
+
if (parsed.kind === "id") {
|
|
638
|
+
const conditions = ["t.id LIKE ?", "t.deleted_at IS NULL"];
|
|
639
|
+
const params2 = [`%${parsed.value}%`];
|
|
640
|
+
if (projectId) {
|
|
641
|
+
conditions.push("t.project_id = ?");
|
|
642
|
+
params2.push(projectId);
|
|
643
|
+
}
|
|
644
|
+
const sql2 = `SELECT t.*, 0 AS fts_rank FROM tasks t WHERE ${conditions.join(" AND ")} ORDER BY t.rank ASC`;
|
|
645
|
+
const rows2 = this.db.prepare(sql2).all(...params2);
|
|
646
|
+
return ok(rows2.map((row) => ({ task: rowToTask(row), rank: row.fts_rank })));
|
|
647
|
+
}
|
|
648
|
+
const ftsQuery = parsed.query.split(/\s+/).map((term) => `"${term.replace(/"/g, '""')}"*`).join(" ");
|
|
535
649
|
let sql;
|
|
536
650
|
let params;
|
|
537
651
|
if (projectId) {
|
|
@@ -877,7 +991,10 @@ var TaskFilterSchema = z2.object({
|
|
|
877
991
|
projectId: z2.string().optional(),
|
|
878
992
|
status: z2.enum(taskStatusValues).optional(),
|
|
879
993
|
type: z2.enum(taskTypeValues).optional(),
|
|
994
|
+
level: z2.number().int().min(1).max(2).optional(),
|
|
880
995
|
parentId: z2.string().optional(),
|
|
996
|
+
/** Multi-select filter: show tasks whose parentId is in this list. */
|
|
997
|
+
parentIds: z2.array(z2.string()).optional(),
|
|
881
998
|
search: z2.string().optional()
|
|
882
999
|
});
|
|
883
1000
|
var RerankTaskSchema = z2.object({
|
|
@@ -906,12 +1023,19 @@ var TaskServiceImpl = class {
|
|
|
906
1023
|
const projectRef = parsed.data.projectId ?? projectIdOrName;
|
|
907
1024
|
const projectResult = this.projectService.resolveProject(projectRef);
|
|
908
1025
|
if (!projectResult.ok) return projectResult;
|
|
1026
|
+
const taskLevel = getTaskLevel(parsed.data.type);
|
|
1027
|
+
if (taskLevel === TaskLevel.Epic && parsed.data.parentId) {
|
|
1028
|
+
return err(new AppError("VALIDATION", "Epic tasks cannot have a parent"));
|
|
1029
|
+
}
|
|
909
1030
|
if (parsed.data.parentId) {
|
|
910
1031
|
const parentResult = this.repo.findById(parsed.data.parentId);
|
|
911
1032
|
if (!parentResult.ok) return parentResult;
|
|
912
1033
|
if (!parentResult.value) {
|
|
913
1034
|
return err(new AppError("NOT_FOUND", `Parent task not found: ${parsed.data.parentId}`));
|
|
914
1035
|
}
|
|
1036
|
+
if (getTaskLevel(parentResult.value.type) !== TaskLevel.Epic) {
|
|
1037
|
+
return err(new AppError("VALIDATION", "Tasks can only be children of epic-level tasks"));
|
|
1038
|
+
}
|
|
915
1039
|
}
|
|
916
1040
|
const project = projectResult.value;
|
|
917
1041
|
const taskIdResult = this.projectService.nextTaskId(project);
|
|
@@ -970,28 +1094,79 @@ var TaskServiceImpl = class {
|
|
|
970
1094
|
if (!blockersResult.ok) return blockersResult;
|
|
971
1095
|
const hasNonTerminalBlocker = blockersResult.value.some((b) => !isTerminalStatus(b.status));
|
|
972
1096
|
if (hasNonTerminalBlocker) {
|
|
973
|
-
return err(
|
|
974
|
-
new AppError("VALIDATION", "Task is blocked by unfinished dependencies")
|
|
975
|
-
);
|
|
1097
|
+
return err(new AppError("VALIDATION", "Task is blocked by unfinished dependencies"));
|
|
976
1098
|
}
|
|
977
1099
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1100
|
+
const existingResult = this.repo.findById(id);
|
|
1101
|
+
if (!existingResult.ok) return existingResult;
|
|
1102
|
+
if (!existingResult.value) {
|
|
1103
|
+
return err(new AppError("NOT_FOUND", `Task not found: ${id}`));
|
|
1104
|
+
}
|
|
1105
|
+
const existing = existingResult.value;
|
|
1106
|
+
if (parsed.data.type) {
|
|
1107
|
+
const newLevel = getTaskLevel(parsed.data.type);
|
|
1108
|
+
const oldLevel = getTaskLevel(existing.type);
|
|
1109
|
+
if (newLevel !== oldLevel) {
|
|
1110
|
+
if (oldLevel === TaskLevel.Epic) {
|
|
1111
|
+
const childrenResult = this.repo.findMany({
|
|
1112
|
+
projectId: existing.projectId,
|
|
1113
|
+
parentId: id
|
|
1114
|
+
});
|
|
1115
|
+
if (childrenResult.ok && childrenResult.value.length > 0) {
|
|
1116
|
+
return err(
|
|
1117
|
+
new AppError(
|
|
1118
|
+
"VALIDATION",
|
|
1119
|
+
"Cannot change type from epic: task has children. Remove children first."
|
|
1120
|
+
)
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (newLevel === TaskLevel.Epic && existing.parentId) {
|
|
1125
|
+
return err(new AppError("VALIDATION", "Cannot change type to epic: task has a parent"));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (parsed.data.parentId !== void 0) {
|
|
1130
|
+
const effectiveType = parsed.data.type ?? existing.type;
|
|
1131
|
+
const effectiveLevel = getTaskLevel(effectiveType);
|
|
1132
|
+
if (effectiveLevel === TaskLevel.Epic && parsed.data.parentId) {
|
|
1133
|
+
return err(new AppError("VALIDATION", "Epic tasks cannot have a parent"));
|
|
983
1134
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1135
|
+
if (parsed.data.parentId) {
|
|
1136
|
+
const parentResult = this.repo.findById(parsed.data.parentId);
|
|
1137
|
+
if (!parentResult.ok) return parentResult;
|
|
1138
|
+
if (!parentResult.value) {
|
|
1139
|
+
return err(new AppError("NOT_FOUND", `Parent task not found: ${parsed.data.parentId}`));
|
|
1140
|
+
}
|
|
1141
|
+
if (getTaskLevel(parentResult.value.type) !== TaskLevel.Epic) {
|
|
1142
|
+
return err(
|
|
1143
|
+
new AppError("VALIDATION", "Tasks can only be children of epic-level tasks")
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (parsed.data.status && isTerminalStatus(parsed.data.status)) {
|
|
1149
|
+
const effectiveType = parsed.data.type ?? existing.type;
|
|
1150
|
+
const level = getTaskLevel(effectiveType);
|
|
1151
|
+
const updateResult2 = this.repo.update(id, parsed.data);
|
|
1152
|
+
if (!updateResult2.ok) return updateResult2;
|
|
987
1153
|
if (!isTerminalStatus(existing.status)) {
|
|
988
|
-
const maxRankResult = this.repo.
|
|
1154
|
+
const maxRankResult = this.repo.getMaxRankByLevel(existing.projectId, level);
|
|
989
1155
|
if (!maxRankResult.ok) return maxRankResult;
|
|
990
|
-
|
|
1156
|
+
const rerankResult = this.repo.rerank(id, maxRankResult.value + RANK_GAP);
|
|
1157
|
+
if (!rerankResult.ok) return rerankResult;
|
|
1158
|
+
this.propagateParentStatus(existing);
|
|
1159
|
+
return rerankResult;
|
|
991
1160
|
}
|
|
992
|
-
|
|
1161
|
+
this.propagateParentStatus(existing);
|
|
1162
|
+
return updateResult2;
|
|
993
1163
|
}
|
|
994
|
-
|
|
1164
|
+
const updateResult = this.repo.update(id, parsed.data);
|
|
1165
|
+
if (!updateResult.ok) return updateResult;
|
|
1166
|
+
if (parsed.data.status && parsed.data.status !== existing.status) {
|
|
1167
|
+
this.propagateParentStatus(existing);
|
|
1168
|
+
}
|
|
1169
|
+
return updateResult;
|
|
995
1170
|
});
|
|
996
1171
|
}
|
|
997
1172
|
deleteTask(id) {
|
|
@@ -1005,6 +1180,9 @@ var TaskServiceImpl = class {
|
|
|
1005
1180
|
return err(new AppError("NOT_FOUND", `Parent task not found: ${parentId}`));
|
|
1006
1181
|
}
|
|
1007
1182
|
const parent = parentResult.value;
|
|
1183
|
+
if (getTaskLevel(parent.type) !== TaskLevel.Epic) {
|
|
1184
|
+
return err(new AppError("VALIDATION", "Breakdown parent must be an epic-level task"));
|
|
1185
|
+
}
|
|
1008
1186
|
const projectResult = this.projectService.resolveProject(parent.projectId);
|
|
1009
1187
|
if (!projectResult.ok) return projectResult;
|
|
1010
1188
|
const project = projectResult.value;
|
|
@@ -1014,6 +1192,9 @@ var TaskServiceImpl = class {
|
|
|
1014
1192
|
if (!parsed.success) {
|
|
1015
1193
|
return err(new AppError("VALIDATION", `Invalid subtask: ${parsed.error.message}`));
|
|
1016
1194
|
}
|
|
1195
|
+
if (getTaskLevel(parsed.data.type) === TaskLevel.Epic) {
|
|
1196
|
+
return err(new AppError("VALIDATION", `Subtask "${parsed.data.name}" cannot be an epic`));
|
|
1197
|
+
}
|
|
1017
1198
|
const taskIdResult = this.projectService.nextTaskId(project);
|
|
1018
1199
|
if (!taskIdResult.ok) return taskIdResult;
|
|
1019
1200
|
const result = this.repo.insert(taskIdResult.value, {
|
|
@@ -1067,18 +1248,19 @@ var TaskServiceImpl = class {
|
|
|
1067
1248
|
)
|
|
1068
1249
|
);
|
|
1069
1250
|
}
|
|
1251
|
+
const taskLevel = getTaskLevel(task.type);
|
|
1070
1252
|
const projectRef = projectIdOrName ?? task.projectId;
|
|
1071
1253
|
const projectResult = this.projectService.resolveProject(projectRef);
|
|
1072
1254
|
if (!projectResult.ok) return projectResult;
|
|
1073
1255
|
const projectId = projectResult.value.id;
|
|
1074
|
-
const rankedResult = this.repo.
|
|
1256
|
+
const rankedResult = this.repo.getRankedNonTerminalTasksByLevel(projectId, taskLevel);
|
|
1075
1257
|
if (!rankedResult.ok) return rankedResult;
|
|
1076
1258
|
const ranked = rankedResult.value.filter((t) => t.id !== taskId);
|
|
1077
1259
|
let newRank;
|
|
1078
1260
|
if (afterId) {
|
|
1079
1261
|
const anchor = ranked.find((t) => t.id === afterId);
|
|
1080
1262
|
if (!anchor) {
|
|
1081
|
-
return err(new AppError("NOT_FOUND", `Anchor task not found
|
|
1263
|
+
return err(new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${afterId}`));
|
|
1082
1264
|
}
|
|
1083
1265
|
const anchorIndex = ranked.indexOf(anchor);
|
|
1084
1266
|
const next = ranked[anchorIndex + 1];
|
|
@@ -1086,7 +1268,7 @@ var TaskServiceImpl = class {
|
|
|
1086
1268
|
} else if (beforeId) {
|
|
1087
1269
|
const anchor = ranked.find((t) => t.id === beforeId);
|
|
1088
1270
|
if (!anchor) {
|
|
1089
|
-
return err(new AppError("NOT_FOUND", `Anchor task not found
|
|
1271
|
+
return err(new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${beforeId}`));
|
|
1090
1272
|
}
|
|
1091
1273
|
const anchorIndex = ranked.indexOf(anchor);
|
|
1092
1274
|
const prev = ranked[anchorIndex - 1];
|
|
@@ -1155,6 +1337,42 @@ var TaskServiceImpl = class {
|
|
|
1155
1337
|
return this.repo.search(query, projectId);
|
|
1156
1338
|
});
|
|
1157
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Auto-propagate status to the parent task after a child status change.
|
|
1342
|
+
* - If a child moves to in-progress and parent is backlog/todo → parent becomes in-progress.
|
|
1343
|
+
* - If all children are terminal (done/cancelled) → parent becomes done.
|
|
1344
|
+
*/
|
|
1345
|
+
propagateParentStatus(child) {
|
|
1346
|
+
if (!child.parentId) return;
|
|
1347
|
+
const parentResult = this.repo.findById(child.parentId);
|
|
1348
|
+
if (!parentResult.ok || !parentResult.value) return;
|
|
1349
|
+
const parent = parentResult.value;
|
|
1350
|
+
const updatedChildResult = this.repo.findById(child.id);
|
|
1351
|
+
if (!updatedChildResult.ok || !updatedChildResult.value) return;
|
|
1352
|
+
const updatedChild = updatedChildResult.value;
|
|
1353
|
+
if (updatedChild.status === TaskStatus.InProgress && (parent.status === TaskStatus.Backlog || parent.status === TaskStatus.Todo)) {
|
|
1354
|
+
this.repo.update(parent.id, { status: TaskStatus.InProgress });
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (isTerminalStatus(updatedChild.status)) {
|
|
1358
|
+
const siblingsResult = this.repo.findMany({
|
|
1359
|
+
projectId: parent.projectId,
|
|
1360
|
+
parentId: parent.id
|
|
1361
|
+
});
|
|
1362
|
+
if (!siblingsResult.ok) return;
|
|
1363
|
+
const allTerminal = siblingsResult.value.every((s) => isTerminalStatus(s.status));
|
|
1364
|
+
if (allTerminal && !isTerminalStatus(parent.status)) {
|
|
1365
|
+
const maxRankResult = this.repo.getMaxRankByLevel(
|
|
1366
|
+
parent.projectId,
|
|
1367
|
+
getTaskLevel(parent.type)
|
|
1368
|
+
);
|
|
1369
|
+
this.repo.update(parent.id, { status: TaskStatus.Done });
|
|
1370
|
+
if (maxRankResult.ok) {
|
|
1371
|
+
this.repo.rerank(parent.id, maxRankResult.value + RANK_GAP);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1158
1376
|
};
|
|
1159
1377
|
|
|
1160
1378
|
// src/types/dependency.ts
|
|
@@ -1568,7 +1786,7 @@ var PortabilityServiceImpl = class {
|
|
|
1568
1786
|
};
|
|
1569
1787
|
|
|
1570
1788
|
// src/cli/container.ts
|
|
1571
|
-
function createContainer(db) {
|
|
1789
|
+
function createContainer(db, dbPath) {
|
|
1572
1790
|
const projectRepo = new SqliteProjectRepository(db);
|
|
1573
1791
|
const taskRepo = new SqliteTaskRepository(db);
|
|
1574
1792
|
const depRepo = new SqliteDependencyRepository(db);
|
|
@@ -1580,7 +1798,7 @@ function createContainer(db) {
|
|
|
1580
1798
|
dependencyService,
|
|
1581
1799
|
projectService
|
|
1582
1800
|
);
|
|
1583
|
-
return { projectService, taskService, dependencyService, portabilityService };
|
|
1801
|
+
return { dbPath, projectService, taskService, dependencyService, portabilityService };
|
|
1584
1802
|
}
|
|
1585
1803
|
|
|
1586
1804
|
// src/cli/index.ts
|
|
@@ -1677,7 +1895,7 @@ function registerProjectSetDefault(parent, container) {
|
|
|
1677
1895
|
|
|
1678
1896
|
// src/cli/commands/task/create.ts
|
|
1679
1897
|
function registerTaskCreate(parent, container) {
|
|
1680
|
-
parent.command("create").description("Create a new task (appended to bottom of backlog)").requiredOption("-n, --name <name>", "Task name").option("-p, --project <project>", "Project id or name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: story, tech-debt, bug", "story").option("-s, --status <status>", "Task status", "backlog").option("--parent <parentId>", "Parent task id for subtask").option("--technical-notes <notes>", "Technical notes (markdown)").option("--additional-requirements <requirements>", "Additional requirements (markdown)").option("--depends-on <ids...>", "Task ids this task depends on (blocks relationship)").action(
|
|
1898
|
+
parent.command("create").description("Create a new task (appended to bottom of backlog)").requiredOption("-n, --name <name>", "Task name").option("-p, --project <project>", "Project id or name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: epic, story, tech-debt, bug", "story").option("-s, --status <status>", "Task status", "backlog").option("--parent <parentId>", "Parent task id for subtask").option("--technical-notes <notes>", "Technical notes (markdown)").option("--additional-requirements <requirements>", "Additional requirements (markdown)").option("--depends-on <ids...>", "Task ids this task depends on (blocks relationship)").action(
|
|
1681
1899
|
(opts) => {
|
|
1682
1900
|
const result = container.taskService.createTask(
|
|
1683
1901
|
{
|
|
@@ -1699,12 +1917,13 @@ function registerTaskCreate(parent, container) {
|
|
|
1699
1917
|
|
|
1700
1918
|
// src/cli/commands/task/list.ts
|
|
1701
1919
|
function registerTaskList(parent, container) {
|
|
1702
|
-
parent.command("list").description("List tasks in rank order (defaults to backlog)").option("-p, --project <project>", "Filter by project id or name").option("-s, --status <status>", "Filter by status (default: backlog)").option("-t, --type <type>", "Filter by type").option("--parent <parentId>", "Filter by parent task id").option("--search <text>", "Search in name, description, and notes").action(
|
|
1920
|
+
parent.command("list").description("List tasks in rank order (defaults to level 2 backlog tasks)").option("-p, --project <project>", "Filter by project id or name").option("-s, --status <status>", "Filter by status (default: backlog)").option("-t, --type <type>", "Filter by type (epic, story, tech-debt, bug)").option("-l, --level <level>", "Filter by level (1=epic, 2=work). Default: 2").option("--parent <parentId>", "Filter by parent task id").option("--search <text>", "Search in name, description, and notes").action(
|
|
1703
1921
|
(opts) => {
|
|
1704
1922
|
const result = container.taskService.listTasks({
|
|
1705
1923
|
projectId: opts.project,
|
|
1706
1924
|
status: opts.status ?? "backlog",
|
|
1707
1925
|
type: opts.type,
|
|
1926
|
+
level: opts.level ? parseInt(opts.level, 10) : void 0,
|
|
1708
1927
|
parentId: opts.parent,
|
|
1709
1928
|
search: opts.search
|
|
1710
1929
|
});
|
|
@@ -1723,7 +1942,7 @@ function registerTaskShow(parent, container) {
|
|
|
1723
1942
|
|
|
1724
1943
|
// src/cli/commands/task/update.ts
|
|
1725
1944
|
function registerTaskUpdate(parent, container) {
|
|
1726
|
-
parent.command("update <id>").description("Update a task").option("-n, --name <name>", "Task name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: story, tech-debt, bug").option("-s, --status <status>", "Task status").option("--parent <parentId>", "Parent task id").option("--technical-notes <notes>", "Replace technical notes").option("--additional-requirements <requirements>", "Replace additional requirements").option("--append-notes <notes>", "Append to technical notes").option("--append-requirements <requirements>", "Append to additional requirements").action(
|
|
1945
|
+
parent.command("update <id>").description("Update a task").option("-n, --name <name>", "Task name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: epic, story, tech-debt, bug").option("-s, --status <status>", "Task status").option("--parent <parentId>", "Parent task id").option("--technical-notes <notes>", "Replace technical notes").option("--additional-requirements <requirements>", "Replace additional requirements").option("--append-notes <notes>", "Append to technical notes").option("--append-requirements <requirements>", "Append to additional requirements").action(
|
|
1727
1946
|
(id, opts) => {
|
|
1728
1947
|
const result = container.taskService.updateTask(id, {
|
|
1729
1948
|
name: opts.name,
|
|
@@ -1931,7 +2150,7 @@ function buildCLI(container) {
|
|
|
1931
2150
|
registerDepList(dep, container);
|
|
1932
2151
|
registerDepGraph(dep, container);
|
|
1933
2152
|
program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
|
|
1934
|
-
const { launchTUI } = await import("./tui-
|
|
2153
|
+
const { launchTUI } = await import("./tui-5JJH67YY.js");
|
|
1935
2154
|
await launchTUI(container, opts.project);
|
|
1936
2155
|
});
|
|
1937
2156
|
return program;
|
|
@@ -1944,10 +2163,10 @@ async function main() {
|
|
|
1944
2163
|
initTelemetry(config);
|
|
1945
2164
|
const db = createDatabase(config.dbPath);
|
|
1946
2165
|
runMigrations(db);
|
|
1947
|
-
const container = createContainer(db);
|
|
2166
|
+
const container = createContainer(db, config.dbPath);
|
|
1948
2167
|
const args = process.argv.slice(2);
|
|
1949
2168
|
if (args.length === 0) {
|
|
1950
|
-
const { launchTUI } = await import("./tui-
|
|
2169
|
+
const { launchTUI } = await import("./tui-5JJH67YY.js");
|
|
1951
2170
|
await launchTUI(container);
|
|
1952
2171
|
} else {
|
|
1953
2172
|
const program = buildCLI(container);
|