@drewpayment/mink 0.11.0 → 0.12.0-beta.2
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/dashboard/out/404.html +1 -1
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +1 -1
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +1 -1
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +1 -1
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +1 -1
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +1 -1
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +1 -1
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +1 -1
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +1 -1
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +1 -1
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +1 -1
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +1 -1
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +1 -1
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +1 -1
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +1 -1
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +1 -1
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +1 -1
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +1 -1
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +1 -1
- package/dist/cli.bun.js +90615 -0
- package/dist/cli.js +27 -92464
- package/dist/cli.node.js +93945 -0
- package/package.json +13 -2
- package/scripts/build.mjs +50 -0
- package/scripts/cli-shim.mjs +39 -0
- package/src/commands/bug-search.ts +2 -4
- package/src/commands/detect-waste.ts +24 -32
- package/src/commands/post-read.ts +10 -11
- package/src/commands/post-write.ts +13 -19
- package/src/commands/pre-read.ts +19 -24
- package/src/commands/scan.ts +103 -40
- package/src/commands/status.ts +45 -26
- package/src/core/bug-memory.ts +32 -34
- package/src/core/dashboard-api.ts +44 -22
- package/src/core/index-store.ts +23 -0
- package/src/core/paths.ts +7 -0
- package/src/core/scanner.ts +8 -4
- package/src/core/state-aggregator.ts +64 -7
- package/src/core/state-counters.ts +11 -31
- package/src/core/sync-merge-drivers.ts +164 -1
- package/src/core/sync.ts +9 -0
- package/src/core/token-ledger.ts +50 -4
- package/src/repositories/bug-memory-repo.ts +268 -0
- package/src/repositories/counters-repo.ts +88 -0
- package/src/repositories/file-index-repo.ts +238 -0
- package/src/repositories/token-ledger-repo.ts +412 -0
- package/src/storage/db.ts +121 -0
- package/src/storage/driver.bun.ts +99 -0
- package/src/storage/driver.node.ts +107 -0
- package/src/storage/driver.ts +76 -0
- package/src/storage/migrate-json.ts +415 -0
- package/src/storage/schema.ts +207 -0
- package/src/types/file-index.ts +9 -0
- /package/dashboard/out/_next/static/{I7QxkFr_LXY4BWGjogOs1 → 7bx94K8a7-O53mwi7UoEu}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{I7QxkFr_LXY4BWGjogOs1 → 7bx94K8a7-O53mwi7UoEu}/_ssgManifest.js +0 -0
|
@@ -75,14 +75,29 @@ function addLifetime(target: LifetimeCounters, source: LifetimeCounters): void {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function aggregateTokenLedgerAt(projDir: string): TokenLedger {
|
|
78
|
+
// Phase 4: token_ledger lives in mink.db. The legacy JSON aggregation
|
|
79
|
+
// is preserved as a fallback for unit tests and pre-migration projects.
|
|
80
|
+
const dbPath = join(projDir, "mink.db");
|
|
81
|
+
if (existsSync(dbPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const { openDriver } = require("../storage/driver") as typeof import("../storage/driver");
|
|
84
|
+
const { applySchema } = require("../storage/schema") as typeof import("../storage/schema");
|
|
85
|
+
const { TokenLedgerRepo } = require("../repositories/token-ledger-repo") as typeof import("../repositories/token-ledger-repo");
|
|
86
|
+
const db = openDriver(dbPath);
|
|
87
|
+
try {
|
|
88
|
+
applySchema(db);
|
|
89
|
+
return new TokenLedgerRepo(db).snapshot();
|
|
90
|
+
} finally {
|
|
91
|
+
db.close();
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Fall through to JSON aggregation if the DB read fails.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
const merged = createEmptyLedger();
|
|
79
99
|
const seenSessions = new Set<string>();
|
|
80
100
|
|
|
81
|
-
// Sum lifetime counters from every source (each shard + legacy). Lifetime
|
|
82
|
-
// persists across archive cycles, so deriving from active sessions alone
|
|
83
|
-
// would lose archived totals. Migration atomically moves legacy → shard
|
|
84
|
-
// (`git mv`), so a session never lives in both simultaneously and lifetime
|
|
85
|
-
// counters do not double-count in production.
|
|
86
101
|
const sources = [
|
|
87
102
|
...listDeviceShardsAt(projDir).map((id) =>
|
|
88
103
|
shardPath(projDir, id, "token-ledger.json")
|
|
@@ -90,8 +105,6 @@ export function aggregateTokenLedgerAt(projDir: string): TokenLedger {
|
|
|
90
105
|
join(projDir, "token-ledger.json"),
|
|
91
106
|
];
|
|
92
107
|
|
|
93
|
-
// Track waste-flags across sources, deduped by (pattern, detectedAt) so
|
|
94
|
-
// each device's flags remain visible without spamming duplicates.
|
|
95
108
|
const seenFlagKeys = new Set<string>();
|
|
96
109
|
const wasteFlags: NonNullable<TokenLedger["wasteFlags"]> = [];
|
|
97
110
|
|
|
@@ -131,6 +144,25 @@ export function aggregateTokenLedger(cwd: string): TokenLedger {
|
|
|
131
144
|
export function aggregateTokenLedgerArchiveAt(
|
|
132
145
|
projDir: string
|
|
133
146
|
): LedgerSession[] {
|
|
147
|
+
// Phase 4: archive is `archived = 1` in ledger_sessions.
|
|
148
|
+
const dbPath = join(projDir, "mink.db");
|
|
149
|
+
if (existsSync(dbPath)) {
|
|
150
|
+
try {
|
|
151
|
+
const { openDriver } = require("../storage/driver") as typeof import("../storage/driver");
|
|
152
|
+
const { applySchema } = require("../storage/schema") as typeof import("../storage/schema");
|
|
153
|
+
const { TokenLedgerRepo } = require("../repositories/token-ledger-repo") as typeof import("../repositories/token-ledger-repo");
|
|
154
|
+
const db = openDriver(dbPath);
|
|
155
|
+
try {
|
|
156
|
+
applySchema(db);
|
|
157
|
+
return new TokenLedgerRepo(db).archivedSessions();
|
|
158
|
+
} finally {
|
|
159
|
+
db.close();
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Fall through to JSON aggregation.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
const seen = new Set<string>();
|
|
135
167
|
const archived: LedgerSession[] = [];
|
|
136
168
|
|
|
@@ -161,6 +193,31 @@ export function aggregateTokenLedgerArchive(cwd: string): LedgerSession[] {
|
|
|
161
193
|
// ── Bug memory ─────────────────────────────────────────────────────────────
|
|
162
194
|
|
|
163
195
|
export function aggregateBugMemoryAt(projDir: string): BugMemory {
|
|
196
|
+
// Phase 3 of the SQLite migration: bug_memory lives in mink.db. The
|
|
197
|
+
// legacy JSON aggregation below is preserved as a fallback for tests /
|
|
198
|
+
// pre-migration projects, but new call sites should read from
|
|
199
|
+
// BugMemoryRepo directly.
|
|
200
|
+
const dbPath = join(projDir, "mink.db");
|
|
201
|
+
if (existsSync(dbPath)) {
|
|
202
|
+
try {
|
|
203
|
+
// Use a fresh handle so we don't disturb the per-process cache used
|
|
204
|
+
// by hook commands. Lazy-require to keep state-aggregator free of
|
|
205
|
+
// a hard storage-layer dependency for tests that mock paths.
|
|
206
|
+
const { openDriver } = require("../storage/driver") as typeof import("../storage/driver");
|
|
207
|
+
const { applySchema } = require("../storage/schema") as typeof import("../storage/schema");
|
|
208
|
+
const { BugMemoryRepo } = require("../repositories/bug-memory-repo") as typeof import("../repositories/bug-memory-repo");
|
|
209
|
+
const db = openDriver(dbPath);
|
|
210
|
+
try {
|
|
211
|
+
applySchema(db); // tolerate older DBs missing newer tables
|
|
212
|
+
return new BugMemoryRepo(db).snapshot();
|
|
213
|
+
} finally {
|
|
214
|
+
db.close();
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Fall through to JSON aggregation if the DB read fails.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
164
221
|
const byId = new Map<string, BugEntry>();
|
|
165
222
|
let maxNextId = 1;
|
|
166
223
|
|
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Wrapper over the per-device counters table. The legacy implementation
|
|
2
|
+
// kept these in projects/<id>/.mink-state-counters.json; Phase 1's
|
|
3
|
+
// importer copies that file's contents into the `counters` table the
|
|
4
|
+
// first time the project DB opens, and the file is moved to
|
|
5
|
+
// legacy-backup/. Both APIs (totals and per-device) remain available so
|
|
6
|
+
// the dashboard and `mink status` keep their existing surface.
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
// and is gitignored so each device's counts never collide. Aggregated views
|
|
6
|
-
// (dashboard, status) sum across devices via aggregateStateCounters().
|
|
8
|
+
import { CountersRepo } from "../repositories/counters-repo";
|
|
7
9
|
|
|
8
10
|
export interface StateCounters {
|
|
9
11
|
fileIndexHits: number;
|
|
10
12
|
fileIndexMisses: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
function emptyCounters(): StateCounters {
|
|
14
|
-
return { fileIndexHits: 0, fileIndexMisses: 0 };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isStateCounters(value: unknown): value is StateCounters {
|
|
18
|
-
if (value === null || typeof value !== "object") return false;
|
|
19
|
-
const obj = value as Record<string, unknown>;
|
|
20
|
-
return (
|
|
21
|
-
typeof obj.fileIndexHits === "number" &&
|
|
22
|
-
typeof obj.fileIndexMisses === "number"
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
15
|
export function loadCounters(cwd: string): StateCounters {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
return emptyCounters();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function saveCounters(cwd: string, counters: StateCounters): void {
|
|
33
|
-
atomicWriteJson(fileIndexCountersPath(cwd), counters);
|
|
16
|
+
const t = CountersRepo.for(cwd).totals();
|
|
17
|
+
return { fileIndexHits: t.hits, fileIndexMisses: t.misses };
|
|
34
18
|
}
|
|
35
19
|
|
|
36
20
|
export function incrementFileIndexHit(cwd: string): void {
|
|
37
|
-
|
|
38
|
-
c.fileIndexHits++;
|
|
39
|
-
saveCounters(cwd, c);
|
|
21
|
+
CountersRepo.for(cwd).incrementHit();
|
|
40
22
|
}
|
|
41
23
|
|
|
42
24
|
export function incrementFileIndexMiss(cwd: string): void {
|
|
43
|
-
|
|
44
|
-
c.fileIndexMisses++;
|
|
45
|
-
saveCounters(cwd, c);
|
|
25
|
+
CountersRepo.for(cwd).incrementMiss();
|
|
46
26
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, appendFileSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, copyFileSync, unlinkSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { minkRoot } from "./paths";
|
|
4
4
|
import { parseLearningMemory, serializeLearningMemory } from "./learning-memory";
|
|
5
|
+
import { openDriver } from "../storage/driver";
|
|
6
|
+
import { applySchema } from "../storage/schema";
|
|
5
7
|
import type { LearningMemory, SectionName } from "../types/learning-memory";
|
|
6
8
|
import type { FileIndex, FileIndexEntry } from "../types/file-index";
|
|
7
9
|
import type { DeviceInfo, DeviceRegistry } from "../types/config";
|
|
@@ -220,6 +222,161 @@ export function mergeDevicesDriver(args: DriverArgs): void {
|
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
// ── mink-db-merge: projects/*/mink.db ──────────────────────────────────────
|
|
226
|
+
// Two-DB reconciliation: open `ours.db` for read/write, ATTACH `theirs.db`
|
|
227
|
+
// as a read-only schema, replay rows via INSERT OR ... ON CONFLICT using
|
|
228
|
+
// the same per-store conflict rules the JSON aggregator uses today.
|
|
229
|
+
// Sessions / counters / ledger_lifetime are append-merge keyed by device.
|
|
230
|
+
// Falls back to "keep ours" on any failure so a merge driver never blocks
|
|
231
|
+
// the rebase.
|
|
232
|
+
|
|
233
|
+
interface DbHandle {
|
|
234
|
+
exec(sql: string): void;
|
|
235
|
+
prepare(sql: string): { run(...args: unknown[]): unknown };
|
|
236
|
+
close(): void;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function attachAndReplay(ours: DbHandle, theirsPath: string): void {
|
|
240
|
+
// ATTACH requires the path to be a literal string parameter; bind it
|
|
241
|
+
// with the prepare binding to avoid SQL injection from a weird filename.
|
|
242
|
+
ours.prepare("ATTACH DATABASE ? AS theirs").run(theirsPath);
|
|
243
|
+
try {
|
|
244
|
+
ours.exec(`
|
|
245
|
+
-- file_index: keep the side with the newer last_modified.
|
|
246
|
+
INSERT INTO file_index
|
|
247
|
+
(file_path, description, estimated_tokens, last_modified, last_indexed,
|
|
248
|
+
mtime_ms, content_hash, size_bytes, device_id)
|
|
249
|
+
SELECT file_path, description, estimated_tokens, last_modified, last_indexed,
|
|
250
|
+
mtime_ms, content_hash, size_bytes, device_id
|
|
251
|
+
FROM theirs.file_index
|
|
252
|
+
WHERE TRUE
|
|
253
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
254
|
+
description = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.description ELSE file_index.description END,
|
|
255
|
+
estimated_tokens = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.estimated_tokens ELSE file_index.estimated_tokens END,
|
|
256
|
+
last_indexed = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.last_indexed ELSE file_index.last_indexed END,
|
|
257
|
+
last_modified = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.last_modified ELSE file_index.last_modified END,
|
|
258
|
+
mtime_ms = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.mtime_ms ELSE file_index.mtime_ms END,
|
|
259
|
+
content_hash = COALESCE(excluded.content_hash, file_index.content_hash),
|
|
260
|
+
size_bytes = COALESCE(excluded.size_bytes, file_index.size_bytes),
|
|
261
|
+
device_id = CASE WHEN excluded.last_modified > file_index.last_modified THEN excluded.device_id ELSE file_index.device_id END;
|
|
262
|
+
|
|
263
|
+
-- bug_memory: oldest createdAt + latest lastSeenAt + max occurrence_count.
|
|
264
|
+
INSERT INTO bug_memory
|
|
265
|
+
(id, created_at, last_seen_at, error_message, file_path, line_number,
|
|
266
|
+
root_cause, fix_description, occurrence_count, device_id)
|
|
267
|
+
SELECT id, created_at, last_seen_at, error_message, file_path, line_number,
|
|
268
|
+
root_cause, fix_description, occurrence_count, device_id
|
|
269
|
+
FROM theirs.bug_memory
|
|
270
|
+
WHERE TRUE
|
|
271
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
272
|
+
created_at = CASE WHEN excluded.created_at < bug_memory.created_at THEN excluded.created_at ELSE bug_memory.created_at END,
|
|
273
|
+
last_seen_at = CASE WHEN excluded.last_seen_at > bug_memory.last_seen_at THEN excluded.last_seen_at ELSE bug_memory.last_seen_at END,
|
|
274
|
+
occurrence_count = MAX(bug_memory.occurrence_count, excluded.occurrence_count);
|
|
275
|
+
|
|
276
|
+
INSERT OR IGNORE INTO bug_tags (bug_id, tag) SELECT bug_id, tag FROM theirs.bug_tags;
|
|
277
|
+
INSERT OR IGNORE INTO bug_related (bug_id, related_bug_id) SELECT bug_id, related_bug_id FROM theirs.bug_related;
|
|
278
|
+
|
|
279
|
+
-- Ledger sessions are insert-only and device-isolated, so first writer
|
|
280
|
+
-- wins is correct (shards never overlap session_id in production).
|
|
281
|
+
INSERT OR IGNORE INTO ledger_sessions
|
|
282
|
+
(session_id, device_id, start_timestamp, end_timestamp, read_count,
|
|
283
|
+
write_count, estimated_tokens, repeated_reads, file_index_hits,
|
|
284
|
+
file_index_misses, estimated_savings, archived)
|
|
285
|
+
SELECT session_id, device_id, start_timestamp, end_timestamp, read_count,
|
|
286
|
+
write_count, estimated_tokens, repeated_reads, file_index_hits,
|
|
287
|
+
file_index_misses, estimated_savings, archived
|
|
288
|
+
FROM theirs.ledger_sessions;
|
|
289
|
+
|
|
290
|
+
INSERT INTO ledger_reads (session_id, file_path, estimated_tokens, read_count)
|
|
291
|
+
SELECT session_id, file_path, estimated_tokens, read_count
|
|
292
|
+
FROM theirs.ledger_reads
|
|
293
|
+
WHERE session_id IN (SELECT session_id FROM theirs.ledger_sessions
|
|
294
|
+
WHERE session_id NOT IN (SELECT session_id FROM ledger_reads));
|
|
295
|
+
|
|
296
|
+
INSERT INTO ledger_writes (session_id, file_path, estimated_tokens, action)
|
|
297
|
+
SELECT session_id, file_path, estimated_tokens, action
|
|
298
|
+
FROM theirs.ledger_writes
|
|
299
|
+
WHERE session_id IN (SELECT session_id FROM theirs.ledger_sessions
|
|
300
|
+
WHERE session_id NOT IN (SELECT session_id FROM ledger_writes));
|
|
301
|
+
|
|
302
|
+
-- Per-device lifetime sums and counters: take the MAX so concurrent
|
|
303
|
+
-- increments on different devices don't double-count when the same
|
|
304
|
+
-- device's row is shared (it shouldn't be, but MAX is a safe upper
|
|
305
|
+
-- bound under concurrent shard mutation).
|
|
306
|
+
INSERT INTO ledger_lifetime
|
|
307
|
+
(device_id, total_tokens, total_reads, total_writes, total_sessions,
|
|
308
|
+
total_file_index_hits, total_file_index_misses, total_repeated_reads,
|
|
309
|
+
total_estimated_savings)
|
|
310
|
+
SELECT device_id, total_tokens, total_reads, total_writes, total_sessions,
|
|
311
|
+
total_file_index_hits, total_file_index_misses, total_repeated_reads,
|
|
312
|
+
total_estimated_savings
|
|
313
|
+
FROM theirs.ledger_lifetime
|
|
314
|
+
WHERE TRUE
|
|
315
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
316
|
+
total_tokens = MAX(ledger_lifetime.total_tokens, excluded.total_tokens),
|
|
317
|
+
total_reads = MAX(ledger_lifetime.total_reads, excluded.total_reads),
|
|
318
|
+
total_writes = MAX(ledger_lifetime.total_writes, excluded.total_writes),
|
|
319
|
+
total_sessions = MAX(ledger_lifetime.total_sessions, excluded.total_sessions),
|
|
320
|
+
total_file_index_hits = MAX(ledger_lifetime.total_file_index_hits, excluded.total_file_index_hits),
|
|
321
|
+
total_file_index_misses = MAX(ledger_lifetime.total_file_index_misses, excluded.total_file_index_misses),
|
|
322
|
+
total_repeated_reads = MAX(ledger_lifetime.total_repeated_reads, excluded.total_repeated_reads),
|
|
323
|
+
total_estimated_savings = MAX(ledger_lifetime.total_estimated_savings, excluded.total_estimated_savings);
|
|
324
|
+
|
|
325
|
+
INSERT INTO counters (device_id, file_index_hits, file_index_misses)
|
|
326
|
+
SELECT device_id, file_index_hits, file_index_misses
|
|
327
|
+
FROM theirs.counters
|
|
328
|
+
WHERE TRUE
|
|
329
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
330
|
+
file_index_hits = MAX(counters.file_index_hits, excluded.file_index_hits),
|
|
331
|
+
file_index_misses = MAX(counters.file_index_misses, excluded.file_index_misses);
|
|
332
|
+
|
|
333
|
+
INSERT OR IGNORE INTO waste_flags (pattern, detected_at, details, device_id)
|
|
334
|
+
SELECT pattern, detected_at, details, device_id FROM theirs.waste_flags;
|
|
335
|
+
`);
|
|
336
|
+
} finally {
|
|
337
|
+
ours.exec("DETACH DATABASE theirs");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function mergeDbDriver(args: DriverArgs): void {
|
|
342
|
+
// Run the merge in a side-DB so a crash mid-replay never leaves ours in
|
|
343
|
+
// a half-merged state. We copy ours -> a temp file, replay theirs into
|
|
344
|
+
// the copy, then atomically replace ours via rename. Sidecar WAL/SHM
|
|
345
|
+
// files in the temp location are cleaned up before rename.
|
|
346
|
+
const tmp = `${args.oursPath}.merge-${process.pid}-${Date.now()}.tmp`;
|
|
347
|
+
let ours: ReturnType<typeof openDriver> | null = null;
|
|
348
|
+
try {
|
|
349
|
+
copyFileSync(args.oursPath, tmp);
|
|
350
|
+
ours = openDriver(tmp);
|
|
351
|
+
ours.exec("PRAGMA journal_mode = WAL");
|
|
352
|
+
ours.exec("PRAGMA foreign_keys = ON");
|
|
353
|
+
applySchema(ours); // tolerate `theirs` carrying tables we don't yet have
|
|
354
|
+
|
|
355
|
+
attachAndReplay(ours, args.theirsPath);
|
|
356
|
+
|
|
357
|
+
ours.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
358
|
+
ours.close();
|
|
359
|
+
ours = null;
|
|
360
|
+
|
|
361
|
+
// Clean up WAL/SHM sidecars left by the temp DB so the rename target
|
|
362
|
+
// is a single self-contained file. SQLite truncates the WAL above; we
|
|
363
|
+
// remove the empty sidecar entries explicitly.
|
|
364
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
365
|
+
try { unlinkSync(`${tmp}${suffix}`); } catch { /* not present */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Atomic replace.
|
|
369
|
+
copyFileSync(tmp, args.oursPath);
|
|
370
|
+
try { unlinkSync(tmp); } catch { /* best effort */ }
|
|
371
|
+
} catch (err) {
|
|
372
|
+
logWarning("mink-db-merge", args, err);
|
|
373
|
+
try { if (ours) ours.close(); } catch { /* ignore */ }
|
|
374
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
375
|
+
// Fall back to ours by doing nothing — the original args.oursPath is
|
|
376
|
+
// untouched.
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
223
380
|
// ── Dispatcher ─────────────────────────────────────────────────────────────
|
|
224
381
|
|
|
225
382
|
export function runMergeDriver(
|
|
@@ -232,8 +389,14 @@ export function runMergeDriver(
|
|
|
232
389
|
const args: DriverArgs = { basePath, oursPath, theirsPath, filePath };
|
|
233
390
|
switch (name) {
|
|
234
391
|
case "mink-json-union":
|
|
392
|
+
// Legacy driver — still registered for projects pre-Phase 2 of the
|
|
393
|
+
// SQLite migration where file-index.json may resurface during sync
|
|
394
|
+
// of legacy-backup contents. New projects don't use it.
|
|
235
395
|
mergeJsonUnion(args);
|
|
236
396
|
return 0;
|
|
397
|
+
case "mink-db-merge":
|
|
398
|
+
mergeDbDriver(args);
|
|
399
|
+
return 0;
|
|
237
400
|
case "mink-learning-memory":
|
|
238
401
|
mergeLearningMemoryDriver(args);
|
|
239
402
|
return 0;
|
package/src/core/sync.ts
CHANGED
|
@@ -65,6 +65,12 @@ projects/*/scheduler-manifest.json
|
|
|
65
65
|
projects/*/design-captures/
|
|
66
66
|
projects/*/.mink-state-counters.json
|
|
67
67
|
|
|
68
|
+
# SQLite write-ahead log + shared-memory sidecars — local-only, must not
|
|
69
|
+
# travel with the main mink.db (WAL is checkpointed before push).
|
|
70
|
+
projects/*/mink.db-wal
|
|
71
|
+
projects/*/mink.db-shm
|
|
72
|
+
projects/*/mink.db-journal
|
|
73
|
+
|
|
68
74
|
# Wiki derived/regenerable pages — each device rebuilds locally
|
|
69
75
|
wiki/_index.md
|
|
70
76
|
wiki/.mink-index.json
|
|
@@ -74,6 +80,8 @@ wiki/projects/*/architecture.md
|
|
|
74
80
|
|
|
75
81
|
const GITATTRIBUTES_CONTENTS = `# Sync v2 — merge drivers eliminate conflicts on shared files.
|
|
76
82
|
# Drivers are registered in .git/config by ensureMergeDriversRegistered().
|
|
83
|
+
projects/*/mink.db merge=mink-db-merge
|
|
84
|
+
projects/*/mink.db binary
|
|
77
85
|
projects/*/file-index.json merge=mink-json-union
|
|
78
86
|
projects/*/learning-memory.*.md merge=union
|
|
79
87
|
projects/*/learning-memory.md merge=mink-learning-memory
|
|
@@ -120,6 +128,7 @@ export function ensureGitAttributes(): void {
|
|
|
120
128
|
|
|
121
129
|
const MERGE_DRIVERS = [
|
|
122
130
|
"mink-json-union",
|
|
131
|
+
"mink-db-merge",
|
|
123
132
|
"mink-learning-memory",
|
|
124
133
|
"mink-devices",
|
|
125
134
|
] as const;
|
package/src/core/token-ledger.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
1
2
|
import { join } from "path";
|
|
2
3
|
import type { TokenLedger, LedgerSession, LifetimeCounters } from "../types/token-ledger";
|
|
3
4
|
import type { SessionFinalizer, SessionSummary } from "../types/session";
|
|
@@ -158,15 +159,60 @@ export function saveArchive(archivePath: string, newlyArchived: LedgerSession[])
|
|
|
158
159
|
|
|
159
160
|
// ── Task 6: Ledger Finalizer Factory ─────────────────────────────────────────
|
|
160
161
|
|
|
162
|
+
// Phase 4 of the SQLite migration: every projectDir that has a mink.db
|
|
163
|
+
// uses TokenLedgerRepo. Legacy JSON paths still get a finalizer for
|
|
164
|
+
// pre-migration projects (and for unit tests that don't open a DB).
|
|
161
165
|
export function createLedgerFinalizer(
|
|
162
166
|
projectDir: string,
|
|
163
167
|
deviceIdOrThreshold?: string | number,
|
|
164
168
|
archiveThreshold: number = 1000
|
|
165
169
|
): SessionFinalizer {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const dbPath = join(projectDir, "mink.db");
|
|
171
|
+
|
|
172
|
+
if (existsSync(dbPath)) {
|
|
173
|
+
// Route through the SQLite repo. We open a fresh handle so this
|
|
174
|
+
// module doesn't depend on the per-process cache used by hooks.
|
|
175
|
+
const { openDriver } = require("../storage/driver") as typeof import("../storage/driver");
|
|
176
|
+
const { applySchema } = require("../storage/schema") as typeof import("../storage/schema");
|
|
177
|
+
const { TokenLedgerRepo } = require("../repositories/token-ledger-repo") as typeof import("../repositories/token-ledger-repo");
|
|
178
|
+
|
|
179
|
+
const deviceId = typeof deviceIdOrThreshold === "string" ? deviceIdOrThreshold : undefined;
|
|
180
|
+
const threshold = typeof deviceIdOrThreshold === "number"
|
|
181
|
+
? deviceIdOrThreshold
|
|
182
|
+
: archiveThreshold;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
appendSession(summary: SessionSummary): void {
|
|
186
|
+
const db = openDriver(dbPath);
|
|
187
|
+
try {
|
|
188
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
189
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
190
|
+
applySchema(db);
|
|
191
|
+
const repo = new TokenLedgerRepo(db);
|
|
192
|
+
repo.appendSession(summary, deviceId);
|
|
193
|
+
repo.archive(threshold);
|
|
194
|
+
} finally {
|
|
195
|
+
db.close();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
updateSession(summary: SessionSummary): void {
|
|
200
|
+
const db = openDriver(dbPath);
|
|
201
|
+
try {
|
|
202
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
203
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
204
|
+
applySchema(db);
|
|
205
|
+
new TokenLedgerRepo(db).updateSession(summary, deviceId);
|
|
206
|
+
} finally {
|
|
207
|
+
db.close();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Legacy JSON fallback ────────────────────────────────────────────
|
|
214
|
+
// Tests + pre-migration projects continue to write to disk. The
|
|
215
|
+
// `(projectDir)` and `(projectDir, threshold)` signatures still work.
|
|
170
216
|
let ledgerPath: string;
|
|
171
217
|
let archivePath: string;
|
|
172
218
|
let threshold: number;
|