trekoon 0.1.9 → 0.2.0
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 +75 -2
- package/README.md +87 -6
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +82 -0
- package/src/commands/epic.ts +186 -2
- package/src/commands/help.ts +33 -3
- package/src/commands/subtask.ts +186 -2
- package/src/commands/task.ts +186 -2
- package/src/domain/mutation-service.ts +242 -1
- package/src/domain/tracker-domain.ts +171 -0
- package/src/domain/types.ts +27 -0
- package/src/sync/event-writes.ts +21 -1
|
@@ -3,7 +3,83 @@ import { type Database } from "bun:sqlite";
|
|
|
3
3
|
import { appendEventWithGitContext } from "../sync/event-writes";
|
|
4
4
|
import { ENTITY_OPERATIONS } from "./mutation-operations";
|
|
5
5
|
import { TrackerDomain } from "./tracker-domain";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type DependencyRecord,
|
|
8
|
+
type EpicRecord,
|
|
9
|
+
type SearchEntityMatch,
|
|
10
|
+
type SearchField,
|
|
11
|
+
type SearchNode,
|
|
12
|
+
type SearchSummary,
|
|
13
|
+
type SubtaskRecord,
|
|
14
|
+
type TaskRecord,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
function countMatches(value: string, searchText: string): number {
|
|
18
|
+
if (searchText.length === 0) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let count = 0;
|
|
23
|
+
let offset = 0;
|
|
24
|
+
while (offset <= value.length - searchText.length) {
|
|
25
|
+
const nextIndex = value.indexOf(searchText, offset);
|
|
26
|
+
if (nextIndex === -1) {
|
|
27
|
+
return count;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
count += 1;
|
|
31
|
+
offset = nextIndex + searchText.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return count;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function replaceMatches(value: string, searchText: string, replacement: string): string {
|
|
38
|
+
return searchText.length === 0 ? value : value.split(searchText).join(replacement);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildMatchSnippet(value: string, searchText: string, contextSize = 24): string {
|
|
42
|
+
if (searchText.length === 0) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const matchIndex = value.indexOf(searchText);
|
|
47
|
+
if (matchIndex === -1) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const start = Math.max(0, matchIndex - contextSize);
|
|
52
|
+
const end = Math.min(value.length, matchIndex + searchText.length + contextSize);
|
|
53
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
54
|
+
const prefix = start > 0 ? "…" : "";
|
|
55
|
+
const suffix = end < value.length ? "…" : "";
|
|
56
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildReplacementSnippet(value: string, replacementIndex: number, replacementLength: number, contextSize = 24): string {
|
|
60
|
+
const start = Math.max(0, replacementIndex - contextSize);
|
|
61
|
+
const end = Math.min(value.length, replacementIndex + replacementLength + contextSize);
|
|
62
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
63
|
+
const prefix = start > 0 ? "…" : "";
|
|
64
|
+
const suffix = end < value.length ? "…" : "";
|
|
65
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function summarizeMatches(matches: readonly SearchEntityMatch[]): SearchSummary {
|
|
69
|
+
return {
|
|
70
|
+
matchedEntities: matches.length,
|
|
71
|
+
matchedFields: matches.reduce((total, match) => total + match.fields.length, 0),
|
|
72
|
+
totalMatches: matches.reduce(
|
|
73
|
+
(total, match) => total + match.fields.reduce((fieldTotal, field) => fieldTotal + field.count, 0),
|
|
74
|
+
0,
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ScopeReplacementResult {
|
|
80
|
+
readonly matches: readonly SearchEntityMatch[];
|
|
81
|
+
readonly summary: SearchSummary;
|
|
82
|
+
}
|
|
7
83
|
|
|
8
84
|
export class MutationService {
|
|
9
85
|
readonly #db: Database;
|
|
@@ -153,6 +229,60 @@ export class MutationService {
|
|
|
153
229
|
})();
|
|
154
230
|
}
|
|
155
231
|
|
|
232
|
+
previewEpicReplacement(
|
|
233
|
+
epicId: string,
|
|
234
|
+
searchText: string,
|
|
235
|
+
replacementText: string,
|
|
236
|
+
fields: readonly SearchField[],
|
|
237
|
+
): ScopeReplacementResult {
|
|
238
|
+
return this.#previewScopeReplacement(this.#domain.collectEpicSearchScope(epicId), searchText, replacementText, fields);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
applyEpicReplacement(
|
|
242
|
+
epicId: string,
|
|
243
|
+
searchText: string,
|
|
244
|
+
replacementText: string,
|
|
245
|
+
fields: readonly SearchField[],
|
|
246
|
+
): ScopeReplacementResult {
|
|
247
|
+
return this.#applyScopeReplacement(this.#domain.collectEpicSearchScope(epicId), searchText, replacementText, fields);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
previewTaskReplacement(
|
|
251
|
+
taskId: string,
|
|
252
|
+
searchText: string,
|
|
253
|
+
replacementText: string,
|
|
254
|
+
fields: readonly SearchField[],
|
|
255
|
+
): ScopeReplacementResult {
|
|
256
|
+
return this.#previewScopeReplacement(this.#domain.collectTaskSearchScope(taskId), searchText, replacementText, fields);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
applyTaskReplacement(
|
|
260
|
+
taskId: string,
|
|
261
|
+
searchText: string,
|
|
262
|
+
replacementText: string,
|
|
263
|
+
fields: readonly SearchField[],
|
|
264
|
+
): ScopeReplacementResult {
|
|
265
|
+
return this.#applyScopeReplacement(this.#domain.collectTaskSearchScope(taskId), searchText, replacementText, fields);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
previewSubtaskReplacement(
|
|
269
|
+
subtaskId: string,
|
|
270
|
+
searchText: string,
|
|
271
|
+
replacementText: string,
|
|
272
|
+
fields: readonly SearchField[],
|
|
273
|
+
): ScopeReplacementResult {
|
|
274
|
+
return this.#previewScopeReplacement(this.#domain.collectSubtaskSearchScope(subtaskId), searchText, replacementText, fields);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
applySubtaskReplacement(
|
|
278
|
+
subtaskId: string,
|
|
279
|
+
searchText: string,
|
|
280
|
+
replacementText: string,
|
|
281
|
+
fields: readonly SearchField[],
|
|
282
|
+
): ScopeReplacementResult {
|
|
283
|
+
return this.#applyScopeReplacement(this.#domain.collectSubtaskSearchScope(subtaskId), searchText, replacementText, fields);
|
|
284
|
+
}
|
|
285
|
+
|
|
156
286
|
#appendEntityEvent(
|
|
157
287
|
entityKind: "epic" | "task" | "subtask" | "dependency",
|
|
158
288
|
entityId: string,
|
|
@@ -166,4 +296,115 @@ export class MutationService {
|
|
|
166
296
|
fields,
|
|
167
297
|
});
|
|
168
298
|
}
|
|
299
|
+
|
|
300
|
+
#previewScopeReplacement(
|
|
301
|
+
nodes: readonly SearchNode[],
|
|
302
|
+
searchText: string,
|
|
303
|
+
replacementText: string,
|
|
304
|
+
fields: readonly SearchField[],
|
|
305
|
+
): ScopeReplacementResult {
|
|
306
|
+
return this.#buildScopeReplacementResult(nodes, searchText, replacementText, fields);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#applyScopeReplacement(
|
|
310
|
+
nodes: readonly SearchNode[],
|
|
311
|
+
searchText: string,
|
|
312
|
+
replacementText: string,
|
|
313
|
+
fields: readonly SearchField[],
|
|
314
|
+
): ScopeReplacementResult {
|
|
315
|
+
const result = this.#buildScopeReplacementResult(nodes, searchText, replacementText, fields, "apply");
|
|
316
|
+
|
|
317
|
+
this.#db.transaction((): void => {
|
|
318
|
+
for (const node of nodes) {
|
|
319
|
+
const nextTitle = fields.includes("title") ? replaceMatches(node.title, searchText, replacementText) : node.title;
|
|
320
|
+
const nextDescription = fields.includes("description")
|
|
321
|
+
? replaceMatches(node.description, searchText, replacementText)
|
|
322
|
+
: node.description;
|
|
323
|
+
|
|
324
|
+
if (nextTitle === node.title && nextDescription === node.description) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (node.kind === "epic") {
|
|
329
|
+
const epic = this.#domain.updateEpic(node.id, { title: nextTitle, description: nextDescription });
|
|
330
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.updated, {
|
|
331
|
+
title: epic.title,
|
|
332
|
+
description: epic.description,
|
|
333
|
+
status: epic.status,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (node.kind === "task") {
|
|
339
|
+
const task = this.#domain.updateTask(node.id, { title: nextTitle, description: nextDescription });
|
|
340
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.updated, {
|
|
341
|
+
epic_id: task.epicId,
|
|
342
|
+
title: task.title,
|
|
343
|
+
description: task.description,
|
|
344
|
+
status: task.status,
|
|
345
|
+
});
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const subtask = this.#domain.updateSubtask(node.id, { title: nextTitle, description: nextDescription });
|
|
350
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.updated, {
|
|
351
|
+
task_id: subtask.taskId,
|
|
352
|
+
title: subtask.title,
|
|
353
|
+
description: subtask.description,
|
|
354
|
+
status: subtask.status,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
})();
|
|
358
|
+
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#buildScopeReplacementResult(
|
|
363
|
+
nodes: readonly SearchNode[],
|
|
364
|
+
searchText: string,
|
|
365
|
+
replacementText: string,
|
|
366
|
+
fields: readonly SearchField[],
|
|
367
|
+
mode: "preview" | "apply" = "preview",
|
|
368
|
+
): ScopeReplacementResult {
|
|
369
|
+
const matches: SearchEntityMatch[] = [];
|
|
370
|
+
|
|
371
|
+
for (const node of nodes) {
|
|
372
|
+
const fieldMatches = fields
|
|
373
|
+
.map((field) => {
|
|
374
|
+
const value = field === "title" ? node.title : node.description;
|
|
375
|
+
const matchIndex = value.indexOf(searchText);
|
|
376
|
+
const nextValue = replaceMatches(value, searchText, replacementText);
|
|
377
|
+
const count = nextValue === value ? 0 : countMatches(value, searchText);
|
|
378
|
+
|
|
379
|
+
if (count === 0) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
field,
|
|
385
|
+
count,
|
|
386
|
+
snippet:
|
|
387
|
+
mode === "apply"
|
|
388
|
+
? buildReplacementSnippet(nextValue, matchIndex, replacementText.length)
|
|
389
|
+
: buildMatchSnippet(value, searchText),
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
.filter((fieldMatch) => fieldMatch !== null);
|
|
393
|
+
|
|
394
|
+
if (fieldMatches.length === 0) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
matches.push({
|
|
399
|
+
kind: node.kind,
|
|
400
|
+
id: node.id,
|
|
401
|
+
fields: fieldMatches,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
matches,
|
|
407
|
+
summary: summarizeMatches(matches),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
169
410
|
}
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type EpicTree,
|
|
11
11
|
type NodeKind,
|
|
12
12
|
type ReverseDependencyNode,
|
|
13
|
+
type SearchEntityMatch,
|
|
14
|
+
type SearchField,
|
|
15
|
+
type SearchFieldMatch,
|
|
16
|
+
type SearchNode,
|
|
17
|
+
type SearchSummary,
|
|
13
18
|
type SubtaskRecord,
|
|
14
19
|
type TaskTreeDetailed,
|
|
15
20
|
type TaskRecord,
|
|
@@ -125,6 +130,55 @@ function mapDependency(row: DependencyRow): DependencyRecord {
|
|
|
125
130
|
};
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
function countMatches(value: string, searchText: string): number {
|
|
134
|
+
if (searchText.length === 0) {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let count = 0;
|
|
139
|
+
let offset = 0;
|
|
140
|
+
while (offset <= value.length - searchText.length) {
|
|
141
|
+
const nextIndex = value.indexOf(searchText, offset);
|
|
142
|
+
if (nextIndex === -1) {
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
count += 1;
|
|
147
|
+
offset = nextIndex + searchText.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return count;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildMatchSnippet(value: string, searchText: string, contextSize = 24): string {
|
|
154
|
+
if (searchText.length === 0) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const matchIndex = value.indexOf(searchText);
|
|
159
|
+
if (matchIndex === -1) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const start = Math.max(0, matchIndex - contextSize);
|
|
164
|
+
const end = Math.min(value.length, matchIndex + searchText.length + contextSize);
|
|
165
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
166
|
+
const prefix = start > 0 ? "…" : "";
|
|
167
|
+
const suffix = end < value.length ? "…" : "";
|
|
168
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function summarizeMatches(matches: readonly SearchEntityMatch[]): SearchSummary {
|
|
172
|
+
return {
|
|
173
|
+
matchedEntities: matches.length,
|
|
174
|
+
matchedFields: matches.reduce((total, match) => total + match.fields.length, 0),
|
|
175
|
+
totalMatches: matches.reduce(
|
|
176
|
+
(total, match) => total + match.fields.reduce((fieldTotal, field) => fieldTotal + field.count, 0),
|
|
177
|
+
0,
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
128
182
|
export class TrackerDomain {
|
|
129
183
|
readonly #db: Database;
|
|
130
184
|
|
|
@@ -433,6 +487,89 @@ export class TrackerDomain {
|
|
|
433
487
|
};
|
|
434
488
|
}
|
|
435
489
|
|
|
490
|
+
collectEpicSearchScope(epicId: string): readonly SearchNode[] {
|
|
491
|
+
const tree = this.buildEpicTreeDetailed(epicId);
|
|
492
|
+
|
|
493
|
+
return [
|
|
494
|
+
{
|
|
495
|
+
kind: "epic",
|
|
496
|
+
id: tree.id,
|
|
497
|
+
title: tree.title,
|
|
498
|
+
description: tree.description,
|
|
499
|
+
},
|
|
500
|
+
...tree.tasks.flatMap((task) => [
|
|
501
|
+
{
|
|
502
|
+
kind: "task" as const,
|
|
503
|
+
id: task.id,
|
|
504
|
+
title: task.title,
|
|
505
|
+
description: task.description,
|
|
506
|
+
},
|
|
507
|
+
...task.subtasks.map((subtask) => ({
|
|
508
|
+
kind: "subtask" as const,
|
|
509
|
+
id: subtask.id,
|
|
510
|
+
title: subtask.title,
|
|
511
|
+
description: subtask.description,
|
|
512
|
+
})),
|
|
513
|
+
]),
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
collectTaskSearchScope(taskId: string): readonly SearchNode[] {
|
|
518
|
+
const tree = this.buildTaskTreeDetailed(taskId);
|
|
519
|
+
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
kind: "task",
|
|
523
|
+
id: tree.id,
|
|
524
|
+
title: tree.title,
|
|
525
|
+
description: tree.description,
|
|
526
|
+
},
|
|
527
|
+
...tree.subtasks.map((subtask) => ({
|
|
528
|
+
kind: "subtask" as const,
|
|
529
|
+
id: subtask.id,
|
|
530
|
+
title: subtask.title,
|
|
531
|
+
description: subtask.description,
|
|
532
|
+
})),
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
collectSubtaskSearchScope(subtaskId: string): readonly SearchNode[] {
|
|
537
|
+
const subtask = this.getSubtaskOrThrow(subtaskId);
|
|
538
|
+
|
|
539
|
+
return [
|
|
540
|
+
{
|
|
541
|
+
kind: "subtask",
|
|
542
|
+
id: subtask.id,
|
|
543
|
+
title: subtask.title,
|
|
544
|
+
description: subtask.description,
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
searchEpicScope(epicId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
550
|
+
const matches = this.collectSearchMatches(this.collectEpicSearchScope(epicId), searchText, fields);
|
|
551
|
+
return {
|
|
552
|
+
matches,
|
|
553
|
+
summary: summarizeMatches(matches),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
searchTaskScope(taskId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
558
|
+
const matches = this.collectSearchMatches(this.collectTaskSearchScope(taskId), searchText, fields);
|
|
559
|
+
return {
|
|
560
|
+
matches,
|
|
561
|
+
summary: summarizeMatches(matches),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
searchSubtaskScope(subtaskId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
566
|
+
const matches = this.collectSearchMatches(this.collectSubtaskSearchScope(subtaskId), searchText, fields);
|
|
567
|
+
return {
|
|
568
|
+
matches,
|
|
569
|
+
summary: summarizeMatches(matches),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
436
573
|
resolveNodeKind(id: string): "task" | "subtask" {
|
|
437
574
|
const task = this.getTask(id);
|
|
438
575
|
if (task) {
|
|
@@ -570,6 +707,40 @@ export class TrackerDomain {
|
|
|
570
707
|
return mapDependency(row);
|
|
571
708
|
}
|
|
572
709
|
|
|
710
|
+
private collectSearchMatches(
|
|
711
|
+
nodes: readonly SearchNode[],
|
|
712
|
+
searchText: string,
|
|
713
|
+
fields: readonly SearchField[],
|
|
714
|
+
): readonly SearchEntityMatch[] {
|
|
715
|
+
const matches: SearchEntityMatch[] = [];
|
|
716
|
+
|
|
717
|
+
for (const node of nodes) {
|
|
718
|
+
const matchedFields: SearchFieldMatch[] = [];
|
|
719
|
+
for (const field of fields) {
|
|
720
|
+
const count = countMatches(node[field], searchText);
|
|
721
|
+
if (count > 0) {
|
|
722
|
+
matchedFields.push({
|
|
723
|
+
field,
|
|
724
|
+
count,
|
|
725
|
+
snippet: buildMatchSnippet(node[field], searchText),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (matchedFields.length === 0) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
matches.push({
|
|
735
|
+
kind: node.kind,
|
|
736
|
+
id: node.id,
|
|
737
|
+
fields: matchedFields,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return matches;
|
|
742
|
+
}
|
|
743
|
+
|
|
573
744
|
private wouldCreateCycle(sourceId: string, dependsOnId: string): boolean {
|
|
574
745
|
const row = this.#db
|
|
575
746
|
.query(
|
package/src/domain/types.ts
CHANGED
|
@@ -85,6 +85,33 @@ export interface EpicTreeDetailed {
|
|
|
85
85
|
readonly tasks: ReadonlyArray<TaskTreeDetailed>;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export type SearchField = "title" | "description";
|
|
89
|
+
|
|
90
|
+
export interface SearchFieldMatch {
|
|
91
|
+
readonly field: SearchField;
|
|
92
|
+
readonly count: number;
|
|
93
|
+
readonly snippet: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SearchEntityMatch {
|
|
97
|
+
readonly kind: NodeKind;
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly fields: readonly SearchFieldMatch[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SearchSummary {
|
|
103
|
+
readonly matchedEntities: number;
|
|
104
|
+
readonly matchedFields: number;
|
|
105
|
+
readonly totalMatches: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SearchNode {
|
|
109
|
+
readonly kind: NodeKind;
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly title: string;
|
|
112
|
+
readonly description: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
export interface DomainErrorShape {
|
|
89
116
|
readonly code: string;
|
|
90
117
|
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(
|