trekoon 0.2.0 → 0.2.4

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.
@@ -3,6 +3,14 @@ import { randomUUID } from "node:crypto";
3
3
  import { Database } from "bun:sqlite";
4
4
 
5
5
  import {
6
+ type CompactEpicExpandResult,
7
+ type CompactDependencyBatchAddResult,
8
+ type CompactDependencySpec,
9
+ type CompactEntityRef,
10
+ type CompactSubtaskBatchCreateResult,
11
+ type CompactSubtaskSpec,
12
+ type CompactTaskBatchCreateResult,
13
+ type CompactTaskSpec,
6
14
  type DependencyRecord,
7
15
  DomainError,
8
16
  type EpicTreeDetailed,
@@ -62,6 +70,47 @@ interface UnresolvedDependencyBlocker {
62
70
  readonly status: string;
63
71
  }
64
72
 
73
+ interface ValidatedTaskBatchSpec {
74
+ readonly tempKey: string;
75
+ readonly title: string;
76
+ readonly description: string;
77
+ readonly status: string;
78
+ }
79
+
80
+ interface ValidatedSubtaskBatchSpec {
81
+ readonly tempKey: string;
82
+ readonly taskId: string;
83
+ readonly title: string;
84
+ readonly description: string;
85
+ readonly status: string;
86
+ }
87
+
88
+ interface ResolvedDependencyBatchSpec {
89
+ readonly index: number;
90
+ readonly sourceId: string;
91
+ readonly sourceKind: "task" | "subtask";
92
+ readonly dependsOnId: string;
93
+ readonly dependsOnKind: "task" | "subtask";
94
+ }
95
+
96
+ interface DependencyBatchValidationIssue {
97
+ readonly index: number;
98
+ readonly type: "missing_id" | "duplicate" | "cycle";
99
+ readonly sourceId: string;
100
+ readonly dependsOnId: string;
101
+ readonly details: Record<string, unknown>;
102
+ }
103
+
104
+ interface DependencyBatchResolution {
105
+ readonly spec?: ResolvedDependencyBatchSpec;
106
+ readonly issues: readonly DependencyBatchValidationIssue[];
107
+ }
108
+
109
+ interface ResolvedCompactEntity {
110
+ readonly id: string;
111
+ readonly kind: "task" | "subtask";
112
+ }
113
+
65
114
  function assertNonEmpty(field: string, value: string | undefined | null): string {
66
115
  const normalized: string = (value ?? "").trim();
67
116
  if (!normalized) {
@@ -271,6 +320,43 @@ export class TrackerDomain {
271
320
  return this.getTaskOrThrow(id);
272
321
  }
273
322
 
323
+ createTaskBatch(input: { epicId: string; specs: readonly CompactTaskSpec[] }): CompactTaskBatchCreateResult {
324
+ const epicId: string = assertNonEmpty("epicId", input.epicId);
325
+ this.getEpicOrThrow(epicId);
326
+
327
+ const validatedSpecs: ValidatedTaskBatchSpec[] = input.specs.map((spec) => ({
328
+ tempKey: assertNonEmpty("tempKey", spec.tempKey),
329
+ title: assertNonEmpty("title", spec.title),
330
+ description: assertNonEmpty("description", spec.description),
331
+ status: normalizeStatus(spec.status),
332
+ }));
333
+
334
+ const tasks: TaskRecord[] = [];
335
+ for (const spec of validatedSpecs) {
336
+ const now: number = Date.now();
337
+ const id: string = randomUUID();
338
+
339
+ this.#db
340
+ .query(
341
+ "INSERT INTO tasks (id, epic_id, title, description, status, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?, ?, ?, 1);",
342
+ )
343
+ .run(id, epicId, spec.title, spec.description, spec.status, now, now);
344
+
345
+ tasks.push(this.getTaskOrThrow(id));
346
+ }
347
+
348
+ return {
349
+ tasks,
350
+ result: {
351
+ mappings: tasks.map((task, index) => ({
352
+ kind: "task",
353
+ tempKey: validatedSpecs[index]?.tempKey ?? "",
354
+ id: task.id,
355
+ })),
356
+ },
357
+ };
358
+ }
359
+
274
360
  listTasks(epicId?: string): readonly TaskRecord[] {
275
361
  if (epicId) {
276
362
  this.getEpicOrThrow(epicId);
@@ -353,6 +439,89 @@ export class TrackerDomain {
353
439
  return this.getSubtaskOrThrow(id);
354
440
  }
355
441
 
442
+ createSubtaskBatch(input: { taskId: string; specs: readonly CompactSubtaskSpec[] }): CompactSubtaskBatchCreateResult {
443
+ const defaultTaskId: string = assertNonEmpty("taskId", input.taskId);
444
+ this.getTaskOrThrow(defaultTaskId);
445
+
446
+ const validatedSpecs: ValidatedSubtaskBatchSpec[] = input.specs.map((spec) => {
447
+ const taskId = spec.parent.kind === "id" ? assertNonEmpty("taskId", spec.parent.id) : defaultTaskId;
448
+ this.getTaskOrThrow(taskId);
449
+
450
+ return {
451
+ tempKey: assertNonEmpty("tempKey", spec.tempKey),
452
+ taskId,
453
+ title: assertNonEmpty("title", spec.title),
454
+ description: spec.description === undefined ? "" : assertNonEmpty("description", spec.description),
455
+ status: normalizeStatus(spec.status),
456
+ };
457
+ });
458
+
459
+ const subtasks: SubtaskRecord[] = [];
460
+ for (const spec of validatedSpecs) {
461
+ const now: number = Date.now();
462
+ const id: string = randomUUID();
463
+
464
+ this.#db
465
+ .query(
466
+ "INSERT INTO subtasks (id, task_id, title, description, status, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?, ?, ?, 1);",
467
+ )
468
+ .run(id, spec.taskId, spec.title, spec.description, spec.status, now, now);
469
+
470
+ subtasks.push(this.getSubtaskOrThrow(id));
471
+ }
472
+
473
+ return {
474
+ subtasks,
475
+ result: {
476
+ mappings: subtasks.map((subtask, index) => ({
477
+ kind: "subtask",
478
+ tempKey: validatedSpecs[index]?.tempKey ?? "",
479
+ id: subtask.id,
480
+ })),
481
+ },
482
+ };
483
+ }
484
+
485
+ expandEpic(input: {
486
+ epicId: string;
487
+ taskSpecs: readonly CompactTaskSpec[];
488
+ subtaskSpecs: readonly CompactSubtaskSpec[];
489
+ dependencySpecs: readonly CompactDependencySpec[];
490
+ }): CompactEpicExpandResult {
491
+ const createdTasks = this.createTaskBatch({
492
+ epicId: input.epicId,
493
+ specs: input.taskSpecs,
494
+ });
495
+
496
+ const resolvedSubtaskSpecs = this.#resolveEpicExpandSubtaskSpecs(input.subtaskSpecs, createdTasks.result.mappings);
497
+ const createdSubtasks = resolvedSubtaskSpecs.length === 0
498
+ ? { subtasks: [], result: { mappings: [] } }
499
+ : this.createSubtaskBatch({
500
+ taskId: resolvedSubtaskSpecs[0]?.parent.kind === "id" ? resolvedSubtaskSpecs[0].parent.id : "",
501
+ specs: resolvedSubtaskSpecs,
502
+ });
503
+
504
+ const mappings = [...createdTasks.result.mappings, ...createdSubtasks.result.mappings];
505
+ const resolvedDependencySpecs = this.#resolveEpicExpandDependencySpecs(input.dependencySpecs, mappings);
506
+ const createdDependencies = resolvedDependencySpecs.length === 0
507
+ ? { dependencies: [], result: { mappings: [] } }
508
+ : this.addDependencyBatch({ specs: resolvedDependencySpecs });
509
+
510
+ return {
511
+ tasks: createdTasks.tasks,
512
+ subtasks: createdSubtasks.subtasks,
513
+ dependencies: createdDependencies.dependencies,
514
+ result: {
515
+ mappings,
516
+ counts: {
517
+ tasks: createdTasks.tasks.length,
518
+ subtasks: createdSubtasks.subtasks.length,
519
+ dependencies: createdDependencies.dependencies.length,
520
+ },
521
+ },
522
+ };
523
+ }
524
+
356
525
  listSubtasks(taskId?: string): readonly SubtaskRecord[] {
357
526
  if (taskId) {
358
527
  this.getTaskOrThrow(taskId);
@@ -633,6 +802,54 @@ export class TrackerDomain {
633
802
  return this.getDependencyOrThrow(id);
634
803
  }
635
804
 
805
+ addDependencyBatch(input: { specs: readonly CompactDependencySpec[] }): CompactDependencyBatchAddResult {
806
+ const resolutions = input.specs.map((spec, index) => this.#resolveDependencyBatchSpec(index, spec));
807
+ const resolvedSpecs = resolutions.flatMap((resolution) => (resolution.spec === undefined ? [] : [resolution.spec]));
808
+ const issues = resolutions.flatMap((resolution) => resolution.issues).concat(this.#collectDependencyBatchIssues(resolvedSpecs));
809
+ if (issues.length > 0) {
810
+ const orderedIssues = issues.sort((left, right) => left.index - right.index || left.type.localeCompare(right.type));
811
+ const firstIssue = orderedIssues[0] ?? null;
812
+ throw new DomainError({
813
+ code: "invalid_dependency",
814
+ message:
815
+ firstIssue?.type === "missing_id"
816
+ ? "dependency batch contains missing ids"
817
+ : firstIssue?.type === "duplicate"
818
+ ? "dependency batch contains duplicate edges"
819
+ : "dependency batch contains cycles",
820
+ details: {
821
+ issues: orderedIssues,
822
+ firstIssue,
823
+ },
824
+ });
825
+ }
826
+
827
+ const dependencies: DependencyRecord[] = [];
828
+ for (const spec of resolvedSpecs) {
829
+ const existing = this.#getDependencyByEdge(spec.sourceId, spec.dependsOnId);
830
+ if (existing) {
831
+ dependencies.push(existing);
832
+ continue;
833
+ }
834
+
835
+ const id: string = randomUUID();
836
+ const now: number = Date.now();
837
+
838
+ this.#db
839
+ .query(
840
+ "INSERT INTO dependencies (id, source_id, source_kind, depends_on_id, depends_on_kind, created_at, updated_at, version) VALUES (?, ?, ?, ?, ?, ?, ?, 1);",
841
+ )
842
+ .run(id, spec.sourceId, spec.sourceKind, spec.dependsOnId, spec.dependsOnKind, now, now);
843
+
844
+ dependencies.push(this.getDependencyOrThrow(id));
845
+ }
846
+
847
+ return {
848
+ dependencies,
849
+ result: { mappings: [] },
850
+ };
851
+ }
852
+
636
853
  removeDependency(sourceId: string, dependsOnId: string): number {
637
854
  const normalizedSourceId: string = assertNonEmpty("sourceId", sourceId);
638
855
  const normalizedDependsOnId: string = assertNonEmpty("dependsOnId", dependsOnId);
@@ -707,6 +924,292 @@ export class TrackerDomain {
707
924
  return mapDependency(row);
708
925
  }
709
926
 
927
+ #getDependencyByEdge(sourceId: string, dependsOnId: string): DependencyRecord | null {
928
+ const row = this.#db
929
+ .query(
930
+ "SELECT id, source_id, source_kind, depends_on_id, depends_on_kind, created_at, updated_at FROM dependencies WHERE source_id = ? AND depends_on_id = ?;",
931
+ )
932
+ .get(sourceId, dependsOnId) as DependencyRow | null;
933
+
934
+ return row ? mapDependency(row) : null;
935
+ }
936
+
937
+ #resolveDependencyBatchSpec(index: number, spec: CompactDependencySpec): DependencyBatchResolution {
938
+ const sourceResolution = this.#resolveDependencyBatchId(spec.source, "source", index);
939
+ const dependsOnResolution = this.#resolveDependencyBatchId(spec.dependsOn, "dependsOn", index);
940
+ const issues = [...sourceResolution.issues, ...dependsOnResolution.issues];
941
+ const sourceId = sourceResolution.id;
942
+ const dependsOnId = dependsOnResolution.id;
943
+
944
+ if (sourceId === undefined || dependsOnId === undefined) {
945
+ return {
946
+ issues,
947
+ };
948
+ }
949
+
950
+ if (sourceId === dependsOnId) {
951
+ return {
952
+ issues: [
953
+ ...issues,
954
+ {
955
+ index,
956
+ type: "cycle",
957
+ sourceId,
958
+ dependsOnId,
959
+ details: { sourceId, dependsOnId, reason: "self_reference" },
960
+ },
961
+ ],
962
+ };
963
+ }
964
+
965
+ return {
966
+ spec: {
967
+ index,
968
+ sourceId,
969
+ sourceKind: this.resolveNodeKind(sourceId),
970
+ dependsOnId,
971
+ dependsOnKind: this.resolveNodeKind(dependsOnId),
972
+ },
973
+ issues,
974
+ };
975
+ }
976
+
977
+ #resolveDependencyBatchId(
978
+ reference: CompactEntityRef,
979
+ field: "source" | "dependsOn",
980
+ index: number,
981
+ ): { readonly id?: string; readonly issues: readonly DependencyBatchValidationIssue[] } {
982
+ if (reference.kind === "temp_key") {
983
+ return {
984
+ issues: [
985
+ {
986
+ index,
987
+ type: "missing_id",
988
+ sourceId: field === "source" ? `@${reference.tempKey}` : "",
989
+ dependsOnId: field === "dependsOn" ? `@${reference.tempKey}` : "",
990
+ details: {
991
+ field,
992
+ tempKey: reference.tempKey,
993
+ message: `Unresolved temp key @${reference.tempKey}`,
994
+ },
995
+ },
996
+ ],
997
+ };
998
+ }
999
+
1000
+ const id = assertNonEmpty(field === "source" ? "sourceId" : "dependsOnId", reference.id);
1001
+ const task = this.getTask(id);
1002
+ const subtask = this.getSubtask(id);
1003
+ if (!task && !subtask) {
1004
+ return {
1005
+ issues: [
1006
+ {
1007
+ index,
1008
+ type: "missing_id",
1009
+ sourceId: field === "source" ? id : "",
1010
+ dependsOnId: field === "dependsOn" ? id : "",
1011
+ details: {
1012
+ field,
1013
+ id,
1014
+ message: `Node not found: ${id}`,
1015
+ },
1016
+ },
1017
+ ],
1018
+ };
1019
+ }
1020
+
1021
+ return { id, issues: [] };
1022
+ }
1023
+
1024
+ #resolveEpicExpandSubtaskSpecs(
1025
+ specs: readonly CompactSubtaskSpec[],
1026
+ mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
1027
+ ): CompactSubtaskSpec[] {
1028
+ return specs.map((spec, index) => {
1029
+ const parent = this.#resolveEpicExpandEntityRef(spec.parent, mappings, "subtask", index, "parent");
1030
+ if (parent.kind !== "task") {
1031
+ throw new DomainError({
1032
+ code: "invalid_input",
1033
+ message: `Subtask parent must resolve to a task in --subtask spec ${index + 1}`,
1034
+ details: {
1035
+ index,
1036
+ field: "parent",
1037
+ kind: parent.kind,
1038
+ id: parent.id,
1039
+ },
1040
+ });
1041
+ }
1042
+
1043
+ return {
1044
+ ...spec,
1045
+ parent: {
1046
+ kind: "id",
1047
+ id: parent.id,
1048
+ },
1049
+ };
1050
+ });
1051
+ }
1052
+
1053
+ #resolveEpicExpandDependencySpecs(
1054
+ specs: readonly CompactDependencySpec[],
1055
+ mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
1056
+ ): CompactDependencySpec[] {
1057
+ return specs.map((spec, index) => ({
1058
+ source: {
1059
+ kind: "id",
1060
+ id: this.#resolveEpicExpandEntityRef(spec.source, mappings, "dep", index, "source").id,
1061
+ },
1062
+ dependsOn: {
1063
+ kind: "id",
1064
+ id: this.#resolveEpicExpandEntityRef(spec.dependsOn, mappings, "dep", index, "dependsOn").id,
1065
+ },
1066
+ }));
1067
+ }
1068
+
1069
+ #resolveEpicExpandEntityRef(
1070
+ reference: CompactEntityRef,
1071
+ mappings: readonly { tempKey: string; id: string; kind: "task" | "subtask" }[],
1072
+ option: "subtask" | "dep",
1073
+ index: number,
1074
+ field: "parent" | "source" | "dependsOn",
1075
+ ): ResolvedCompactEntity {
1076
+ if (reference.kind === "temp_key") {
1077
+ const mapping = mappings.find((candidate) => candidate.tempKey === reference.tempKey);
1078
+ if (mapping === undefined) {
1079
+ throw new DomainError({
1080
+ code: "invalid_input",
1081
+ message: `Unknown temp key @${reference.tempKey} in --${option} spec ${index + 1}`,
1082
+ details: {
1083
+ index,
1084
+ field,
1085
+ tempKey: reference.tempKey,
1086
+ option,
1087
+ },
1088
+ });
1089
+ }
1090
+
1091
+ return {
1092
+ id: mapping.id,
1093
+ kind: mapping.kind,
1094
+ };
1095
+ }
1096
+
1097
+ const id = assertNonEmpty(field === "parent" ? "taskId" : `${field}Id`, reference.id);
1098
+ return {
1099
+ id,
1100
+ kind: this.resolveNodeKind(id),
1101
+ };
1102
+ }
1103
+
1104
+ #collectDependencyBatchIssues(specs: readonly ResolvedDependencyBatchSpec[]): DependencyBatchValidationIssue[] {
1105
+ const issues: DependencyBatchValidationIssue[] = [];
1106
+ const seenEdges = new Map<string, number>();
1107
+ const adjacency = this.#buildDependencyAdjacency();
1108
+
1109
+ for (const spec of specs) {
1110
+ const edgeKey = `${spec.sourceId}->${spec.dependsOnId}`;
1111
+ const existingIndex = seenEdges.get(edgeKey);
1112
+ if (existingIndex !== undefined) {
1113
+ issues.push({
1114
+ index: spec.index,
1115
+ type: "duplicate",
1116
+ sourceId: spec.sourceId,
1117
+ dependsOnId: spec.dependsOnId,
1118
+ details: {
1119
+ sourceId: spec.sourceId,
1120
+ dependsOnId: spec.dependsOnId,
1121
+ firstIndex: existingIndex,
1122
+ duplicateIndex: spec.index,
1123
+ duplicateKind: "batch",
1124
+ },
1125
+ });
1126
+ continue;
1127
+ }
1128
+
1129
+ if (this.#getDependencyByEdge(spec.sourceId, spec.dependsOnId) !== null) {
1130
+ issues.push({
1131
+ index: spec.index,
1132
+ type: "duplicate",
1133
+ sourceId: spec.sourceId,
1134
+ dependsOnId: spec.dependsOnId,
1135
+ details: {
1136
+ sourceId: spec.sourceId,
1137
+ dependsOnId: spec.dependsOnId,
1138
+ duplicateKind: "existing",
1139
+ },
1140
+ });
1141
+ continue;
1142
+ }
1143
+
1144
+ if (this.#wouldCreateCycleInAdjacency(adjacency, spec.sourceId, spec.dependsOnId)) {
1145
+ issues.push({
1146
+ index: spec.index,
1147
+ type: "cycle",
1148
+ sourceId: spec.sourceId,
1149
+ dependsOnId: spec.dependsOnId,
1150
+ details: {
1151
+ sourceId: spec.sourceId,
1152
+ dependsOnId: spec.dependsOnId,
1153
+ },
1154
+ });
1155
+ continue;
1156
+ }
1157
+
1158
+ const nextNeighbors = adjacency.get(spec.sourceId) ?? new Set<string>();
1159
+ nextNeighbors.add(spec.dependsOnId);
1160
+ adjacency.set(spec.sourceId, nextNeighbors);
1161
+ seenEdges.set(edgeKey, spec.index);
1162
+ }
1163
+
1164
+ return issues.sort((left, right) => left.index - right.index || left.type.localeCompare(right.type));
1165
+ }
1166
+
1167
+ #buildDependencyAdjacency(): Map<string, Set<string>> {
1168
+ const rows = this.#db.query("SELECT source_id, depends_on_id FROM dependencies ORDER BY source_id ASC, depends_on_id ASC;").all() as Array<{
1169
+ source_id: string;
1170
+ depends_on_id: string;
1171
+ }>;
1172
+ const adjacency = new Map<string, Set<string>>();
1173
+
1174
+ for (const row of rows) {
1175
+ const neighbors = adjacency.get(row.source_id) ?? new Set<string>();
1176
+ neighbors.add(row.depends_on_id);
1177
+ adjacency.set(row.source_id, neighbors);
1178
+ }
1179
+
1180
+ return adjacency;
1181
+ }
1182
+
1183
+ #wouldCreateCycleInAdjacency(adjacency: ReadonlyMap<string, ReadonlySet<string>>, sourceId: string, dependsOnId: string): boolean {
1184
+ const visited = new Set<string>();
1185
+ const queue: string[] = [dependsOnId];
1186
+
1187
+ while (queue.length > 0) {
1188
+ const current = queue.shift();
1189
+ if (current === undefined || visited.has(current)) {
1190
+ continue;
1191
+ }
1192
+
1193
+ if (current === sourceId) {
1194
+ return true;
1195
+ }
1196
+
1197
+ visited.add(current);
1198
+ const neighbors = adjacency.get(current);
1199
+ if (neighbors === undefined) {
1200
+ continue;
1201
+ }
1202
+
1203
+ for (const neighbor of neighbors) {
1204
+ if (!visited.has(neighbor)) {
1205
+ queue.push(neighbor);
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ return false;
1211
+ }
1212
+
710
1213
  private collectSearchMatches(
711
1214
  nodes: readonly SearchNode[],
712
1215
  searchText: string,
@@ -1,5 +1,85 @@
1
1
  export type NodeKind = "epic" | "task" | "subtask";
2
2
 
3
+ export const COMPACT_TEMP_KEY_PREFIX = "@";
4
+
5
+ export type CompactTempKey = string;
6
+
7
+ export interface CompactTempKeyRef {
8
+ readonly kind: "temp_key";
9
+ readonly tempKey: CompactTempKey;
10
+ }
11
+
12
+ export interface CompactEntityIdRef {
13
+ readonly kind: "id";
14
+ readonly id: string;
15
+ }
16
+
17
+ export type CompactEntityRef = CompactTempKeyRef | CompactEntityIdRef;
18
+
19
+ export interface CompactTaskSpec {
20
+ readonly tempKey: CompactTempKey;
21
+ readonly title: string;
22
+ readonly description: string;
23
+ readonly status?: string;
24
+ }
25
+
26
+ export interface CompactSubtaskSpec {
27
+ readonly parent: CompactEntityRef;
28
+ readonly tempKey: CompactTempKey;
29
+ readonly title: string;
30
+ readonly description: string;
31
+ readonly status?: string;
32
+ }
33
+
34
+ export interface CompactDependencySpec {
35
+ readonly source: CompactEntityRef;
36
+ readonly dependsOn: CompactEntityRef;
37
+ }
38
+
39
+ export interface CompactTempKeyMapping<TKind extends Extract<NodeKind, "task" | "subtask"> = Extract<NodeKind, "task" | "subtask">> {
40
+ readonly kind: TKind;
41
+ readonly tempKey: CompactTempKey;
42
+ readonly id: string;
43
+ }
44
+
45
+ export interface CompactBatchResultContract {
46
+ readonly mappings: ReadonlyArray<CompactTempKeyMapping>;
47
+ }
48
+
49
+ export interface CompactTaskBatchCreateResult {
50
+ readonly tasks: ReadonlyArray<TaskRecord>;
51
+ readonly result: CompactBatchResultContract;
52
+ }
53
+
54
+ export interface CompactSubtaskBatchCreateResult {
55
+ readonly subtasks: ReadonlyArray<SubtaskRecord>;
56
+ readonly result: CompactBatchResultContract;
57
+ }
58
+
59
+ export interface CompactDependencyBatchAddResult {
60
+ readonly dependencies: ReadonlyArray<DependencyRecord>;
61
+ readonly result: CompactBatchResultContract;
62
+ }
63
+
64
+ export interface CompactBatchCounts {
65
+ readonly tasks: number;
66
+ readonly subtasks: number;
67
+ readonly dependencies: number;
68
+ }
69
+
70
+ export interface CompactEpicExpandResult {
71
+ readonly tasks: ReadonlyArray<TaskRecord>;
72
+ readonly subtasks: ReadonlyArray<SubtaskRecord>;
73
+ readonly dependencies: ReadonlyArray<DependencyRecord>;
74
+ readonly result: CompactBatchResultContract & {
75
+ readonly counts: CompactBatchCounts;
76
+ };
77
+ }
78
+
79
+ export interface CompactEpicCreateResult extends CompactEpicExpandResult {
80
+ readonly epic: EpicRecord;
81
+ }
82
+
3
83
  export interface EpicRecord {
4
84
  readonly id: string;
5
85
  readonly title: string;