trekoon 0.1.9 → 0.2.1

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.
@@ -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;
@@ -85,6 +165,33 @@ export interface EpicTreeDetailed {
85
165
  readonly tasks: ReadonlyArray<TaskTreeDetailed>;
86
166
  }
87
167
 
168
+ export type SearchField = "title" | "description";
169
+
170
+ export interface SearchFieldMatch {
171
+ readonly field: SearchField;
172
+ readonly count: number;
173
+ readonly snippet: string;
174
+ }
175
+
176
+ export interface SearchEntityMatch {
177
+ readonly kind: NodeKind;
178
+ readonly id: string;
179
+ readonly fields: readonly SearchFieldMatch[];
180
+ }
181
+
182
+ export interface SearchSummary {
183
+ readonly matchedEntities: number;
184
+ readonly matchedFields: number;
185
+ readonly totalMatches: number;
186
+ }
187
+
188
+ export interface SearchNode {
189
+ readonly kind: NodeKind;
190
+ readonly id: string;
191
+ readonly title: string;
192
+ readonly description: string;
193
+ }
194
+
88
195
  export interface DomainErrorShape {
89
196
  readonly code: string;
90
197
  readonly message: string;
@@ -11,11 +11,31 @@ interface EventRecordInput {
11
11
  readonly fields: Record<string, unknown>;
12
12
  }
13
13
 
14
+ function nextEventTimestamp(db: Database): number {
15
+ const now: number = Date.now();
16
+ const latestEvent = db
17
+ .query(
18
+ `
19
+ SELECT created_at
20
+ FROM events
21
+ ORDER BY created_at DESC, id DESC
22
+ LIMIT 1;
23
+ `,
24
+ )
25
+ .get() as { created_at: number } | null;
26
+
27
+ if (!latestEvent) {
28
+ return now;
29
+ }
30
+
31
+ return Math.max(now, latestEvent.created_at + 1);
32
+ }
33
+
14
34
  export function appendEventWithGitContext(db: Database, cwd: string, input: EventRecordInput): string {
15
35
  const git = resolveGitContext(cwd);
16
36
  persistGitContext(db, git);
17
37
 
18
- const now: number = Date.now();
38
+ const now: number = nextEventTimestamp(db);
19
39
  const eventId: string = randomUUID();
20
40
 
21
41
  db.query(
@@ -401,6 +401,18 @@ function rowExists(db: Database, tableName: string, id: string): boolean {
401
401
  return row !== null;
402
402
  }
403
403
 
404
+ function dependencyNodeExists(db: Database, nodeKind: string, nodeId: string): boolean {
405
+ if (nodeKind === "task") {
406
+ return rowExists(db, "tasks", nodeId);
407
+ }
408
+
409
+ if (nodeKind === "subtask") {
410
+ return rowExists(db, "subtasks", nodeId);
411
+ }
412
+
413
+ return false;
414
+ }
415
+
404
416
  function validateRequiredStringField(fields: Record<string, unknown>, fieldName: string): string | null {
405
417
  const value: unknown = fields[fieldName];
406
418
  if (typeof value !== "string" || value.length === 0) {
@@ -503,6 +515,10 @@ function applyCreate(db: Database, event: StoredEvent, fields: Record<string, un
503
515
  return false;
504
516
  }
505
517
 
518
+ if (!dependencyNodeExists(db, sourceKind, sourceId) || !dependencyNodeExists(db, dependsOnKind, dependsOnId)) {
519
+ return false;
520
+ }
521
+
506
522
  db.query(
507
523
  `
508
524
  INSERT INTO dependencies (
@@ -625,6 +641,28 @@ function applyEntityFields(db: Database, event: StoredEvent, fields: Record<stri
625
641
  return false;
626
642
  }
627
643
 
644
+ function applyReplayedCreateWithConflicts(
645
+ db: Database,
646
+ event: StoredEvent,
647
+ fields: Record<string, unknown>,
648
+ withheldConflictCount: number,
649
+ ): boolean {
650
+ if (withheldConflictCount === 0 || !event.operation.endsWith(".created")) {
651
+ return false;
652
+ }
653
+
654
+ const tableName = tableForEntityKind(event.entity_kind);
655
+ if (!tableName || !rowExists(db, tableName, event.entity_id)) {
656
+ return false;
657
+ }
658
+
659
+ if (Object.keys(fields).length === 0) {
660
+ return true;
661
+ }
662
+
663
+ return applyUpdatePatch(db, event, fields);
664
+ }
665
+
628
666
  function storeEvent(db: Database, event: StoredEvent): void {
629
667
  db.query(
630
668
  `
@@ -727,11 +765,13 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
727
765
 
728
766
  const payload: EventPayload = { fields: payloadValidation.fields };
729
767
  const fieldsToApply: Record<string, unknown> = {};
768
+ let withheldConflictCount = 0;
730
769
 
731
770
  for (const [fieldName, value] of Object.entries(payload.fields)) {
732
771
  const conflict = entityFieldConflict(storage.db, sourceBranch, incoming, fieldName, value);
733
772
 
734
773
  if (conflict) {
774
+ withheldConflictCount += 1;
735
775
  conflictEvents += 1;
736
776
  createConflict(storage.db, incoming, fieldName, conflict.oursValue, conflict.theirsValue);
737
777
  createdConflicts += 1;
@@ -743,6 +783,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
743
783
 
744
784
  if (applyEntityFields(storage.db, incoming, fieldsToApply)) {
745
785
  appliedEvents += 1;
786
+ } else if (applyReplayedCreateWithConflicts(storage.db, incoming, fieldsToApply, withheldConflictCount)) {
787
+ appliedEvents += 1;
746
788
  } else if (incoming.operation !== "resolve_conflict") {
747
789
  applyRejectedEvents += 1;
748
790
  quarantinedEvents += 1;