@tomkapa/tayto 0.3.1 → 0.3.2
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-FUNYPBWJ.js → chunk-STYT4TGJ.js} +6 -1
- package/dist/{chunk-FUNYPBWJ.js.map → chunk-STYT4TGJ.js.map} +1 -1
- package/dist/index.js +244 -81
- package/dist/index.js.map +1 -1
- package/dist/{tui-5JJH67YY.js → tui-JEP3F4JS.js} +57 -4
- package/dist/tui-JEP3F4JS.js.map +1 -0
- package/package.json +1 -1
- package/dist/tui-5JJH67YY.js.map +0 -1
|
@@ -131,6 +131,10 @@ var UIDependencyType = {
|
|
|
131
131
|
BlockedBy: "blocked-by"
|
|
132
132
|
};
|
|
133
133
|
var RANK_GAP = 1e3;
|
|
134
|
+
function midpoint(a, b) {
|
|
135
|
+
const m = (a + b) / 2;
|
|
136
|
+
return m > a && m < b ? m : null;
|
|
137
|
+
}
|
|
134
138
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
135
139
|
TaskStatus.Done,
|
|
136
140
|
TaskStatus.Cancelled
|
|
@@ -149,7 +153,8 @@ export {
|
|
|
149
153
|
DependencyType,
|
|
150
154
|
UIDependencyType,
|
|
151
155
|
RANK_GAP,
|
|
156
|
+
midpoint,
|
|
152
157
|
TERMINAL_STATUSES,
|
|
153
158
|
isTerminalStatus
|
|
154
159
|
};
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
160
|
+
//# sourceMappingURL=chunk-STYT4TGJ.js.map
|
|
@@ -1 +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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
|
|
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/**\n * Collapse-safe midpoint between two rank values. Returns `null` when\n * IEEE 754 double precision cannot represent a strictly-between value —\n * callers use this as a signal to rebalance the rank grid and retry.\n * Returning a number unconditionally would silently collide with an\n * endpoint and corrupt the task ordering.\n */\nexport function midpoint(a: number, b: number): number | null {\n const m = (a + b) / 2;\n return m > a && m < b ? m : null;\n}\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
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
WORK_TYPES,
|
|
11
11
|
getTaskLevel,
|
|
12
12
|
isTerminalStatus,
|
|
13
|
-
logger
|
|
14
|
-
|
|
13
|
+
logger,
|
|
14
|
+
midpoint
|
|
15
|
+
} from "./chunk-STYT4TGJ.js";
|
|
15
16
|
|
|
16
17
|
// src/config/index.ts
|
|
17
18
|
import { mkdirSync } from "fs";
|
|
@@ -332,18 +333,9 @@ var SqliteTaskRepository = class {
|
|
|
332
333
|
try {
|
|
333
334
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
334
335
|
const level = getTaskLevel(input.type);
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
337
|
-
const
|
|
338
|
-
const minTerminalResult = this.getMinTerminalRankByLevel(input.projectId, level);
|
|
339
|
-
if (!minTerminalResult.ok) return minTerminalResult;
|
|
340
|
-
const minTerminalRank = minTerminalResult.value;
|
|
341
|
-
let rank;
|
|
342
|
-
if (minTerminalRank !== null && minTerminalRank > maxActiveRank) {
|
|
343
|
-
rank = maxActiveRank > 0 ? (maxActiveRank + minTerminalRank) / 2 : minTerminalRank - RANK_GAP;
|
|
344
|
-
} else {
|
|
345
|
-
rank = maxActiveRank + RANK_GAP;
|
|
346
|
-
}
|
|
336
|
+
const rankResult = this.computeInsertRank(input.projectId, level);
|
|
337
|
+
if (!rankResult.ok) return rankResult;
|
|
338
|
+
const rank = rankResult.value;
|
|
347
339
|
this.db.prepare(
|
|
348
340
|
`INSERT INTO tasks (id, project_id, parent_id, name, description, type, status, rank, technical_notes, additional_requirements, created_at, updated_at)
|
|
349
341
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
@@ -630,6 +622,104 @@ _${now}_
|
|
|
630
622
|
return err(new AppError("DB_ERROR", "Failed to get ranked non-terminal tasks by level", e));
|
|
631
623
|
}
|
|
632
624
|
}
|
|
625
|
+
rebalanceByLevel(projectId, level) {
|
|
626
|
+
return logger.startSpan("TaskRepository.rebalanceByLevel", () => {
|
|
627
|
+
try {
|
|
628
|
+
const types = this.getTypesForLevel(level);
|
|
629
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
630
|
+
const rows = this.db.prepare(
|
|
631
|
+
`SELECT * FROM tasks
|
|
632
|
+
WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders})
|
|
633
|
+
ORDER BY
|
|
634
|
+
CASE WHEN status IN (${TERMINAL_PLACEHOLDERS}) THEN 1 ELSE 0 END ASC,
|
|
635
|
+
rank ASC,
|
|
636
|
+
id ASC`
|
|
637
|
+
).all(projectId, ...types, ...TERMINAL_STATUS_ARRAY);
|
|
638
|
+
if (rows.length === 0) return ok(void 0);
|
|
639
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
640
|
+
const updateStmt = this.db.prepare(
|
|
641
|
+
"UPDATE tasks SET rank = ?, updated_at = ? WHERE id = ?"
|
|
642
|
+
);
|
|
643
|
+
this.db.exec("BEGIN");
|
|
644
|
+
try {
|
|
645
|
+
for (let i = 0; i < rows.length; i++) {
|
|
646
|
+
const row = rows[i];
|
|
647
|
+
if (!row) continue;
|
|
648
|
+
const newRank = (i + 1) * RANK_GAP;
|
|
649
|
+
if (row.rank === newRank) continue;
|
|
650
|
+
updateStmt.run(newRank, now, row.id);
|
|
651
|
+
}
|
|
652
|
+
this.db.exec("COMMIT");
|
|
653
|
+
} catch (inner) {
|
|
654
|
+
this.db.exec("ROLLBACK");
|
|
655
|
+
throw inner;
|
|
656
|
+
}
|
|
657
|
+
return ok(void 0);
|
|
658
|
+
} catch (e) {
|
|
659
|
+
return err(new AppError("DB_ERROR", "Failed to rebalance ranks by level", e));
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Fetch `(maxActive, minTerminal)` for a level in a single SQL round-trip.
|
|
665
|
+
* Used by the insert hot path — faster than calling the two separate
|
|
666
|
+
* level accessors in sequence.
|
|
667
|
+
*/
|
|
668
|
+
getRankBoundsByLevel(projectId, level) {
|
|
669
|
+
try {
|
|
670
|
+
const types = this.getTypesForLevel(level);
|
|
671
|
+
const typePlaceholders = types.map(() => "?").join(", ");
|
|
672
|
+
const row = this.db.prepare(
|
|
673
|
+
`SELECT
|
|
674
|
+
MAX(CASE WHEN status NOT IN (${TERMINAL_PLACEHOLDERS}) THEN rank END) AS max_active,
|
|
675
|
+
MIN(CASE WHEN status IN (${TERMINAL_PLACEHOLDERS}) THEN rank END) AS min_terminal
|
|
676
|
+
FROM tasks
|
|
677
|
+
WHERE project_id = ? AND ${NOT_DELETED} AND type IN (${typePlaceholders})`
|
|
678
|
+
).get(...TERMINAL_STATUS_ARRAY, ...TERMINAL_STATUS_ARRAY, projectId, ...types);
|
|
679
|
+
return ok({
|
|
680
|
+
maxActive: row?.max_active ?? 0,
|
|
681
|
+
minTerminal: row?.min_terminal ?? null
|
|
682
|
+
});
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return err(new AppError("DB_ERROR", "Failed to get rank bounds by level", e));
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Compute a fresh rank for a new active task at the given level.
|
|
689
|
+
*
|
|
690
|
+
* Wedges the new task between the last active task and the first terminal
|
|
691
|
+
* task. If the midpoint collapses against either endpoint, or if the
|
|
692
|
+
* level is already in a corrupt interleaved state (terminal rank ≤
|
|
693
|
+
* active rank), rebalance the level once and recompute.
|
|
694
|
+
*/
|
|
695
|
+
computeInsertRank(projectId, level) {
|
|
696
|
+
const attempt = () => {
|
|
697
|
+
const boundsResult = this.getRankBoundsByLevel(projectId, level);
|
|
698
|
+
if (!boundsResult.ok) return boundsResult;
|
|
699
|
+
const { maxActive, minTerminal } = boundsResult.value;
|
|
700
|
+
if (minTerminal === null) {
|
|
701
|
+
return ok(maxActive + RANK_GAP);
|
|
702
|
+
}
|
|
703
|
+
if (minTerminal <= maxActive) {
|
|
704
|
+
return ok(null);
|
|
705
|
+
}
|
|
706
|
+
if (maxActive <= 0) {
|
|
707
|
+
return ok(minTerminal - RANK_GAP);
|
|
708
|
+
}
|
|
709
|
+
return ok(midpoint(maxActive, minTerminal));
|
|
710
|
+
};
|
|
711
|
+
const first = attempt();
|
|
712
|
+
if (!first.ok) return first;
|
|
713
|
+
if (first.value !== null) return ok(first.value);
|
|
714
|
+
const rebalanceResult = this.rebalanceByLevel(projectId, level);
|
|
715
|
+
if (!rebalanceResult.ok) return rebalanceResult;
|
|
716
|
+
const second = attempt();
|
|
717
|
+
if (!second.ok) return second;
|
|
718
|
+
if (second.value === null) {
|
|
719
|
+
return err(new AppError("DB_ERROR", "Rank computation did not converge after rebalance"));
|
|
720
|
+
}
|
|
721
|
+
return ok(second.value);
|
|
722
|
+
}
|
|
633
723
|
search(query, projectId) {
|
|
634
724
|
return logger.startSpan("TaskRepository.search", () => {
|
|
635
725
|
try {
|
|
@@ -1001,7 +1091,11 @@ var RerankTaskSchema = z2.object({
|
|
|
1001
1091
|
taskId: z2.string().min(1, "Task id is required"),
|
|
1002
1092
|
afterId: z2.string().optional(),
|
|
1003
1093
|
beforeId: z2.string().optional(),
|
|
1004
|
-
position: z2.number().int().min(1).optional()
|
|
1094
|
+
position: z2.number().int().min(1).optional(),
|
|
1095
|
+
/** Move to the top of active tasks (highest priority). */
|
|
1096
|
+
top: z2.boolean().optional(),
|
|
1097
|
+
/** Move to the bottom of active tasks, kept above terminal (done/cancelled) tasks. */
|
|
1098
|
+
bottom: z2.boolean().optional()
|
|
1005
1099
|
});
|
|
1006
1100
|
|
|
1007
1101
|
// src/service/task.service.ts
|
|
@@ -1224,13 +1318,13 @@ var TaskServiceImpl = class {
|
|
|
1224
1318
|
if (!parsed.success) {
|
|
1225
1319
|
return err(new AppError("VALIDATION", parsed.error.message));
|
|
1226
1320
|
}
|
|
1227
|
-
const { taskId, afterId, beforeId, position } = parsed.data;
|
|
1228
|
-
const specifiedCount = [afterId, beforeId, position].filter((v) => v !== void 0).length;
|
|
1321
|
+
const { taskId, afterId, beforeId, position, top, bottom } = parsed.data;
|
|
1322
|
+
const specifiedCount = [afterId, beforeId, position].filter((v) => v !== void 0).length + (top ? 1 : 0) + (bottom ? 1 : 0);
|
|
1229
1323
|
if (specifiedCount !== 1) {
|
|
1230
1324
|
return err(
|
|
1231
1325
|
new AppError(
|
|
1232
1326
|
"VALIDATION",
|
|
1233
|
-
"Exactly one of --after, --before, or --
|
|
1327
|
+
"Exactly one of --after, --before, --position, --top, or --bottom must be specified"
|
|
1234
1328
|
)
|
|
1235
1329
|
);
|
|
1236
1330
|
}
|
|
@@ -1253,77 +1347,102 @@ var TaskServiceImpl = class {
|
|
|
1253
1347
|
const projectResult = this.projectService.resolveProject(projectRef);
|
|
1254
1348
|
if (!projectResult.ok) return projectResult;
|
|
1255
1349
|
const projectId = projectResult.value.id;
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1350
|
+
const depService = this.getDependencyService();
|
|
1351
|
+
const blockersResult = depService.listBlockers(taskId);
|
|
1352
|
+
if (!blockersResult.ok) return blockersResult;
|
|
1353
|
+
const constrainingBlockers = blockersResult.value.filter(
|
|
1354
|
+
(b) => !isTerminalStatus(b.status) && b.projectId === projectId
|
|
1355
|
+
);
|
|
1356
|
+
const dependentsResult = depService.listDependents(taskId);
|
|
1357
|
+
if (!dependentsResult.ok) return dependentsResult;
|
|
1358
|
+
const constrainingDependents = dependentsResult.value.filter(
|
|
1359
|
+
(d) => !isTerminalStatus(d.status) && d.projectId === projectId
|
|
1360
|
+
);
|
|
1361
|
+
const attempt = () => {
|
|
1362
|
+
const rankedResult = this.repo.getRankedNonTerminalTasksByLevel(projectId, taskLevel);
|
|
1363
|
+
if (!rankedResult.ok) return rankedResult;
|
|
1364
|
+
const ranked = rankedResult.value.filter((t) => t.id !== taskId);
|
|
1365
|
+
if (top === true) {
|
|
1366
|
+
return ok(this.computeTopRank(ranked, constrainingBlockers));
|
|
1266
1367
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (!anchor) {
|
|
1273
|
-
return err(
|
|
1274
|
-
new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${beforeId}`)
|
|
1368
|
+
if (bottom === true) {
|
|
1369
|
+
const minTerminalResult = this.repo.getMinTerminalRankByLevel(projectId, taskLevel);
|
|
1370
|
+
if (!minTerminalResult.ok) return minTerminalResult;
|
|
1371
|
+
return ok(
|
|
1372
|
+
this.computeBottomRank(ranked, minTerminalResult.value, constrainingDependents)
|
|
1275
1373
|
);
|
|
1276
1374
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1375
|
+
if (afterId) {
|
|
1376
|
+
const anchorIndex = ranked.findIndex((t) => t.id === afterId);
|
|
1377
|
+
const anchor = ranked[anchorIndex];
|
|
1378
|
+
if (!anchor) {
|
|
1379
|
+
return err(
|
|
1380
|
+
new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${afterId}`)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const next = ranked[anchorIndex + 1];
|
|
1384
|
+
return ok(next ? midpoint(anchor.rank, next.rank) : anchor.rank + RANK_GAP);
|
|
1385
|
+
}
|
|
1386
|
+
if (beforeId) {
|
|
1387
|
+
const anchorIndex = ranked.findIndex((t) => t.id === beforeId);
|
|
1388
|
+
const anchor = ranked[anchorIndex];
|
|
1389
|
+
if (!anchor) {
|
|
1390
|
+
return err(
|
|
1391
|
+
new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${beforeId}`)
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
const prev = ranked[anchorIndex - 1];
|
|
1395
|
+
return ok(prev ? midpoint(prev.rank, anchor.rank) : anchor.rank - RANK_GAP);
|
|
1396
|
+
}
|
|
1281
1397
|
const pos = position;
|
|
1282
1398
|
if (pos < 1) {
|
|
1283
1399
|
return err(new AppError("VALIDATION", "Position must be >= 1"));
|
|
1284
1400
|
}
|
|
1285
1401
|
if (pos === 1) {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1402
|
+
return ok(this.computeTopRank(ranked));
|
|
1403
|
+
}
|
|
1404
|
+
if (pos > ranked.length) {
|
|
1405
|
+
const minTerminalResult = this.repo.getMinTerminalRankByLevel(projectId, taskLevel);
|
|
1406
|
+
if (!minTerminalResult.ok) return minTerminalResult;
|
|
1407
|
+
return ok(this.computeBottomRank(ranked, minTerminalResult.value));
|
|
1408
|
+
}
|
|
1409
|
+
const above = ranked[pos - 2];
|
|
1410
|
+
const below = ranked[pos - 1];
|
|
1411
|
+
if (!above || !below) {
|
|
1412
|
+
return err(new AppError("DB_ERROR", "Unexpected missing neighbor tasks"));
|
|
1413
|
+
}
|
|
1414
|
+
return ok(midpoint(above.rank, below.rank));
|
|
1415
|
+
};
|
|
1416
|
+
let computed = attempt();
|
|
1417
|
+
if (!computed.ok) return computed;
|
|
1418
|
+
if (computed.value === null) {
|
|
1419
|
+
const rb = this.repo.rebalanceByLevel(projectId, taskLevel);
|
|
1420
|
+
if (!rb.ok) return rb;
|
|
1421
|
+
computed = attempt();
|
|
1422
|
+
if (!computed.ok) return computed;
|
|
1423
|
+
if (computed.value === null) {
|
|
1424
|
+
return err(new AppError("DB_ERROR", "Rank computation did not converge after rebalance"));
|
|
1298
1425
|
}
|
|
1299
1426
|
}
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
`Cannot rank above blocker "${blocker.id}" (${blocker.name}). Complete or remove the dependency first.`
|
|
1310
|
-
)
|
|
1311
|
-
);
|
|
1312
|
-
}
|
|
1427
|
+
const newRank = computed.value;
|
|
1428
|
+
for (const blocker of constrainingBlockers) {
|
|
1429
|
+
if (newRank < blocker.rank) {
|
|
1430
|
+
return err(
|
|
1431
|
+
new AppError(
|
|
1432
|
+
"VALIDATION",
|
|
1433
|
+
`Cannot rank above blocker "${blocker.id}" (${blocker.name}). Complete or remove the dependency first.`
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1313
1436
|
}
|
|
1314
1437
|
}
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
`Cannot rank below dependent "${dep.id}" (${dep.name}). Complete or remove the dependency first.`
|
|
1324
|
-
)
|
|
1325
|
-
);
|
|
1326
|
-
}
|
|
1438
|
+
for (const dep of constrainingDependents) {
|
|
1439
|
+
if (newRank > dep.rank) {
|
|
1440
|
+
return err(
|
|
1441
|
+
new AppError(
|
|
1442
|
+
"VALIDATION",
|
|
1443
|
+
`Cannot rank below dependent "${dep.id}" (${dep.name}). Complete or remove the dependency first.`
|
|
1444
|
+
)
|
|
1445
|
+
);
|
|
1327
1446
|
}
|
|
1328
1447
|
}
|
|
1329
1448
|
return this.repo.rerank(taskId, newRank);
|
|
@@ -1343,6 +1462,48 @@ var TaskServiceImpl = class {
|
|
|
1343
1462
|
return this.repo.search(query, projectId);
|
|
1344
1463
|
});
|
|
1345
1464
|
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Rank for placing a task at the top of the active list. Clamps to
|
|
1467
|
+
* "immediately after the highest-ranked blocker" when blockers exist
|
|
1468
|
+
* rather than returning a value that would fail validation.
|
|
1469
|
+
*
|
|
1470
|
+
* Returns `null` to signal FP precision collapse (caller rebalances).
|
|
1471
|
+
*/
|
|
1472
|
+
computeTopRank(ranked, constrainingBlockers = []) {
|
|
1473
|
+
if (constrainingBlockers.length > 0) {
|
|
1474
|
+
const highestBlocker = constrainingBlockers.reduce((a, b) => a.rank > b.rank ? a : b);
|
|
1475
|
+
const idx = ranked.findIndex((t) => t.id === highestBlocker.id);
|
|
1476
|
+
if (idx >= 0) {
|
|
1477
|
+
const next = ranked[idx + 1];
|
|
1478
|
+
return next ? midpoint(highestBlocker.rank, next.rank) : highestBlocker.rank + RANK_GAP;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const first = ranked[0];
|
|
1482
|
+
return first ? first.rank - RANK_GAP : RANK_GAP;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Rank for placing a task at the bottom of the active list.
|
|
1486
|
+
* - Stays above terminal tasks: terminal tasks live at `maxRank + RANK_GAP`
|
|
1487
|
+
* so a naive `last.rank + RANK_GAP` would collide with the most-recently
|
|
1488
|
+
* completed task.
|
|
1489
|
+
* - Clamps above any dependents rather than failing validation.
|
|
1490
|
+
*
|
|
1491
|
+
* `minTerminal` is passed in so this helper stays pure (no DB access).
|
|
1492
|
+
* Returns `null` to signal FP precision collapse.
|
|
1493
|
+
*/
|
|
1494
|
+
computeBottomRank(ranked, minTerminal, constrainingDependents = []) {
|
|
1495
|
+
if (constrainingDependents.length > 0) {
|
|
1496
|
+
const lowestDependent = constrainingDependents.reduce((a, b) => a.rank < b.rank ? a : b);
|
|
1497
|
+
const idx = ranked.findIndex((t) => t.id === lowestDependent.id);
|
|
1498
|
+
if (idx >= 0) {
|
|
1499
|
+
const prev = ranked[idx - 1];
|
|
1500
|
+
return prev ? midpoint(prev.rank, lowestDependent.rank) : lowestDependent.rank - RANK_GAP;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
const last = ranked[ranked.length - 1];
|
|
1504
|
+
if (!last) return RANK_GAP;
|
|
1505
|
+
return minTerminal !== null && minTerminal > last.rank ? midpoint(last.rank, minTerminal) : last.rank + RANK_GAP;
|
|
1506
|
+
}
|
|
1346
1507
|
/**
|
|
1347
1508
|
* Auto-propagate status to the parent task after a child status change.
|
|
1348
1509
|
* - If a child moves to in-progress and parent is backlog/todo → parent becomes in-progress.
|
|
@@ -2003,14 +2164,16 @@ function registerTaskBreakdown(parent, container) {
|
|
|
2003
2164
|
|
|
2004
2165
|
// src/cli/commands/task/rank.ts
|
|
2005
2166
|
function registerTaskRank(parent, container) {
|
|
2006
|
-
parent.command("rank <id>").description("Re-rank a task in the backlog (Jira-style positioning)").option("--after <taskId>", "Place immediately after this task").option("--before <taskId>", "Place immediately before this task").option("--position <n>", "Place at 1-based position in backlog").option("-p, --project <project>", "Project id or name").action(
|
|
2167
|
+
parent.command("rank <id>").description("Re-rank a task in the backlog (Jira-style positioning)").option("--after <taskId>", "Place immediately after this task").option("--before <taskId>", "Place immediately before this task").option("--position <n>", "Place at 1-based position in backlog").option("--top", "Move to the top of active tasks").option("--bottom", "Move to the bottom of active tasks (above done tasks)").option("-p, --project <project>", "Project id or name").action(
|
|
2007
2168
|
(id, opts) => {
|
|
2008
2169
|
const result = container.taskService.rerankTask(
|
|
2009
2170
|
{
|
|
2010
2171
|
taskId: id,
|
|
2011
2172
|
afterId: opts.after,
|
|
2012
2173
|
beforeId: opts.before,
|
|
2013
|
-
position: opts.position ? parseInt(opts.position, 10) : void 0
|
|
2174
|
+
position: opts.position ? parseInt(opts.position, 10) : void 0,
|
|
2175
|
+
top: opts.top,
|
|
2176
|
+
bottom: opts.bottom
|
|
2014
2177
|
},
|
|
2015
2178
|
opts.project
|
|
2016
2179
|
);
|
|
@@ -2156,7 +2319,7 @@ function buildCLI(container) {
|
|
|
2156
2319
|
registerDepList(dep, container);
|
|
2157
2320
|
registerDepGraph(dep, container);
|
|
2158
2321
|
program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
|
|
2159
|
-
const { launchTUI } = await import("./tui-
|
|
2322
|
+
const { launchTUI } = await import("./tui-JEP3F4JS.js");
|
|
2160
2323
|
await launchTUI(container, opts.project);
|
|
2161
2324
|
});
|
|
2162
2325
|
return program;
|
|
@@ -2172,7 +2335,7 @@ async function main() {
|
|
|
2172
2335
|
const container = createContainer(db, config.dbPath);
|
|
2173
2336
|
const args = process.argv.slice(2);
|
|
2174
2337
|
if (args.length === 0) {
|
|
2175
|
-
const { launchTUI } = await import("./tui-
|
|
2338
|
+
const { launchTUI } = await import("./tui-JEP3F4JS.js");
|
|
2176
2339
|
await launchTUI(container);
|
|
2177
2340
|
} else {
|
|
2178
2341
|
const program = buildCLI(container);
|