@drewpayment/mink 0.7.0 → 0.9.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/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.js +2113 -1050
- package/package.json +1 -1
- package/src/commands/bug-search.ts +3 -3
- package/src/commands/detect-waste.ts +34 -25
- package/src/commands/init.ts +27 -0
- package/src/commands/post-read.ts +6 -3
- package/src/commands/post-write.ts +6 -3
- package/src/commands/pre-read.ts +14 -10
- package/src/commands/pre-write.ts +8 -5
- package/src/commands/reflect.ts +12 -7
- package/src/commands/session-start.ts +34 -3
- package/src/commands/session-stop.ts +10 -6
- package/src/commands/status.ts +29 -17
- package/src/commands/sync-migrate.ts +330 -0
- package/src/commands/sync.ts +75 -1
- package/src/core/conflict-park.ts +84 -0
- package/src/core/dashboard-api.ts +12 -31
- package/src/core/note-writer.ts +52 -6
- package/src/core/paths.ts +66 -10
- package/src/core/state-aggregator.ts +304 -0
- package/src/core/state-counters.ts +46 -0
- package/src/core/sync-merge-drivers.ts +247 -0
- package/src/core/sync.ts +150 -68
- package/src/core/token-ledger.ts +19 -3
- /package/dashboard/out/_next/static/{IWTIkvB7I3-GawTXJW4-9 → FLxzihv7lbkF71kIxdNQT}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{IWTIkvB7I3-GawTXJW4-9 → FLxzihv7lbkF71kIxdNQT}/_ssgManifest.js +0 -0
package/src/core/paths.ts
CHANGED
|
@@ -2,15 +2,26 @@ import { join } from "path";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { generateProjectId } from "./project-id";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Resolved per-call so tests can override via MINK_ROOT_OVERRIDE without
|
|
6
|
+
// reloading modules. Production callers get the default homedir/.mink path.
|
|
7
|
+
function resolveMinkRoot(): string {
|
|
8
|
+
return process.env.MINK_ROOT_OVERRIDE || join(homedir(), ".mink");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MINK_ROOT = resolveMinkRoot();
|
|
6
12
|
|
|
7
13
|
export function minkRoot(): string {
|
|
14
|
+
// Re-resolve when the test override is in play so individual tests can
|
|
15
|
+
// point at their own temp dir without contaminating each other.
|
|
16
|
+
if (process.env.MINK_ROOT_OVERRIDE) {
|
|
17
|
+
return process.env.MINK_ROOT_OVERRIDE;
|
|
18
|
+
}
|
|
8
19
|
return MINK_ROOT;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
export function projectDir(cwd: string): string {
|
|
12
23
|
const id = generateProjectId(cwd);
|
|
13
|
-
return join(
|
|
24
|
+
return join(minkRoot(), "projects", id);
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
export function sessionPath(cwd: string): string {
|
|
@@ -46,11 +57,11 @@ export function actionLogPath(cwd: string): string {
|
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export function schedulerPidPath(): string {
|
|
49
|
-
return join(
|
|
60
|
+
return join(minkRoot(), "scheduler.pid");
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
export function schedulerLogPath(): string {
|
|
53
|
-
return join(
|
|
64
|
+
return join(minkRoot(), "scheduler.log");
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export function schedulerManifestPath(cwd: string): string {
|
|
@@ -58,27 +69,27 @@ export function schedulerManifestPath(cwd: string): string {
|
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export function channelPidPath(): string {
|
|
61
|
-
return join(
|
|
72
|
+
return join(minkRoot(), "channel.pid");
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
export function channelLogPath(): string {
|
|
65
|
-
return join(
|
|
76
|
+
return join(minkRoot(), "channel.log");
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
export function globalConfigPath(): string {
|
|
69
|
-
return join(
|
|
80
|
+
return join(minkRoot(), "config");
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
export function localConfigPath(): string {
|
|
73
|
-
return join(
|
|
84
|
+
return join(minkRoot(), "config.local");
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
export function deviceIdPath(): string {
|
|
77
|
-
return join(
|
|
88
|
+
return join(minkRoot(), "device-id");
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export function deviceRegistryPath(): string {
|
|
81
|
-
return join(
|
|
92
|
+
return join(minkRoot(), "devices.json");
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
export function projectMetaPath(cwd: string): string {
|
|
@@ -89,6 +100,51 @@ export function backupDirPath(cwd: string): string {
|
|
|
89
100
|
return join(projectDir(cwd), "backups");
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
// ── Sync v2 — shard-aware paths ────────────────────────────────────────────
|
|
104
|
+
// Per-device shards isolate machine-rewritten state files so two devices never
|
|
105
|
+
// write to the same path. Aggregators in state-aggregator.ts compose the
|
|
106
|
+
// authoritative view by reading every device's shard plus the legacy paths
|
|
107
|
+
// above. The legacy helpers (tokenLedgerPath, bugMemoryPath, actionLogPath,
|
|
108
|
+
// tokenLedgerArchivePath) remain valid for fallback reads during the migration
|
|
109
|
+
// window — they are NOT removed.
|
|
110
|
+
|
|
111
|
+
export function syncVersionPath(): string {
|
|
112
|
+
return join(minkRoot(), ".mink-sync-version");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function projectStateDir(cwd: string): string {
|
|
116
|
+
return join(projectDir(cwd), "state");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function deviceShardDir(cwd: string, deviceId: string): string {
|
|
120
|
+
return join(projectStateDir(cwd), deviceId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function tokenLedgerShardPath(cwd: string, deviceId: string): string {
|
|
124
|
+
return join(deviceShardDir(cwd, deviceId), "token-ledger.json");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function tokenLedgerArchiveShardPath(cwd: string, deviceId: string): string {
|
|
128
|
+
return join(deviceShardDir(cwd, deviceId), "token-ledger-archive.json");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function bugMemoryShardPath(cwd: string, deviceId: string): string {
|
|
132
|
+
return join(deviceShardDir(cwd, deviceId), "bug-memory.json");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function actionLogShardPath(cwd: string, deviceId: string): string {
|
|
136
|
+
return join(deviceShardDir(cwd, deviceId), "action-log.md");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function learningMemorySidecarPath(cwd: string, deviceId: string): string {
|
|
140
|
+
return join(projectDir(cwd), `learning-memory.${deviceId}.md`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Per-device telemetry counters split out of file-index.json (gitignored, not synced).
|
|
144
|
+
export function fileIndexCountersPath(cwd: string): string {
|
|
145
|
+
return join(projectDir(cwd), ".mink-state-counters.json");
|
|
146
|
+
}
|
|
147
|
+
|
|
92
148
|
export function designCapturesDir(cwd: string): string {
|
|
93
149
|
return join(projectDir(cwd), "design-captures");
|
|
94
150
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { projectDir } from "./paths";
|
|
4
|
+
import {
|
|
5
|
+
loadLedger,
|
|
6
|
+
loadArchive,
|
|
7
|
+
createEmptyLedger,
|
|
8
|
+
} from "./token-ledger";
|
|
9
|
+
import { loadBugMemory, createEmptyBugMemory } from "./bug-memory";
|
|
10
|
+
import {
|
|
11
|
+
parseLearningMemory,
|
|
12
|
+
createEmptyLearningMemory,
|
|
13
|
+
} from "./learning-memory";
|
|
14
|
+
import { parseLogSessions, safeReadLog } from "./action-log";
|
|
15
|
+
import type {
|
|
16
|
+
TokenLedger,
|
|
17
|
+
LedgerSession,
|
|
18
|
+
LifetimeCounters,
|
|
19
|
+
} from "../types/token-ledger";
|
|
20
|
+
import type { BugMemory, BugEntry } from "../types/bug-memory";
|
|
21
|
+
import type {
|
|
22
|
+
LearningMemory,
|
|
23
|
+
SectionName,
|
|
24
|
+
} from "../types/learning-memory";
|
|
25
|
+
|
|
26
|
+
// ── Shard discovery ────────────────────────────────────────────────────────
|
|
27
|
+
// All aggregators take a project state directory (the path returned by
|
|
28
|
+
// projectDir(cwd)). The cwd-based variants below are thin wrappers for the
|
|
29
|
+
// common case where the caller has cwd in hand.
|
|
30
|
+
|
|
31
|
+
function listDeviceShardsAt(projDir: string): string[] {
|
|
32
|
+
const stateDir = join(projDir, "state");
|
|
33
|
+
if (!existsSync(stateDir)) return [];
|
|
34
|
+
try {
|
|
35
|
+
return readdirSync(stateDir).filter((name) => {
|
|
36
|
+
try {
|
|
37
|
+
return statSync(join(stateDir, name)).isDirectory();
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SIDECAR_RE = /^learning-memory\.([^.]+)\.md$/;
|
|
48
|
+
|
|
49
|
+
function listLearningMemorySidecarPathsAt(projDir: string): string[] {
|
|
50
|
+
if (!existsSync(projDir)) return [];
|
|
51
|
+
try {
|
|
52
|
+
return readdirSync(projDir)
|
|
53
|
+
.filter((f) => SIDECAR_RE.test(f))
|
|
54
|
+
.map((f) => join(projDir, f));
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shardPath(projDir: string, deviceId: string, file: string): string {
|
|
61
|
+
return join(projDir, "state", deviceId, file);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Token ledger ───────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function addLifetime(target: LifetimeCounters, source: LifetimeCounters): void {
|
|
67
|
+
target.totalTokens += source.totalTokens;
|
|
68
|
+
target.totalReads += source.totalReads;
|
|
69
|
+
target.totalWrites += source.totalWrites;
|
|
70
|
+
target.totalSessions += source.totalSessions;
|
|
71
|
+
target.totalFileIndexHits += source.totalFileIndexHits;
|
|
72
|
+
target.totalFileIndexMisses += source.totalFileIndexMisses;
|
|
73
|
+
target.totalRepeatedReads += source.totalRepeatedReads;
|
|
74
|
+
target.totalEstimatedSavings += source.totalEstimatedSavings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function aggregateTokenLedgerAt(projDir: string): TokenLedger {
|
|
78
|
+
const merged = createEmptyLedger();
|
|
79
|
+
const seenSessions = new Set<string>();
|
|
80
|
+
|
|
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
|
+
const sources = [
|
|
87
|
+
...listDeviceShardsAt(projDir).map((id) =>
|
|
88
|
+
shardPath(projDir, id, "token-ledger.json")
|
|
89
|
+
),
|
|
90
|
+
join(projDir, "token-ledger.json"),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Track waste-flags across sources, deduped by (pattern, detectedAt) so
|
|
94
|
+
// each device's flags remain visible without spamming duplicates.
|
|
95
|
+
const seenFlagKeys = new Set<string>();
|
|
96
|
+
const wasteFlags: NonNullable<TokenLedger["wasteFlags"]> = [];
|
|
97
|
+
|
|
98
|
+
for (const path of sources) {
|
|
99
|
+
if (!existsSync(path)) continue;
|
|
100
|
+
const ledger = loadLedger(path);
|
|
101
|
+
addLifetime(merged.lifetime, ledger.lifetime);
|
|
102
|
+
for (const session of ledger.sessions) {
|
|
103
|
+
if (seenSessions.has(session.sessionId)) continue;
|
|
104
|
+
seenSessions.add(session.sessionId);
|
|
105
|
+
merged.sessions.push(session);
|
|
106
|
+
}
|
|
107
|
+
if (ledger.wasteFlags) {
|
|
108
|
+
for (const flag of ledger.wasteFlags) {
|
|
109
|
+
const key = `${flag.pattern}|${flag.detectedAt}`;
|
|
110
|
+
if (seenFlagKeys.has(key)) continue;
|
|
111
|
+
seenFlagKeys.add(key);
|
|
112
|
+
wasteFlags.push(flag);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (wasteFlags.length > 0) {
|
|
118
|
+
merged.wasteFlags = wasteFlags;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
merged.sessions.sort((a, b) =>
|
|
122
|
+
a.startTimestamp.localeCompare(b.startTimestamp)
|
|
123
|
+
);
|
|
124
|
+
return merged;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function aggregateTokenLedger(cwd: string): TokenLedger {
|
|
128
|
+
return aggregateTokenLedgerAt(projectDir(cwd));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function aggregateTokenLedgerArchiveAt(
|
|
132
|
+
projDir: string
|
|
133
|
+
): LedgerSession[] {
|
|
134
|
+
const seen = new Set<string>();
|
|
135
|
+
const archived: LedgerSession[] = [];
|
|
136
|
+
|
|
137
|
+
const sources = [
|
|
138
|
+
...listDeviceShardsAt(projDir).map((id) =>
|
|
139
|
+
shardPath(projDir, id, "token-ledger-archive.json")
|
|
140
|
+
),
|
|
141
|
+
join(projDir, "token-ledger-archive.json"),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const path of sources) {
|
|
145
|
+
if (!existsSync(path)) continue;
|
|
146
|
+
for (const session of loadArchive(path)) {
|
|
147
|
+
if (seen.has(session.sessionId)) continue;
|
|
148
|
+
seen.add(session.sessionId);
|
|
149
|
+
archived.push(session);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
archived.sort((a, b) => a.startTimestamp.localeCompare(b.startTimestamp));
|
|
154
|
+
return archived;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function aggregateTokenLedgerArchive(cwd: string): LedgerSession[] {
|
|
158
|
+
return aggregateTokenLedgerArchiveAt(projectDir(cwd));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Bug memory ─────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export function aggregateBugMemoryAt(projDir: string): BugMemory {
|
|
164
|
+
const byId = new Map<string, BugEntry>();
|
|
165
|
+
let maxNextId = 1;
|
|
166
|
+
|
|
167
|
+
const sources = [
|
|
168
|
+
...listDeviceShardsAt(projDir).map((id) =>
|
|
169
|
+
shardPath(projDir, id, "bug-memory.json")
|
|
170
|
+
),
|
|
171
|
+
join(projDir, "bug-memory.json"),
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
for (const path of sources) {
|
|
175
|
+
if (!existsSync(path)) continue;
|
|
176
|
+
const mem = loadBugMemory(path);
|
|
177
|
+
if (mem.nextId > maxNextId) maxNextId = mem.nextId;
|
|
178
|
+
for (const entry of mem.entries) {
|
|
179
|
+
const existing = byId.get(entry.id);
|
|
180
|
+
if (!existing) {
|
|
181
|
+
byId.set(entry.id, { ...entry });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
existing.occurrenceCount = Math.max(
|
|
185
|
+
existing.occurrenceCount,
|
|
186
|
+
entry.occurrenceCount
|
|
187
|
+
);
|
|
188
|
+
if (entry.lastSeenAt > existing.lastSeenAt) {
|
|
189
|
+
existing.lastSeenAt = entry.lastSeenAt;
|
|
190
|
+
}
|
|
191
|
+
if (entry.createdAt < existing.createdAt) {
|
|
192
|
+
existing.createdAt = entry.createdAt;
|
|
193
|
+
}
|
|
194
|
+
const tags = new Set([...existing.tags, ...entry.tags]);
|
|
195
|
+
existing.tags = [...tags];
|
|
196
|
+
const related = new Set([
|
|
197
|
+
...existing.relatedBugIds,
|
|
198
|
+
...entry.relatedBugIds,
|
|
199
|
+
]);
|
|
200
|
+
existing.relatedBugIds = [...related];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
entries: [...byId.values()].sort((a, b) =>
|
|
206
|
+
a.lastSeenAt < b.lastSeenAt ? 1 : -1
|
|
207
|
+
),
|
|
208
|
+
nextId: maxNextId,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function aggregateBugMemory(cwd: string): BugMemory {
|
|
213
|
+
return aggregateBugMemoryAt(projectDir(cwd));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Action log ─────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
export function aggregateActionLogAt(projDir: string): string {
|
|
219
|
+
type Block = { date: string; content: string; offset: number };
|
|
220
|
+
const blocks: Block[] = [];
|
|
221
|
+
let order = 0;
|
|
222
|
+
|
|
223
|
+
const sources = [
|
|
224
|
+
...listDeviceShardsAt(projDir).map((id) =>
|
|
225
|
+
shardPath(projDir, id, "action-log.md")
|
|
226
|
+
),
|
|
227
|
+
join(projDir, "action-log.md"),
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
for (const path of sources) {
|
|
231
|
+
if (!existsSync(path)) continue;
|
|
232
|
+
const content = safeReadLog(path);
|
|
233
|
+
if (!content) continue;
|
|
234
|
+
const sessions = parseLogSessions(content);
|
|
235
|
+
for (const session of sessions) {
|
|
236
|
+
blocks.push({
|
|
237
|
+
date: session.date,
|
|
238
|
+
content: session.content,
|
|
239
|
+
offset: order++,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
blocks.sort((a, b) => {
|
|
245
|
+
if (a.date !== b.date) return a.date.localeCompare(b.date);
|
|
246
|
+
return a.offset - b.offset;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return blocks.map((b) => b.content).join("");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function aggregateActionLog(cwd: string): string {
|
|
253
|
+
return aggregateActionLogAt(projectDir(cwd));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Learning memory ────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export function aggregateLearningMemoryAt(projDir: string): LearningMemory {
|
|
259
|
+
const canonicalPath = join(projDir, "learning-memory.md");
|
|
260
|
+
let merged: LearningMemory;
|
|
261
|
+
if (existsSync(canonicalPath)) {
|
|
262
|
+
try {
|
|
263
|
+
merged = parseLearningMemory(readFileSync(canonicalPath, "utf-8"));
|
|
264
|
+
} catch {
|
|
265
|
+
merged = createEmptyLearningMemory("unknown");
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
merged = createEmptyLearningMemory("unknown");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const sidecarPath of listLearningMemorySidecarPathsAt(projDir)) {
|
|
272
|
+
let sidecar: LearningMemory;
|
|
273
|
+
try {
|
|
274
|
+
sidecar = parseLearningMemory(readFileSync(sidecarPath, "utf-8"));
|
|
275
|
+
} catch {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
merged.projectName === "unknown" &&
|
|
281
|
+
sidecar.projectName !== "unknown"
|
|
282
|
+
) {
|
|
283
|
+
merged.projectName = sidecar.projectName;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const section of Object.keys(sidecar.sections) as SectionName[]) {
|
|
287
|
+
const existing = new Set(
|
|
288
|
+
merged.sections[section].map((e) => e.trim().toLowerCase())
|
|
289
|
+
);
|
|
290
|
+
for (const entry of sidecar.sections[section]) {
|
|
291
|
+
const norm = entry.trim().toLowerCase();
|
|
292
|
+
if (existing.has(norm)) continue;
|
|
293
|
+
existing.add(norm);
|
|
294
|
+
merged.sections[section].push(entry);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return merged;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function aggregateLearningMemory(cwd: string): LearningMemory {
|
|
303
|
+
return aggregateLearningMemoryAt(projectDir(cwd));
|
|
304
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { atomicWriteJson, safeReadJson } from "./fs-utils";
|
|
2
|
+
import { fileIndexCountersPath } from "./paths";
|
|
3
|
+
|
|
4
|
+
// Per-device telemetry counters. Lives at projects/<id>/.mink-state-counters.json
|
|
5
|
+
// and is gitignored so each device's counts never collide. Aggregated views
|
|
6
|
+
// (dashboard, status) sum across devices via aggregateStateCounters().
|
|
7
|
+
|
|
8
|
+
export interface StateCounters {
|
|
9
|
+
fileIndexHits: number;
|
|
10
|
+
fileIndexMisses: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
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
|
+
export function loadCounters(cwd: string): StateCounters {
|
|
27
|
+
const raw = safeReadJson(fileIndexCountersPath(cwd));
|
|
28
|
+
if (raw !== null && isStateCounters(raw)) return raw;
|
|
29
|
+
return emptyCounters();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function saveCounters(cwd: string, counters: StateCounters): void {
|
|
33
|
+
atomicWriteJson(fileIndexCountersPath(cwd), counters);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function incrementFileIndexHit(cwd: string): void {
|
|
37
|
+
const c = loadCounters(cwd);
|
|
38
|
+
c.fileIndexHits++;
|
|
39
|
+
saveCounters(cwd, c);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function incrementFileIndexMiss(cwd: string): void {
|
|
43
|
+
const c = loadCounters(cwd);
|
|
44
|
+
c.fileIndexMisses++;
|
|
45
|
+
saveCounters(cwd, c);
|
|
46
|
+
}
|