engine-dj-mcp 0.11.2 → 0.12.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.
@@ -6,6 +6,44 @@ export interface CreatePlaylistResult {
6
6
  tracks_added: number;
7
7
  backup_path: string;
8
8
  }
9
+ /**
10
+ * What editing an existing playlist's entries returns. One shape for
11
+ * add/remove/reorder alike -- each op leaves the fields it did not touch
12
+ * undefined rather than the module growing a result type per verb.
13
+ */
14
+ export interface EditResult {
15
+ playlist_id: number;
16
+ tracks_added?: number;
17
+ tracks_removed?: number;
18
+ positions?: number[];
19
+ removed?: {
20
+ position: number;
21
+ track_id: number | null;
22
+ }[];
23
+ undo: UndoStep[];
24
+ /**
25
+ * Whether replaying every step of `undo` puts the playlist back exactly as
26
+ * it was. Always present, never inferred from `undo`'s length: a client
27
+ * that read a missing field as false -- or a full-looking `undo` as
28
+ * complete -- would get the one question that matters here backwards.
29
+ *
30
+ * False only for removeTracksFromPlaylist, and only for a removal that
31
+ * included an entry whose stored origin pair matches no track in this
32
+ * library. Such an entry has no track id to hand back to
33
+ * add_tracks_to_playlist, so no undo step for it can exist; `undo_note`
34
+ * then names those positions. The steps that *are* emitted still run, and
35
+ * still restore everything else to its original position.
36
+ */
37
+ undo_complete: boolean;
38
+ /** Set only when `undo_complete` is false: which positions have no way back, and why. */
39
+ undo_note?: string;
40
+ backup_path: string;
41
+ }
42
+ /** One step of the tool call that would undo an edit, in the shape a client replays it. */
43
+ export interface UndoStep {
44
+ tool: string;
45
+ arguments: Record<string, unknown>;
46
+ }
9
47
  export interface OriginRef {
10
48
  uuid: string;
11
49
  trackId: number;
@@ -28,6 +66,48 @@ export declare function resetSessionSnapshots(): void;
28
66
  * re-originated-track test is for, not this readback.
29
67
  */
30
68
  export declare function walkFrom(db: DatabaseSync, listId: number, headId: number): OriginRef[];
69
+ /**
70
+ * One playlist's `PlaylistEntity` rows, in the shape `checkChain` wants: an
71
+ * entry id and the id it links to (0 for "links to nothing").
72
+ *
73
+ * Shared by every op that edits an *existing* playlist's entries --
74
+ * createPlaylist never calls this, because it builds a chain from nothing
75
+ * rather than reading one back.
76
+ */
77
+ export declare function readChain(db: DatabaseSync, listId: number): {
78
+ id: number;
79
+ next: number;
80
+ }[];
81
+ export interface ChainCheck {
82
+ ok: boolean;
83
+ reason?: string;
84
+ /** Entry ids head to tail. Meaningful only when `ok`. */
85
+ order: number[];
86
+ }
87
+ /**
88
+ * Whether one playlist's entry chain is sound enough to edit.
89
+ *
90
+ * Deliberately not `orderByChain` from src/playlists.ts. That function's
91
+ * contract is that it returns every node it was given, degrading a damaged
92
+ * chain to a warning, because a reader that silently returned 30 of 43
93
+ * entries would be worse than one that guesses an order and says so. An edit
94
+ * needs the opposite: a yes or no.
95
+ *
96
+ * Four conditions, and each is needed because different breakages trip
97
+ * different ones. Checking only that the walk covered every row is the trap:
98
+ * a list severed into two runs has two heads, and walking from both covers
99
+ * everything -- measured, on a chain broken on purpose, as "5 of 5" while the
100
+ * walk from the real head reached 2. Checking coverage without also checking
101
+ * that the walk *ended* is a second, subtler version of the same trap: a row
102
+ * whose next points back into an already-linked interior row (two
103
+ * predecessors, no row pointing at 0) can visit every row and still never
104
+ * terminate -- the walk stops only because it revisits a row it has already
105
+ * seen, not because it reached the end.
106
+ */
107
+ export declare function checkChain(rows: {
108
+ id: number;
109
+ next: number;
110
+ }[]): ChainCheck;
31
111
  export declare function sameOrder(a: OriginRef[], b: OriginRef[]): boolean;
32
112
  export declare function createPlaylist(mdbPath: string, uuid: string, input: {
33
113
  title: string;
@@ -35,3 +115,92 @@ export declare function createPlaylist(mdbPath: string, uuid: string, input: {
35
115
  }, opts: {
36
116
  backupDir: string;
37
117
  }): Promise<CreatePlaylistResult | EngineError>;
118
+ /** Where `addTracksToPlaylist`'s `at` can put the new run. */
119
+ export type InsertAt = "end" | "start" | {
120
+ after_position: number;
121
+ };
122
+ /**
123
+ * Adds one or more tracks to an existing playlist, at the start, the end, or
124
+ * after a named position in its current order.
125
+ *
126
+ * Shares its skeleton with createPlaylist: a read-only pre-check first (cheap
127
+ * enough to rule out the common failure modes without ever opening the
128
+ * library for writing), then withWriteTransaction. Where createPlaylist
129
+ * builds a chain from nothing, this extends one that already exists, so it
130
+ * also has to confirm that chain is sound before it touches it -- twice. The
131
+ * pre-check gates it once, both to fail fast (and skip the snapshot) for a
132
+ * playlist that cannot be edited at all, and because validating `at` needs
133
+ * to know how many entries the playlist currently has. The transaction gates
134
+ * it again after BEGIN IMMEDIATE, because that lock is the first moment
135
+ * nothing else can change the chain -- gating on the pre-check's read alone,
136
+ * or reusing the insert position it computed, would be trusting one that
137
+ * could already be stale.
138
+ */
139
+ export declare function addTracksToPlaylist(mdbPath: string, uuid: string, input: {
140
+ listId: number;
141
+ trackIds: number[];
142
+ at: InsertAt;
143
+ }, opts: {
144
+ backupDir: string;
145
+ }): Promise<EditResult | EngineError>;
146
+ /**
147
+ * Removes one or more tracks from an existing playlist by their current
148
+ * position.
149
+ *
150
+ * `trigger_before_delete_PlaylistEntity` (see gen-library.ts's copy of it,
151
+ * taken verbatim from a real 3.0.2 library) relinks each deleted row's
152
+ * predecessor onto its successor as SQLite processes the delete -- verified
153
+ * for a row removed at the head, the middle and the tail, and, by
154
+ * construction of the trigger itself, for a batch that removes several
155
+ * rows, adjacent or not, in one statement. So this function does no chain
156
+ * maintenance of its own; writing any would just be fighting Engine's own
157
+ * trigger. What it does own is checking that the trigger's job actually
158
+ * landed: its `WHEN OLD.trackId > 0` means a row with trackId <= 0 is
159
+ * deleted *without* relinking, leaving its predecessor pointing at a row
160
+ * that is now gone. No real library measured has such a row, but the
161
+ * post-delete check below is the only thing that would ever notice one --
162
+ * and it checks the surviving order against what was expected, not only
163
+ * that some sound chain is left, because "sound" and "right" are different
164
+ * questions and only the second one is what the caller asked for.
165
+ *
166
+ * Shares createPlaylist/addTracksToPlaylist's skeleton: a read-only
167
+ * pre-check first, then withWriteTransaction.
168
+ */
169
+ export declare function removeTracksFromPlaylist(mdbPath: string, uuid: string, input: {
170
+ listId: number;
171
+ positions: number[];
172
+ expectTrackIds?: (number | null)[];
173
+ }, opts: {
174
+ backupDir: string;
175
+ }): Promise<EditResult | EngineError>;
176
+ /**
177
+ * Reorders an existing playlist's entries to a caller-given permutation of
178
+ * its current order.
179
+ *
180
+ * Unlike add/remove, this rewrites links only -- no INSERT, no DELETE -- so
181
+ * none of Engine's PlaylistEntity triggers fire and there is no trigger
182
+ * behaviour to trust or verify here, only the links this function writes
183
+ * itself. `order[i]` names the *current* 1-based position of the track that
184
+ * should end up at position `i + 1` (see validatePermutation for why the
185
+ * spec takes a full permutation rather than a move instruction). Only the
186
+ * entries whose successor actually changes get an UPDATE -- measured on a
187
+ * real library, moving an entry from the middle to the front took exactly
188
+ * two updates, and the identity permutation writes no PlaylistEntity row at
189
+ * all. It is still not a no-op: like every other op here it stamps
190
+ * `Playlist.lastEditTime`, and the session's snapshot is copied before the
191
+ * body ever runs, so the identity case costs a timestamp and (once per
192
+ * session) a snapshot. Left that way deliberately -- "did this permutation
193
+ * change anything" can only be answered honestly after BEGIN IMMEDIATE, by
194
+ * which point the snapshot is already taken, and an edit that reports
195
+ * success without touching lastEditTime would be the one op whose result
196
+ * Engine cannot see.
197
+ *
198
+ * Shares createPlaylist/addTracksToPlaylist/removeTracksFromPlaylist's
199
+ * skeleton: a read-only pre-check first, then withWriteTransaction.
200
+ */
201
+ export declare function reorderPlaylist(mdbPath: string, uuid: string, input: {
202
+ listId: number;
203
+ order: number[];
204
+ }, opts: {
205
+ backupDir: string;
206
+ }): Promise<EditResult | EngineError>;