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.
- package/.agents/skills/trekoon/SKILL.md +176 -230
- package/README.md +299 -7
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +198 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +674 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +66 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +408 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +381 -26
- package/src/domain/mutation-service.ts +394 -1
- package/src/domain/tracker-domain.ts +674 -0
- package/src/domain/types.ts +107 -0
- package/src/sync/event-writes.ts +21 -1
- package/src/sync/service.ts +42 -0
package/src/domain/types.ts
CHANGED
|
@@ -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;
|
package/src/sync/event-writes.ts
CHANGED
|
@@ -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 =
|
|
38
|
+
const now: number = nextEventTimestamp(db);
|
|
19
39
|
const eventId: string = randomUUID();
|
|
20
40
|
|
|
21
41
|
db.query(
|
package/src/sync/service.ts
CHANGED
|
@@ -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;
|