trekoon 0.1.4 → 0.1.6

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,7 +1,8 @@
1
- import { hasFlag, parseArgs, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
1
+ import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
2
2
 
3
- import { DomainError, type SubtaskRecord } from "../domain/types";
3
+ import { MutationService } from "../domain/mutation-service";
4
4
  import { TrackerDomain } from "../domain/tracker-domain";
5
+ import { DomainError, type SubtaskRecord } from "../domain/types";
5
6
  import { formatHumanTable } from "../io/human-table";
6
7
  import { failResult, okResult } from "../io/output";
7
8
  import { type CliContext, type CliResult } from "../runtime/command-types";
@@ -12,6 +13,8 @@ function formatSubtask(subtask: SubtaskRecord): string {
12
13
  }
13
14
 
14
15
  const VIEW_MODES = ["table", "compact"] as const;
16
+ const DEFAULT_SUBTASK_LIST_LIMIT = 10;
17
+ const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
15
18
 
16
19
  function parseIdsOption(rawIds: string | undefined): string[] {
17
20
  if (rawIds === undefined) {
@@ -24,6 +27,45 @@ function parseIdsOption(rawIds: string | undefined): string[] {
24
27
  .filter((value) => value.length > 0);
25
28
  }
26
29
 
30
+ function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
31
+ if (rawStatuses === undefined) {
32
+ return undefined;
33
+ }
34
+
35
+ return rawStatuses
36
+ .split(",")
37
+ .map((value) => value.trim())
38
+ .filter((value) => value.length > 0);
39
+ }
40
+
41
+ function subtaskStatusPriority(status: string): number {
42
+ if (status === "in_progress" || status === "in-progress") {
43
+ return 0;
44
+ }
45
+
46
+ if (status === "todo") {
47
+ return 1;
48
+ }
49
+
50
+ return 2;
51
+ }
52
+
53
+ function filterSortAndLimitSubtasks(
54
+ subtasks: readonly SubtaskRecord[],
55
+ statuses: readonly string[] | undefined,
56
+ limit: number | undefined,
57
+ ): SubtaskRecord[] {
58
+ const allowedStatuses = statuses === undefined ? undefined : new Set(statuses);
59
+ const filtered = allowedStatuses === undefined ? [...subtasks] : subtasks.filter((subtask) => allowedStatuses.has(subtask.status));
60
+ const sorted = [...filtered].sort((left, right) => subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status));
61
+
62
+ if (limit === undefined) {
63
+ return sorted;
64
+ }
65
+
66
+ return sorted.slice(0, limit);
67
+ }
68
+
27
69
  function appendLine(existing: string, line: string): string {
28
70
  return existing.length > 0 ? `${existing}\n${line}` : line;
29
71
  }
@@ -85,6 +127,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
85
127
  const parsed = parseArgs(context.args);
86
128
  const subcommand: string | undefined = parsed.positional[0];
87
129
  const domain = new TrackerDomain(database.db);
130
+ const mutations = new MutationService(database.db, context.cwd);
88
131
 
89
132
  switch (subcommand) {
90
133
  case "create": {
@@ -100,7 +143,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
100
143
  const title: string | undefined = readOption(parsed.options, "title") ?? parsed.positional[2];
101
144
  const description: string | undefined = readOption(parsed.options, "description", "d");
102
145
  const status: string | undefined = readOption(parsed.options, "status", "s");
103
- const subtask = domain.createSubtask({
146
+ const subtask = mutations.createSubtask({
104
147
  taskId: taskId ?? "",
105
148
  title: title ?? "",
106
149
  description,
@@ -116,6 +159,8 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
116
159
  case "list": {
117
160
  const missingListOption =
118
161
  readMissingOptionValue(parsed.missingOptionValues, "view") ??
162
+ readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
163
+ readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
119
164
  readMissingOptionValue(parsed.missingOptionValues, "task", "t");
120
165
  if (missingListOption !== undefined) {
121
166
  return failMissingOptionValue("subtask.list", missingListOption);
@@ -123,6 +168,10 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
123
168
 
124
169
  const rawView: string | undefined = readOption(parsed.options, "view");
125
170
  const view = readEnumOption(parsed.options, VIEW_MODES, "view");
171
+ const includeAll = hasFlag(parsed.flags, "all");
172
+ const rawStatuses = readOption(parsed.options, "status", "s");
173
+ const rawLimit = readOption(parsed.options, "limit", "l");
174
+
126
175
  if (rawView !== undefined && view === undefined) {
127
176
  return failResult({
128
177
  command: "subtask.list",
@@ -135,8 +184,64 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
135
184
  });
136
185
  }
137
186
 
187
+ if (includeAll && rawStatuses !== undefined) {
188
+ return failResult({
189
+ command: "subtask.list",
190
+ human: "Use either --all or --status, not both.",
191
+ data: { code: "invalid_input", flags: ["all", "status"] },
192
+ error: {
193
+ code: "invalid_input",
194
+ message: "--all and --status are mutually exclusive",
195
+ },
196
+ });
197
+ }
198
+
199
+ if (includeAll && rawLimit !== undefined) {
200
+ return failResult({
201
+ command: "subtask.list",
202
+ human: "Use either --all or --limit, not both.",
203
+ data: { code: "invalid_input", flags: ["all", "limit"] },
204
+ error: {
205
+ code: "invalid_input",
206
+ message: "--all and --limit are mutually exclusive",
207
+ },
208
+ });
209
+ }
210
+
211
+ const statuses = parseStatusCsv(rawStatuses);
212
+ if (rawStatuses !== undefined && statuses !== undefined && statuses.length === 0) {
213
+ return failResult({
214
+ command: "subtask.list",
215
+ human: "Provide at least one status with --status.",
216
+ data: { code: "invalid_input", status: rawStatuses },
217
+ error: {
218
+ code: "invalid_input",
219
+ message: "Invalid --status value",
220
+ },
221
+ });
222
+ }
223
+
224
+ const parsedLimit = parseStrictPositiveInt(rawLimit);
225
+ if (Number.isNaN(parsedLimit)) {
226
+ return failResult({
227
+ command: "subtask.list",
228
+ human: "Invalid --limit value. Use an integer >= 1.",
229
+ data: { code: "invalid_input", limit: rawLimit },
230
+ error: {
231
+ code: "invalid_input",
232
+ message: "Invalid --limit value",
233
+ },
234
+ });
235
+ }
236
+
138
237
  const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
139
- const subtasks = domain.listSubtasks(taskId);
238
+ const selectedStatuses = includeAll
239
+ ? undefined
240
+ : statuses ?? [...DEFAULT_OPEN_SUBTASK_STATUSES];
241
+ const selectedLimit = includeAll
242
+ ? undefined
243
+ : parsedLimit ?? DEFAULT_SUBTASK_LIST_LIMIT;
244
+ const subtasks = filterSortAndLimitSubtasks(domain.listSubtasks(taskId), selectedStatuses, selectedLimit);
140
245
  const listView = view ?? "table";
141
246
  const human =
142
247
  subtasks.length === 0
@@ -234,7 +339,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
234
339
 
235
340
  const targets = updateAll ? [...domain.listSubtasks()] : ids.map((id) => domain.getSubtaskOrThrow(id));
236
341
  const subtasks = targets.map((target) =>
237
- domain.updateSubtask(target.id, {
342
+ mutations.updateSubtask(target.id, {
238
343
  status,
239
344
  description: append === undefined ? undefined : appendLine(target.description, append),
240
345
  }),
@@ -267,7 +372,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
267
372
  append === undefined
268
373
  ? description
269
374
  : appendLine(domain.getSubtaskOrThrow(subtaskId).description, append);
270
- const subtask = domain.updateSubtask(subtaskId, { title, description: nextDescription, status });
375
+ const subtask = mutations.updateSubtask(subtaskId, { title, description: nextDescription, status });
271
376
 
272
377
  return okResult({
273
378
  command: "subtask.update",
@@ -277,7 +382,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
277
382
  }
278
383
  case "delete": {
279
384
  const subtaskId: string = parsed.positional[1] ?? "";
280
- domain.deleteSubtask(subtaskId);
385
+ mutations.deleteSubtask(subtaskId);
281
386
 
282
387
  return okResult({
283
388
  command: "subtask.delete",
@@ -1,8 +1,8 @@
1
1
  import { failResult, okResult } from "../io/output";
2
2
  import { type CliContext, type CliResult } from "../runtime/command-types";
3
3
  import { MissingBranchDatabaseError } from "../sync/branch-db";
4
- import { syncPull, syncResolve, syncStatus } from "../sync/service";
5
- import { type SyncResolution } from "../sync/types";
4
+ import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
5
+ import { type SyncConflictMode, type SyncResolution } from "../sync/types";
6
6
 
7
7
  function parseOption(args: readonly string[], option: string): string | null {
8
8
  const index: number = args.indexOf(option);
@@ -17,7 +17,7 @@ function parseOption(args: readonly string[], option: string): string | null {
17
17
  function usage(message: string): CliResult {
18
18
  return failResult({
19
19
  command: "sync",
20
- human: `${message}\nUsage: trekoon sync <status|pull|resolve> [options]`,
20
+ human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
21
21
  data: { message },
22
22
  error: {
23
23
  code: "invalid_args",
@@ -35,6 +35,50 @@ function statusMessage(sourceBranch: string, ahead: number, behind: number, conf
35
35
  ].join("\n");
36
36
  }
37
37
 
38
+ function formatConflictList(
39
+ conflicts: ReadonlyArray<{
40
+ id: string;
41
+ entity_kind: string;
42
+ entity_id: string;
43
+ field_name: string;
44
+ resolution: string;
45
+ }>,
46
+ ): string {
47
+ if (conflicts.length === 0) {
48
+ return "No conflicts found.";
49
+ }
50
+
51
+ return conflicts
52
+ .map((conflict) =>
53
+ [
54
+ conflict.id,
55
+ conflict.entity_kind,
56
+ conflict.entity_id,
57
+ conflict.field_name,
58
+ conflict.resolution,
59
+ ].join(" | "),
60
+ )
61
+ .join("\n");
62
+ }
63
+
64
+ function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
65
+ const modeIndex = args.indexOf("--mode");
66
+ if (modeIndex < 0) {
67
+ return "pending";
68
+ }
69
+
70
+ const explicitMode = args[modeIndex + 1];
71
+ if (!explicitMode || explicitMode.startsWith("--")) {
72
+ return null;
73
+ }
74
+
75
+ if (explicitMode === "pending" || explicitMode === "all") {
76
+ return explicitMode;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
38
82
  export async function runSync(context: CliContext): Promise<CliResult> {
39
83
  const subcommand: string | undefined = context.args[0];
40
84
 
@@ -95,6 +139,57 @@ export async function runSync(context: CliContext): Promise<CliResult> {
95
139
  });
96
140
  }
97
141
 
142
+ if (subcommand === "conflicts") {
143
+ const conflictsCommand: string | undefined = context.args[1];
144
+ if (!conflictsCommand) {
145
+ return usage("sync conflicts requires list|show.");
146
+ }
147
+
148
+ if (conflictsCommand === "list") {
149
+ const mode = parseConflictMode(context.args);
150
+ if (!mode) {
151
+ return usage("sync conflicts list --mode only accepts pending|all.");
152
+ }
153
+
154
+ const conflicts = listSyncConflicts(context.cwd, mode);
155
+
156
+ return okResult({
157
+ command: "sync conflicts list",
158
+ human: formatConflictList(conflicts),
159
+ data: {
160
+ mode,
161
+ conflicts,
162
+ },
163
+ });
164
+ }
165
+
166
+ if (conflictsCommand === "show") {
167
+ const conflictId: string | undefined = context.args[2];
168
+ if (!conflictId) {
169
+ return usage("sync conflicts show requires <conflict-id>.");
170
+ }
171
+
172
+ const conflict = getSyncConflict(context.cwd, conflictId);
173
+
174
+ return okResult({
175
+ command: "sync conflicts show",
176
+ human: [
177
+ `Conflict: ${conflict.id}`,
178
+ `Entity: ${conflict.entityKind} ${conflict.entityId}`,
179
+ `Field: ${conflict.fieldName}`,
180
+ `Resolution: ${conflict.resolution}`,
181
+ `Ours: ${JSON.stringify(conflict.oursValue)}`,
182
+ `Theirs: ${JSON.stringify(conflict.theirsValue)}`,
183
+ ].join("\n"),
184
+ data: {
185
+ conflict,
186
+ },
187
+ });
188
+ }
189
+
190
+ return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`);
191
+ }
192
+
98
193
  return usage(`Unknown sync subcommand '${subcommand}'.`);
99
194
  } catch (error) {
100
195
  if (error instanceof MissingBranchDatabaseError) {
@@ -1,7 +1,8 @@
1
1
  import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
2
2
 
3
- import { DomainError, type TaskRecord } from "../domain/types";
3
+ import { MutationService } from "../domain/mutation-service";
4
4
  import { TrackerDomain } from "../domain/tracker-domain";
5
+ import { DomainError, type TaskRecord } from "../domain/types";
5
6
  import { formatHumanTable } from "../io/human-table";
6
7
  import { failResult, okResult } from "../io/output";
7
8
  import { type CliContext, type CliResult } from "../runtime/command-types";
@@ -192,6 +193,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
192
193
  const parsed = parseArgs(context.args);
193
194
  const subcommand: string | undefined = parsed.positional[0];
194
195
  const domain = new TrackerDomain(database.db);
196
+ const mutations = new MutationService(database.db, context.cwd);
195
197
 
196
198
  switch (subcommand) {
197
199
  case "create": {
@@ -207,7 +209,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
207
209
  const title: string | undefined = readOption(parsed.options, "title", "t");
208
210
  const description: string | undefined = readOption(parsed.options, "description", "d");
209
211
  const status: string | undefined = readOption(parsed.options, "status", "s");
210
- const task = domain.createTask({
212
+ const task = mutations.createTask({
211
213
  epicId: epicId ?? "",
212
214
  title: title ?? "",
213
215
  description: description ?? "",
@@ -482,7 +484,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
482
484
 
483
485
  const targets = updateAll ? [...domain.listTasks()] : ids.map((id) => domain.getTaskOrThrow(id));
484
486
  const tasks = targets.map((target) =>
485
- domain.updateTask(target.id, {
487
+ mutations.updateTask(target.id, {
486
488
  status,
487
489
  description: append === undefined ? undefined : appendLine(target.description, append),
488
490
  }),
@@ -515,7 +517,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
515
517
  append === undefined
516
518
  ? description
517
519
  : appendLine(domain.getTaskOrThrow(taskId).description, append);
518
- const task = domain.updateTask(taskId, { title, description: nextDescription, status });
520
+ const task = mutations.updateTask(taskId, { title, description: nextDescription, status });
519
521
 
520
522
  return okResult({
521
523
  command: "task.update",
@@ -525,7 +527,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
525
527
  }
526
528
  case "delete": {
527
529
  const taskId: string = parsed.positional[1] ?? "";
528
- domain.deleteTask(taskId);
530
+ mutations.deleteTask(taskId);
529
531
 
530
532
  return okResult({
531
533
  command: "task.delete",
@@ -0,0 +1,27 @@
1
+ export const ENTITY_OPERATIONS = {
2
+ epic: {
3
+ created: "epic.created",
4
+ updated: "epic.updated",
5
+ deleted: "epic.deleted",
6
+ },
7
+ task: {
8
+ created: "task.created",
9
+ updated: "task.updated",
10
+ deleted: "task.deleted",
11
+ },
12
+ subtask: {
13
+ created: "subtask.created",
14
+ updated: "subtask.updated",
15
+ deleted: "subtask.deleted",
16
+ },
17
+ dependency: {
18
+ added: "dependency.added",
19
+ removed: "dependency.removed",
20
+ },
21
+ } as const;
22
+
23
+ export type MutationOperation =
24
+ | (typeof ENTITY_OPERATIONS)["epic"][keyof (typeof ENTITY_OPERATIONS)["epic"]]
25
+ | (typeof ENTITY_OPERATIONS)["task"][keyof (typeof ENTITY_OPERATIONS)["task"]]
26
+ | (typeof ENTITY_OPERATIONS)["subtask"][keyof (typeof ENTITY_OPERATIONS)["subtask"]]
27
+ | (typeof ENTITY_OPERATIONS)["dependency"][keyof (typeof ENTITY_OPERATIONS)["dependency"]];
@@ -0,0 +1,169 @@
1
+ import { type Database } from "bun:sqlite";
2
+
3
+ import { appendEventWithGitContext } from "../sync/event-writes";
4
+ import { ENTITY_OPERATIONS } from "./mutation-operations";
5
+ import { TrackerDomain } from "./tracker-domain";
6
+ import { type DependencyRecord, type EpicRecord, type SubtaskRecord, type TaskRecord } from "./types";
7
+
8
+ export class MutationService {
9
+ readonly #db: Database;
10
+ readonly #cwd: string;
11
+ readonly #domain: TrackerDomain;
12
+
13
+ constructor(db: Database, cwd: string) {
14
+ this.#db = db;
15
+ this.#cwd = cwd;
16
+ this.#domain = new TrackerDomain(db);
17
+ }
18
+
19
+ createEpic(input: { title: string; description: string; status?: string | undefined }): EpicRecord {
20
+ return this.#db.transaction((): EpicRecord => {
21
+ const epic = this.#domain.createEpic(input);
22
+ this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.created, {
23
+ title: epic.title,
24
+ description: epic.description,
25
+ status: epic.status,
26
+ });
27
+ return epic;
28
+ })();
29
+ }
30
+
31
+ updateEpic(
32
+ id: string,
33
+ input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
34
+ ): EpicRecord {
35
+ return this.#db.transaction((): EpicRecord => {
36
+ const epic = this.#domain.updateEpic(id, input);
37
+ this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.updated, {
38
+ title: epic.title,
39
+ description: epic.description,
40
+ status: epic.status,
41
+ });
42
+ return epic;
43
+ })();
44
+ }
45
+
46
+ deleteEpic(id: string): void {
47
+ this.#db.transaction((): void => {
48
+ this.#domain.deleteEpic(id);
49
+ this.#appendEntityEvent("epic", id, ENTITY_OPERATIONS.epic.deleted, {});
50
+ })();
51
+ }
52
+
53
+ createTask(input: { epicId: string; title: string; description: string; status?: string | undefined }): TaskRecord {
54
+ return this.#db.transaction((): TaskRecord => {
55
+ const task = this.#domain.createTask(input);
56
+ this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
57
+ epic_id: task.epicId,
58
+ title: task.title,
59
+ description: task.description,
60
+ status: task.status,
61
+ });
62
+ return task;
63
+ })();
64
+ }
65
+
66
+ updateTask(
67
+ id: string,
68
+ input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
69
+ ): TaskRecord {
70
+ return this.#db.transaction((): TaskRecord => {
71
+ const task = this.#domain.updateTask(id, input);
72
+ this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.updated, {
73
+ epic_id: task.epicId,
74
+ title: task.title,
75
+ description: task.description,
76
+ status: task.status,
77
+ });
78
+ return task;
79
+ })();
80
+ }
81
+
82
+ deleteTask(id: string): void {
83
+ this.#db.transaction((): void => {
84
+ this.#domain.deleteTask(id);
85
+ this.#appendEntityEvent("task", id, ENTITY_OPERATIONS.task.deleted, {});
86
+ })();
87
+ }
88
+
89
+ createSubtask(input: {
90
+ taskId: string;
91
+ title: string;
92
+ description?: string | undefined;
93
+ status?: string | undefined;
94
+ }): SubtaskRecord {
95
+ return this.#db.transaction((): SubtaskRecord => {
96
+ const subtask = this.#domain.createSubtask(input);
97
+ this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
98
+ task_id: subtask.taskId,
99
+ title: subtask.title,
100
+ description: subtask.description,
101
+ status: subtask.status,
102
+ });
103
+ return subtask;
104
+ })();
105
+ }
106
+
107
+ updateSubtask(
108
+ id: string,
109
+ input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
110
+ ): SubtaskRecord {
111
+ return this.#db.transaction((): SubtaskRecord => {
112
+ const subtask = this.#domain.updateSubtask(id, input);
113
+ this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.updated, {
114
+ task_id: subtask.taskId,
115
+ title: subtask.title,
116
+ description: subtask.description,
117
+ status: subtask.status,
118
+ });
119
+ return subtask;
120
+ })();
121
+ }
122
+
123
+ deleteSubtask(id: string): void {
124
+ this.#db.transaction((): void => {
125
+ this.#domain.deleteSubtask(id);
126
+ this.#appendEntityEvent("subtask", id, ENTITY_OPERATIONS.subtask.deleted, {});
127
+ })();
128
+ }
129
+
130
+ addDependency(sourceId: string, dependsOnId: string): DependencyRecord {
131
+ return this.#db.transaction((): DependencyRecord => {
132
+ const dependency = this.#domain.addDependency(sourceId, dependsOnId);
133
+ this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
134
+ source_id: dependency.sourceId,
135
+ source_kind: dependency.sourceKind,
136
+ depends_on_id: dependency.dependsOnId,
137
+ depends_on_kind: dependency.dependsOnKind,
138
+ });
139
+ return dependency;
140
+ })();
141
+ }
142
+
143
+ removeDependency(sourceId: string, dependsOnId: string): number {
144
+ return this.#db.transaction((): number => {
145
+ const removed = this.#domain.removeDependency(sourceId, dependsOnId);
146
+ if (removed > 0) {
147
+ this.#appendEntityEvent("dependency", `${sourceId}->${dependsOnId}`, ENTITY_OPERATIONS.dependency.removed, {
148
+ source_id: sourceId,
149
+ depends_on_id: dependsOnId,
150
+ });
151
+ }
152
+ return removed;
153
+ })();
154
+ }
155
+
156
+ #appendEntityEvent(
157
+ entityKind: "epic" | "task" | "subtask" | "dependency",
158
+ entityId: string,
159
+ operation: string,
160
+ fields: Record<string, unknown>,
161
+ ): void {
162
+ appendEventWithGitContext(this.#db, this.#cwd, {
163
+ entityKind,
164
+ entityId,
165
+ operation,
166
+ fields,
167
+ });
168
+ }
169
+ }