@tomkapa/tayto 0.3.0 → 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 +245 -76
- 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,71 +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
|
-
|
|
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));
|
|
1264
1367
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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)
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
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);
|
|
1272
1396
|
}
|
|
1273
|
-
const anchorIndex = ranked.indexOf(anchor);
|
|
1274
|
-
const prev = ranked[anchorIndex - 1];
|
|
1275
|
-
newRank = prev ? (prev.rank + anchor.rank) / 2 : anchor.rank - RANK_GAP;
|
|
1276
|
-
} else {
|
|
1277
1397
|
const pos = position;
|
|
1278
1398
|
if (pos < 1) {
|
|
1279
1399
|
return err(new AppError("VALIDATION", "Position must be >= 1"));
|
|
1280
1400
|
}
|
|
1281
1401
|
if (pos === 1) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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"));
|
|
1294
1425
|
}
|
|
1295
1426
|
}
|
|
1296
|
-
const
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
)
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
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
|
+
);
|
|
1308
1436
|
}
|
|
1309
1437
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
)
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
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
|
+
);
|
|
1321
1446
|
}
|
|
1322
1447
|
}
|
|
1323
1448
|
return this.repo.rerank(taskId, newRank);
|
|
@@ -1337,6 +1462,48 @@ var TaskServiceImpl = class {
|
|
|
1337
1462
|
return this.repo.search(query, projectId);
|
|
1338
1463
|
});
|
|
1339
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
|
+
}
|
|
1340
1507
|
/**
|
|
1341
1508
|
* Auto-propagate status to the parent task after a child status change.
|
|
1342
1509
|
* - If a child moves to in-progress and parent is backlog/todo → parent becomes in-progress.
|
|
@@ -1997,14 +2164,16 @@ function registerTaskBreakdown(parent, container) {
|
|
|
1997
2164
|
|
|
1998
2165
|
// src/cli/commands/task/rank.ts
|
|
1999
2166
|
function registerTaskRank(parent, container) {
|
|
2000
|
-
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(
|
|
2001
2168
|
(id, opts) => {
|
|
2002
2169
|
const result = container.taskService.rerankTask(
|
|
2003
2170
|
{
|
|
2004
2171
|
taskId: id,
|
|
2005
2172
|
afterId: opts.after,
|
|
2006
2173
|
beforeId: opts.before,
|
|
2007
|
-
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
|
|
2008
2177
|
},
|
|
2009
2178
|
opts.project
|
|
2010
2179
|
);
|
|
@@ -2150,7 +2319,7 @@ function buildCLI(container) {
|
|
|
2150
2319
|
registerDepList(dep, container);
|
|
2151
2320
|
registerDepGraph(dep, container);
|
|
2152
2321
|
program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
|
|
2153
|
-
const { launchTUI } = await import("./tui-
|
|
2322
|
+
const { launchTUI } = await import("./tui-JEP3F4JS.js");
|
|
2154
2323
|
await launchTUI(container, opts.project);
|
|
2155
2324
|
});
|
|
2156
2325
|
return program;
|
|
@@ -2166,7 +2335,7 @@ async function main() {
|
|
|
2166
2335
|
const container = createContainer(db, config.dbPath);
|
|
2167
2336
|
const args = process.argv.slice(2);
|
|
2168
2337
|
if (args.length === 0) {
|
|
2169
|
-
const { launchTUI } = await import("./tui-
|
|
2338
|
+
const { launchTUI } = await import("./tui-JEP3F4JS.js");
|
|
2170
2339
|
await launchTUI(container);
|
|
2171
2340
|
} else {
|
|
2172
2341
|
const program = buildCLI(container);
|