@drewpayment/mink 0.11.0-beta.4 → 0.12.0-beta.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/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 → cli.node.js} +2227 -758
- package/package.json +14 -4
- package/scripts/build.mjs +47 -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/{fci7mSuW5y3ri6IlmLojm → 9ElzGFcXpcjLq-QSQslWY}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{fci7mSuW5y3ri6IlmLojm → 9ElzGFcXpcjLq-QSQslWY}/_ssgManifest.js +0 -0
package/src/commands/status.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import {
|
|
3
3
|
sessionPath,
|
|
4
|
-
|
|
4
|
+
projectDbPath,
|
|
5
5
|
configPath,
|
|
6
6
|
learningMemoryPath,
|
|
7
|
-
tokenLedgerPath,
|
|
8
|
-
bugMemoryPath,
|
|
9
7
|
actionLogPath,
|
|
10
8
|
} from "../core/paths";
|
|
11
9
|
import { safeReadJson } from "../core/fs-utils";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { parseLearningMemory, totalEntryCount } from "../core/learning-memory";
|
|
15
|
-
import { loadBugMemory } from "../core/bug-memory";
|
|
10
|
+
import { FileIndexRepo } from "../repositories/file-index-repo";
|
|
11
|
+
import { openProjectDb } from "../storage/db";
|
|
16
12
|
import {
|
|
17
13
|
aggregateTokenLedger,
|
|
18
14
|
aggregateBugMemory,
|
|
@@ -20,6 +16,7 @@ import {
|
|
|
20
16
|
} from "../core/state-aggregator";
|
|
21
17
|
import { loadCounters } from "../core/state-counters";
|
|
22
18
|
import { getDaemonStatus } from "../core/daemon";
|
|
19
|
+
import { totalEntryCount } from "../core/learning-memory";
|
|
23
20
|
|
|
24
21
|
interface FileCheck {
|
|
25
22
|
name: string;
|
|
@@ -45,18 +42,42 @@ function checkTextFile(name: string, filePath: string): FileCheck {
|
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
45
|
+
function checkDbFile(name: string, filePath: string): FileCheck {
|
|
46
|
+
if (!existsSync(filePath)) return { name, path: filePath, status: "missing" };
|
|
47
|
+
try {
|
|
48
|
+
const header = readFileSync(filePath).slice(0, 16).toString("utf-8");
|
|
49
|
+
if (!header.startsWith("SQLite format 3")) {
|
|
50
|
+
return { name, path: filePath, status: "corrupt" };
|
|
51
|
+
}
|
|
52
|
+
return { name, path: filePath, status: "ok" };
|
|
53
|
+
} catch {
|
|
54
|
+
return { name, path: filePath, status: "corrupt" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export function status(cwd: string): void {
|
|
49
59
|
console.log("[mink] project status");
|
|
50
60
|
console.log();
|
|
51
61
|
|
|
52
|
-
//
|
|
62
|
+
// Open the project DB up-front so the lazy JSON → SQLite migration
|
|
63
|
+
// runs before checkDbFile inspects mink.db. Otherwise the integrity
|
|
64
|
+
// section would always report "missing" on a project that has only
|
|
65
|
+
// legacy JSON state.
|
|
66
|
+
try {
|
|
67
|
+
openProjectDb(cwd);
|
|
68
|
+
} catch {
|
|
69
|
+
// Migration failures are non-fatal — report the DB as corrupt below.
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Section 1: State directory integrity. file-index, bug-memory, and
|
|
73
|
+
// token-ledger all live inside mink.db. The remaining JSON/MD files
|
|
74
|
+
// are intentionally separate — session is per-process, config is
|
|
75
|
+
// user-editable, learning-memory and action-log are human-readable.
|
|
53
76
|
const checks: FileCheck[] = [
|
|
54
77
|
checkJsonFile("session.json", sessionPath(cwd)),
|
|
55
|
-
|
|
78
|
+
checkDbFile("mink.db", projectDbPath(cwd)),
|
|
56
79
|
checkJsonFile("config.json", configPath(cwd)),
|
|
57
80
|
checkTextFile("learning-memory.md", learningMemoryPath(cwd)),
|
|
58
|
-
checkJsonFile("token-ledger.json", tokenLedgerPath(cwd)),
|
|
59
|
-
checkJsonFile("bug-memory.json", bugMemoryPath(cwd)),
|
|
60
81
|
checkTextFile("action-log.md", actionLogPath(cwd)),
|
|
61
82
|
];
|
|
62
83
|
|
|
@@ -73,24 +94,22 @@ export function status(cwd: string): void {
|
|
|
73
94
|
}
|
|
74
95
|
console.log();
|
|
75
96
|
|
|
76
|
-
// Section 2: File index
|
|
97
|
+
// Section 2: File index — sourced from mink.db.
|
|
77
98
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
const repo = FileIndexRepo.for(cwd);
|
|
100
|
+
const total = repo.totalFiles();
|
|
101
|
+
if (total === 0) {
|
|
102
|
+
console.log(" File index: not available");
|
|
103
|
+
} else {
|
|
83
104
|
const counters = loadCounters(cwd);
|
|
84
|
-
const hits = counters.fileIndexHits
|
|
85
|
-
const misses = counters.fileIndexMisses
|
|
86
|
-
const
|
|
87
|
-
const ratio =
|
|
105
|
+
const hits = counters.fileIndexHits;
|
|
106
|
+
const misses = counters.fileIndexMisses;
|
|
107
|
+
const totalLookups = hits + misses;
|
|
108
|
+
const ratio = totalLookups > 0 ? ((hits / totalLookups) * 100).toFixed(1) : "N/A";
|
|
88
109
|
console.log(" File index:");
|
|
89
|
-
console.log(` Files: ${
|
|
90
|
-
console.log(` Last scan: ${
|
|
91
|
-
console.log(` Hit/miss ratio: ${ratio}${
|
|
92
|
-
} else {
|
|
93
|
-
console.log(" File index: not available");
|
|
110
|
+
console.log(` Files: ${total}`);
|
|
111
|
+
console.log(` Last scan: ${repo.getLastScanTimestamp() || "never"}`);
|
|
112
|
+
console.log(` Hit/miss ratio: ${ratio}${totalLookups > 0 ? "%" : ""} (${hits} hits, ${misses} misses)`);
|
|
94
113
|
}
|
|
95
114
|
} catch {
|
|
96
115
|
console.log(" File index: error reading");
|
package/src/core/bug-memory.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Wrapper over the SQLite bug-memory storage layer. The function-based
|
|
2
|
+
// API below stays compatible with existing unit tests (which build
|
|
3
|
+
// in-memory BugMemory objects), while the repo-aware paths route through
|
|
4
|
+
// BugMemoryRepo so 20k+ bug histories stay searchable in milliseconds.
|
|
5
|
+
//
|
|
6
|
+
// FTS5 is the search backbone — see BugMemoryRepo.searchBugs for the
|
|
7
|
+
// query semantics, score normalization, and false-positive guards.
|
|
8
|
+
|
|
2
9
|
import type { BugEntry, BugMemory, SimilarityMatch } from "../types/bug-memory";
|
|
3
10
|
|
|
4
11
|
export function createEmptyBugMemory(): BugMemory {
|
|
5
12
|
return { entries: [], nextId: 1 };
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
export function loadBugMemory(path: string): BugMemory {
|
|
9
|
-
const raw = safeReadJson(path);
|
|
10
|
-
if (raw !== null && isBugMemory(raw)) return raw;
|
|
11
|
-
return createEmptyBugMemory();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function saveBugMemory(path: string, memory: BugMemory): void {
|
|
15
|
-
atomicWriteJson(path, memory);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
15
|
export function isBugMemory(value: unknown): value is BugMemory {
|
|
19
16
|
if (value === null || typeof value !== "object") return false;
|
|
20
17
|
const obj = value as Record<string, unknown>;
|
|
@@ -25,6 +22,8 @@ export function generateBugId(nextId: number): string {
|
|
|
25
22
|
return `bug-${String(nextId).padStart(3, "0")}`;
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
// ── In-memory operations (used by unit tests and the JSON migration importer) ──
|
|
26
|
+
|
|
28
27
|
export function findDuplicate(
|
|
29
28
|
memory: BugMemory,
|
|
30
29
|
errorMessage: string,
|
|
@@ -42,9 +41,7 @@ export function addBugEntry(
|
|
|
42
41
|
fields: Omit<BugEntry, "id" | "createdAt" | "lastSeenAt" | "occurrenceCount">
|
|
43
42
|
): BugMemory {
|
|
44
43
|
const existing = findDuplicate(memory, fields.errorMessage, fields.filePath);
|
|
45
|
-
if (existing)
|
|
46
|
-
return updateOccurrence(memory, existing.id);
|
|
47
|
-
}
|
|
44
|
+
if (existing) return updateOccurrence(memory, existing.id);
|
|
48
45
|
|
|
49
46
|
const now = new Date().toISOString();
|
|
50
47
|
const entry: BugEntry = {
|
|
@@ -73,14 +70,11 @@ export function updateOccurrence(memory: BugMemory, id: string): BugMemory {
|
|
|
73
70
|
};
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
//
|
|
73
|
+
// ── Similarity scoring (in-memory) ────────────────────────────────────────
|
|
77
74
|
|
|
78
75
|
function tokenize(text: string): Set<string> {
|
|
79
76
|
return new Set(
|
|
80
|
-
text
|
|
81
|
-
.toLowerCase()
|
|
82
|
-
.split(/\W+/)
|
|
83
|
-
.filter((w) => w.length > 0)
|
|
77
|
+
text.toLowerCase().split(/\W+/).filter((w) => w.length > 0)
|
|
84
78
|
);
|
|
85
79
|
}
|
|
86
80
|
|
|
@@ -101,18 +95,12 @@ export function computeSimilarity(
|
|
|
101
95
|
const matchReasons: string[] = [];
|
|
102
96
|
let score = 0;
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
entry.errorMessage.length > 0 &&
|
|
107
|
-
entry.errorMessage.includes(query)
|
|
108
|
-
) {
|
|
98
|
+
if (entry.errorMessage.length > 0 && entry.errorMessage.includes(query)) {
|
|
109
99
|
score += 1.0;
|
|
110
100
|
matchReasons.push("exact_error_match");
|
|
111
101
|
}
|
|
112
102
|
|
|
113
|
-
// 2. Word overlap (Jaccard) across searchable fields
|
|
114
103
|
const queryTokens = tokenize(query);
|
|
115
|
-
|
|
116
104
|
const fields: [string, string][] = [
|
|
117
105
|
["error_message", entry.errorMessage],
|
|
118
106
|
["root_cause", entry.rootCause],
|
|
@@ -142,6 +130,8 @@ function hasTagOverlap(entry: BugEntry, query: string): boolean {
|
|
|
142
130
|
return entry.tags.some((tag) => queryTokens.has(tag.toLowerCase()));
|
|
143
131
|
}
|
|
144
132
|
|
|
133
|
+
// Pure-memory search — kept for unit tests that don't open a DB. Production
|
|
134
|
+
// call sites use BugMemoryRepo.searchBugs which goes through FTS5.
|
|
145
135
|
export function searchBugs(
|
|
146
136
|
memory: BugMemory,
|
|
147
137
|
query: string,
|
|
@@ -153,23 +143,16 @@ export function searchBugs(
|
|
|
153
143
|
|
|
154
144
|
for (const entry of memory.entries) {
|
|
155
145
|
const match = computeSimilarity(query, entry);
|
|
156
|
-
|
|
157
|
-
// False positive guard: require file-path match OR tag overlap
|
|
158
146
|
const fileMatch = hasFilePathMatch(entry, options?.filePath);
|
|
159
147
|
const tagMatch = hasTagOverlap(entry, query);
|
|
160
148
|
if (!fileMatch && !tagMatch && match.score <= 0.3) continue;
|
|
161
|
-
|
|
162
|
-
// Boost same-file matches
|
|
163
149
|
if (fileMatch) {
|
|
164
150
|
match.score += 0.2;
|
|
165
151
|
if (!match.matchReasons.includes("file_path")) {
|
|
166
152
|
match.matchReasons.push("file_path");
|
|
167
153
|
}
|
|
168
154
|
}
|
|
169
|
-
|
|
170
|
-
if (match.score > 0.3) {
|
|
171
|
-
results.push(match);
|
|
172
|
-
}
|
|
155
|
+
if (match.score > 0.3) results.push(match);
|
|
173
156
|
}
|
|
174
157
|
|
|
175
158
|
return results.sort((a, b) => b.score - a.score);
|
|
@@ -221,3 +204,18 @@ export function hasBugForFileInSession(
|
|
|
221
204
|
new Date(e.createdAt).getTime() >= sessionStart
|
|
222
205
|
);
|
|
223
206
|
}
|
|
207
|
+
|
|
208
|
+
// ── JSON shim — kept so the migration importer + state-aggregator can still
|
|
209
|
+
// read legacy files during the rollout window. New writes go through the repo.
|
|
210
|
+
|
|
211
|
+
import { safeReadJson, atomicWriteJson } from "./fs-utils";
|
|
212
|
+
|
|
213
|
+
export function loadBugMemory(path: string): BugMemory {
|
|
214
|
+
const raw = safeReadJson(path);
|
|
215
|
+
if (raw !== null && isBugMemory(raw)) return raw;
|
|
216
|
+
return createEmptyBugMemory();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function saveBugMemory(path: string, memory: BugMemory): void {
|
|
220
|
+
atomicWriteJson(path, memory);
|
|
221
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import {
|
|
3
3
|
projectDir,
|
|
4
|
-
|
|
5
|
-
tokenLedgerPath,
|
|
6
|
-
bugMemoryPath,
|
|
4
|
+
projectDbPath,
|
|
7
5
|
actionLogPath,
|
|
8
6
|
learningMemoryPath,
|
|
9
7
|
projectMetaPath,
|
|
@@ -13,7 +11,8 @@ import {
|
|
|
13
11
|
designReportPath,
|
|
14
12
|
} from "./paths";
|
|
15
13
|
import { safeReadJson } from "./fs-utils";
|
|
16
|
-
import {
|
|
14
|
+
import { FileIndexRepo } from "../repositories/file-index-repo";
|
|
15
|
+
import { CountersRepo } from "../repositories/counters-repo";
|
|
17
16
|
import { loadLedger } from "./token-ledger";
|
|
18
17
|
import { parseLearningMemory } from "./learning-memory";
|
|
19
18
|
import { loadBugMemory } from "./bug-memory";
|
|
@@ -125,6 +124,18 @@ function checkTextFile(name: string, filePath: string): FileStatus {
|
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
|
|
127
|
+
function checkDbFile(name: string, filePath: string): FileStatus {
|
|
128
|
+
if (!existsSync(filePath)) return { name, status: "missing" };
|
|
129
|
+
try {
|
|
130
|
+
const header = readFileSync(filePath).slice(0, 16).toString("utf-8");
|
|
131
|
+
return header.startsWith("SQLite format 3")
|
|
132
|
+
? { name, status: "ok" }
|
|
133
|
+
: { name, status: "corrupt" };
|
|
134
|
+
} catch {
|
|
135
|
+
return { name, status: "corrupt" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
// ── Panel Loaders ──────────────────────────────────────────────────────────
|
|
129
140
|
|
|
130
141
|
export function loadOverview(cwd: string): OverviewPayload {
|
|
@@ -163,14 +174,13 @@ export function loadOverview(cwd: string): OverviewPayload {
|
|
|
163
174
|
estimatedSavings: ledger.lifetime.totalEstimatedSavings,
|
|
164
175
|
};
|
|
165
176
|
|
|
166
|
-
// State file health
|
|
177
|
+
// State file health. mink.db replaced file-index.json in Phase 2; the
|
|
178
|
+
// other JSON checks remain until Phases 3 (bug-memory) and 4 (ledger).
|
|
167
179
|
const stateFiles: FileStatus[] = [
|
|
168
180
|
checkJsonFile("session.json", sessionPath(cwd)),
|
|
169
|
-
|
|
181
|
+
checkDbFile("mink.db", projectDbPath(cwd)),
|
|
170
182
|
checkJsonFile("config.json", configPath(cwd)),
|
|
171
183
|
checkTextFile("learning-memory.md", learningMemoryPath(cwd)),
|
|
172
|
-
checkJsonFile("token-ledger.json", tokenLedgerPath(cwd)),
|
|
173
|
-
checkJsonFile("bug-memory.json", bugMemoryPath(cwd)),
|
|
174
184
|
checkTextFile("action-log.md", actionLogPath(cwd)),
|
|
175
185
|
checkJsonFile("scheduler-manifest.json", schedulerManifestPath(cwd)),
|
|
176
186
|
];
|
|
@@ -188,21 +198,33 @@ export function loadTokenLedgerPanel(cwd: string): TokenLedgerPayload {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
export function loadFileIndexPanel(cwd: string): FileIndexPayload {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
// file_index + per-device counters now live in mink.db; we synthesize
|
|
202
|
+
// the FileIndexPayload shape the dashboard expects so the frontend
|
|
203
|
+
// doesn't need to change in this phase.
|
|
204
|
+
try {
|
|
205
|
+
const repo = FileIndexRepo.for(cwd);
|
|
206
|
+
const entries: FileIndexEntry[] = repo.listAll();
|
|
207
|
+
const totals = CountersRepo.for(cwd).totals();
|
|
208
|
+
return {
|
|
209
|
+
header: {
|
|
210
|
+
lastScanTimestamp: repo.getLastScanTimestamp(),
|
|
211
|
+
totalFiles: repo.totalFiles(),
|
|
212
|
+
lifetimeHits: totals.hits,
|
|
213
|
+
lifetimeMisses: totals.misses,
|
|
214
|
+
},
|
|
215
|
+
entries,
|
|
216
|
+
};
|
|
217
|
+
} catch {
|
|
218
|
+
return {
|
|
219
|
+
header: {
|
|
220
|
+
lastScanTimestamp: "",
|
|
221
|
+
totalFiles: 0,
|
|
222
|
+
lifetimeHits: 0,
|
|
223
|
+
lifetimeMisses: 0,
|
|
224
|
+
},
|
|
225
|
+
entries: [],
|
|
226
|
+
};
|
|
196
227
|
}
|
|
197
|
-
return {
|
|
198
|
-
header: {
|
|
199
|
-
lastScanTimestamp: "",
|
|
200
|
-
totalFiles: 0,
|
|
201
|
-
lifetimeHits: 0,
|
|
202
|
-
lifetimeMisses: 0,
|
|
203
|
-
},
|
|
204
|
-
entries: [],
|
|
205
|
-
};
|
|
206
228
|
}
|
|
207
229
|
|
|
208
230
|
export function loadSchedulerPanel(cwd: string): SchedulerPayload {
|
package/src/core/index-store.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
// Wrapper over the file-index storage layer. Hooks and `mink scan` write
|
|
2
|
+
// through to the SQLite repository at `src/repositories/file-index-repo.ts`;
|
|
3
|
+
// the in-memory `FileIndex` helpers below stay around so unit tests can
|
|
4
|
+
// build fixtures without spinning up a DB. The on-disk file-index.json is
|
|
5
|
+
// no longer the source of truth — Phase 1's migrator moves any existing
|
|
6
|
+
// JSON into the DB on first open.
|
|
7
|
+
//
|
|
8
|
+
// Callers that need lookup-only access should prefer the IndexLookup
|
|
9
|
+
// interface (see src/types/file-index.ts) — it's the minimal surface the
|
|
10
|
+
// hook hot path depends on and is satisfied by both FileIndexRepo and the
|
|
11
|
+
// in-memory adapter below.
|
|
12
|
+
|
|
1
13
|
import type {
|
|
2
14
|
FileIndex,
|
|
3
15
|
FileIndexEntry,
|
|
16
|
+
IndexLookup,
|
|
4
17
|
StalenessReport,
|
|
5
18
|
} from "../types/file-index";
|
|
6
19
|
|
|
@@ -70,3 +83,13 @@ export function checkStaleness(
|
|
|
70
83
|
isStale: missingFromIndex.length > 0 || orphanedEntries.length > 0,
|
|
71
84
|
};
|
|
72
85
|
}
|
|
86
|
+
|
|
87
|
+
// Adapter: lets analyzers that have an in-memory FileIndex (typically
|
|
88
|
+
// unit tests) pass it to functions expecting the IndexLookup contract
|
|
89
|
+
// without duplicating the lookup logic.
|
|
90
|
+
export function indexAsLookup(index: FileIndex | null): IndexLookup | null {
|
|
91
|
+
if (index === null) return null;
|
|
92
|
+
return {
|
|
93
|
+
lookupEntry: (filePath: string) => lookupEntry(index, filePath),
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/core/paths.ts
CHANGED
|
@@ -68,6 +68,13 @@ export function bugMemoryPath(cwd: string): string {
|
|
|
68
68
|
return join(projectDir(cwd), "bug-memory.json");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// SQLite-backed state introduced in spec 17 (file index + bug memory +
|
|
72
|
+
// token ledger). The legacy JSON path helpers above remain valid for
|
|
73
|
+
// backup / rollback during the migration window.
|
|
74
|
+
export function projectDbPath(cwd: string): string {
|
|
75
|
+
return join(projectDir(cwd), "mink.db");
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
export function actionLogPath(cwd: string): string {
|
|
72
79
|
return join(projectDir(cwd), "action-log.md");
|
|
73
80
|
}
|
package/src/core/scanner.ts
CHANGED
|
@@ -21,7 +21,10 @@ export const DEFAULT_EXCLUDES: string[] = [
|
|
|
21
21
|
".mink",
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Phase 5 of the SQLite migration drops the legacy default cap; callers
|
|
25
|
+
// that want one set `maxFiles` explicitly. SQLite's per-row write cost is
|
|
26
|
+
// flat in index size, so capping by default no longer pays off.
|
|
27
|
+
const NO_CAP = Number.POSITIVE_INFINITY;
|
|
25
28
|
|
|
26
29
|
function matchesPattern(name: string, pattern: string): boolean {
|
|
27
30
|
if (pattern.includes("*")) {
|
|
@@ -96,20 +99,21 @@ export interface ScanStats {
|
|
|
96
99
|
export function scanProjectWithStats(
|
|
97
100
|
projectRoot: string,
|
|
98
101
|
excludes: string[],
|
|
99
|
-
maxFiles: number =
|
|
102
|
+
maxFiles: number | undefined = NO_CAP
|
|
100
103
|
): ScanStats {
|
|
101
104
|
const results: ScannedFile[] = [];
|
|
102
105
|
walkDirectory(projectRoot, projectRoot, excludes, results);
|
|
103
106
|
results.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
104
107
|
const totalScanned = results.length;
|
|
105
|
-
const
|
|
108
|
+
const cap = maxFiles ?? NO_CAP;
|
|
109
|
+
const files = Number.isFinite(cap) ? results.slice(0, cap) : results;
|
|
106
110
|
return { files, totalScanned, truncated: totalScanned - files.length };
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
export function scanProject(
|
|
110
114
|
projectRoot: string,
|
|
111
115
|
excludes: string[],
|
|
112
|
-
maxFiles: number =
|
|
116
|
+
maxFiles: number | undefined = NO_CAP
|
|
113
117
|
): ScannedFile[] {
|
|
114
118
|
return scanProjectWithStats(projectRoot, excludes, maxFiles).files;
|
|
115
119
|
}
|
|
@@ -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
|
}
|