@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.
@@ -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-FUNYPBWJ.js.map
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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","names":[]}
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
- } from "./chunk-FUNYPBWJ.js";
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 maxActiveResult = this.getMaxActiveRankByLevel(input.projectId, level);
336
- if (!maxActiveResult.ok) return maxActiveResult;
337
- const maxActiveRank = maxActiveResult.value;
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 --position must be specified"
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 rankedResult = this.repo.getRankedNonTerminalTasksByLevel(projectId, taskLevel);
1257
- if (!rankedResult.ok) return rankedResult;
1258
- const ranked = rankedResult.value.filter((t) => t.id !== taskId);
1259
- let newRank;
1260
- if (afterId) {
1261
- const anchor = ranked.find((t) => t.id === afterId);
1262
- if (!anchor) {
1263
- return err(
1264
- new AppError("NOT_FOUND", `Anchor task not found among active tasks: ${afterId}`)
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
- const anchorIndex = ranked.indexOf(anchor);
1268
- const next = ranked[anchorIndex + 1];
1269
- newRank = next ? (anchor.rank + next.rank) / 2 : anchor.rank + RANK_GAP;
1270
- } else if (beforeId) {
1271
- const anchor = ranked.find((t) => t.id === beforeId);
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
- const anchorIndex = ranked.indexOf(anchor);
1278
- const prev = ranked[anchorIndex - 1];
1279
- newRank = prev ? (prev.rank + anchor.rank) / 2 : anchor.rank - RANK_GAP;
1280
- } else {
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
- const first = ranked[0];
1287
- newRank = first ? first.rank - RANK_GAP : RANK_GAP;
1288
- } else if (pos > ranked.length) {
1289
- const last = ranked[ranked.length - 1];
1290
- newRank = last ? last.rank + RANK_GAP : RANK_GAP;
1291
- } else {
1292
- const above = ranked[pos - 2];
1293
- const below = ranked[pos - 1];
1294
- if (!above || !below) {
1295
- return err(new AppError("DB_ERROR", "Unexpected missing neighbor tasks"));
1296
- }
1297
- newRank = (above.rank + below.rank) / 2;
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 depService = this.getDependencyService();
1301
- const blockersResult = depService.listBlockers(taskId);
1302
- if (blockersResult.ok) {
1303
- for (const blocker of blockersResult.value) {
1304
- if (isTerminalStatus(blocker.status)) continue;
1305
- if (blocker.projectId === projectId && newRank < blocker.rank) {
1306
- return err(
1307
- new AppError(
1308
- "VALIDATION",
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 dependentsResult = depService.listDependents(taskId);
1316
- if (dependentsResult.ok) {
1317
- for (const dep of dependentsResult.value) {
1318
- if (isTerminalStatus(dep.status)) continue;
1319
- if (dep.projectId === projectId && newRank > dep.rank) {
1320
- return err(
1321
- new AppError(
1322
- "VALIDATION",
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-5JJH67YY.js");
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-5JJH67YY.js");
2338
+ const { launchTUI } = await import("./tui-JEP3F4JS.js");
2176
2339
  await launchTUI(container);
2177
2340
  } else {
2178
2341
  const program = buildCLI(container);