@drewpayment/mink 0.1.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/README.md +347 -0
- package/package.json +32 -0
- package/src/cli.ts +176 -0
- package/src/commands/bug-search.ts +32 -0
- package/src/commands/config.ts +109 -0
- package/src/commands/cron.ts +295 -0
- package/src/commands/daemon.ts +46 -0
- package/src/commands/dashboard.ts +21 -0
- package/src/commands/designqc.ts +160 -0
- package/src/commands/detect-waste.ts +81 -0
- package/src/commands/framework-advisor.ts +52 -0
- package/src/commands/init.ts +159 -0
- package/src/commands/post-read.ts +123 -0
- package/src/commands/post-write.ts +157 -0
- package/src/commands/pre-read.ts +109 -0
- package/src/commands/pre-write.ts +136 -0
- package/src/commands/reflect.ts +39 -0
- package/src/commands/restore.ts +31 -0
- package/src/commands/scan.ts +101 -0
- package/src/commands/session-start.ts +21 -0
- package/src/commands/session-stop.ts +115 -0
- package/src/commands/status.ts +152 -0
- package/src/commands/update.ts +121 -0
- package/src/core/action-log.ts +341 -0
- package/src/core/backup.ts +122 -0
- package/src/core/bug-memory.ts +223 -0
- package/src/core/cron-parser.ts +94 -0
- package/src/core/daemon.ts +152 -0
- package/src/core/dashboard-api.ts +280 -0
- package/src/core/dashboard-server.ts +580 -0
- package/src/core/description.ts +232 -0
- package/src/core/design-eval/capture.ts +269 -0
- package/src/core/design-eval/route-detect.ts +165 -0
- package/src/core/design-eval/server-detect.ts +91 -0
- package/src/core/framework-advisor/catalog.ts +360 -0
- package/src/core/framework-advisor/decision-tree.ts +287 -0
- package/src/core/framework-advisor/generate.ts +132 -0
- package/src/core/framework-advisor/migration-prompts.ts +502 -0
- package/src/core/framework-advisor/validate.ts +137 -0
- package/src/core/fs-utils.ts +30 -0
- package/src/core/global-config.ts +74 -0
- package/src/core/index-store.ts +72 -0
- package/src/core/learning-memory.ts +120 -0
- package/src/core/paths.ts +86 -0
- package/src/core/pattern-engine.ts +108 -0
- package/src/core/project-id.ts +19 -0
- package/src/core/project-registry.ts +64 -0
- package/src/core/reflection.ts +256 -0
- package/src/core/scanner.ts +99 -0
- package/src/core/scheduler.ts +352 -0
- package/src/core/seed.ts +239 -0
- package/src/core/session.ts +128 -0
- package/src/core/stdin.ts +13 -0
- package/src/core/task-registry.ts +202 -0
- package/src/core/token-estimate.ts +36 -0
- package/src/core/token-ledger.ts +185 -0
- package/src/core/waste-detection.ts +214 -0
- package/src/core/write-exclusions.ts +24 -0
- package/src/types/action-log.ts +20 -0
- package/src/types/backup.ts +6 -0
- package/src/types/bug-memory.ts +24 -0
- package/src/types/config.ts +59 -0
- package/src/types/dashboard.ts +104 -0
- package/src/types/design-eval.ts +64 -0
- package/src/types/file-index.ts +38 -0
- package/src/types/framework-advisor.ts +97 -0
- package/src/types/hook-input.ts +27 -0
- package/src/types/learning-memory.ts +36 -0
- package/src/types/scheduler.ts +82 -0
- package/src/types/session.ts +50 -0
- package/src/types/token-ledger.ts +43 -0
- package/src/types/waste-detection.ts +21 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import type { TokenLedger, LedgerSession, LifetimeCounters } from "../types/token-ledger";
|
|
3
|
+
import type { SessionFinalizer, SessionSummary } from "../types/session";
|
|
4
|
+
import { atomicWriteJson, safeReadJson } from "./fs-utils";
|
|
5
|
+
|
|
6
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function addToLifetime(lifetime: LifetimeCounters, session: LedgerSession): void {
|
|
9
|
+
lifetime.totalTokens += session.totals.estimatedTokens;
|
|
10
|
+
lifetime.totalReads += session.totals.readCount;
|
|
11
|
+
lifetime.totalWrites += session.totals.writeCount;
|
|
12
|
+
lifetime.totalSessions += 1;
|
|
13
|
+
lifetime.totalFileIndexHits += session.totals.fileIndexHits;
|
|
14
|
+
lifetime.totalFileIndexMisses += session.totals.fileIndexMisses;
|
|
15
|
+
lifetime.totalRepeatedReads += session.totals.repeatedReads;
|
|
16
|
+
lifetime.totalEstimatedSavings += session.estimatedSavings;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function subtractFromLifetime(lifetime: LifetimeCounters, session: LedgerSession): void {
|
|
20
|
+
lifetime.totalTokens -= session.totals.estimatedTokens;
|
|
21
|
+
lifetime.totalReads -= session.totals.readCount;
|
|
22
|
+
lifetime.totalWrites -= session.totals.writeCount;
|
|
23
|
+
lifetime.totalSessions -= 1;
|
|
24
|
+
lifetime.totalFileIndexHits -= session.totals.fileIndexHits;
|
|
25
|
+
lifetime.totalFileIndexMisses -= session.totals.fileIndexMisses;
|
|
26
|
+
lifetime.totalRepeatedReads -= session.totals.repeatedReads;
|
|
27
|
+
lifetime.totalEstimatedSavings -= session.estimatedSavings;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Core functions ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export function createEmptyLedger(): TokenLedger {
|
|
33
|
+
return {
|
|
34
|
+
lifetime: {
|
|
35
|
+
totalTokens: 0,
|
|
36
|
+
totalReads: 0,
|
|
37
|
+
totalWrites: 0,
|
|
38
|
+
totalSessions: 0,
|
|
39
|
+
totalFileIndexHits: 0,
|
|
40
|
+
totalFileIndexMisses: 0,
|
|
41
|
+
totalRepeatedReads: 0,
|
|
42
|
+
totalEstimatedSavings: 0,
|
|
43
|
+
},
|
|
44
|
+
sessions: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isTokenLedger(value: unknown): value is TokenLedger {
|
|
49
|
+
if (value === null || typeof value !== "object") return false;
|
|
50
|
+
const obj = value as Record<string, unknown>;
|
|
51
|
+
return (
|
|
52
|
+
typeof obj.lifetime === "object" &&
|
|
53
|
+
obj.lifetime !== null &&
|
|
54
|
+
Array.isArray(obj.sessions)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadLedger(ledgerPath: string): TokenLedger {
|
|
59
|
+
const raw = safeReadJson(ledgerPath);
|
|
60
|
+
if (raw === null) {
|
|
61
|
+
return createEmptyLedger();
|
|
62
|
+
}
|
|
63
|
+
if (!isTokenLedger(raw)) {
|
|
64
|
+
console.warn(`[mink] Warning: corrupt token ledger at ${ledgerPath}, starting fresh`);
|
|
65
|
+
return createEmptyLedger();
|
|
66
|
+
}
|
|
67
|
+
return raw;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveLedger(ledgerPath: string, ledger: TokenLedger): void {
|
|
71
|
+
atomicWriteJson(ledgerPath, ledger);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Task 3: Append Session ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export function summaryToLedgerSession(summary: SessionSummary): LedgerSession {
|
|
77
|
+
return {
|
|
78
|
+
sessionId: summary.sessionId,
|
|
79
|
+
startTimestamp: summary.startTimestamp,
|
|
80
|
+
endTimestamp: summary.endTimestamp,
|
|
81
|
+
reads: summary.reads.map((r) => ({
|
|
82
|
+
filePath: r.filePath,
|
|
83
|
+
estimatedTokens: r.estimatedTokens,
|
|
84
|
+
readCount: r.readCount,
|
|
85
|
+
})),
|
|
86
|
+
writes: summary.writes.map((w) => ({
|
|
87
|
+
filePath: w.filePath,
|
|
88
|
+
estimatedTokens: w.estimatedTokens,
|
|
89
|
+
action: w.action,
|
|
90
|
+
})),
|
|
91
|
+
totals: {
|
|
92
|
+
readCount: summary.totals.readCount,
|
|
93
|
+
writeCount: summary.totals.writeCount,
|
|
94
|
+
estimatedTokens: summary.totals.estimatedTokens,
|
|
95
|
+
repeatedReads: summary.totals.repeatedReads,
|
|
96
|
+
fileIndexHits: summary.totals.fileIndexHits,
|
|
97
|
+
fileIndexMisses: summary.totals.fileIndexMisses,
|
|
98
|
+
},
|
|
99
|
+
estimatedSavings: summary.estimatedSavings,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function appendSession(ledger: TokenLedger, summary: SessionSummary): void {
|
|
104
|
+
const session = summaryToLedgerSession(summary);
|
|
105
|
+
ledger.sessions.push(session);
|
|
106
|
+
addToLifetime(ledger.lifetime, session);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Task 4: Update Session ────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export function updateSession(ledger: TokenLedger, summary: SessionSummary): void {
|
|
112
|
+
const idx = ledger.sessions.findIndex((s) => s.sessionId === summary.sessionId);
|
|
113
|
+
if (idx === -1) {
|
|
114
|
+
appendSession(ledger, summary);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const oldSession = ledger.sessions[idx];
|
|
118
|
+
subtractFromLifetime(ledger.lifetime, oldSession);
|
|
119
|
+
const newSession = summaryToLedgerSession(summary);
|
|
120
|
+
addToLifetime(ledger.lifetime, newSession);
|
|
121
|
+
ledger.sessions[idx] = newSession;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Task 5: Archive ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export function archiveIfNeeded(
|
|
127
|
+
ledger: TokenLedger,
|
|
128
|
+
threshold: number = 1000
|
|
129
|
+
): { archived: LedgerSession[] } {
|
|
130
|
+
if (threshold <= 0) {
|
|
131
|
+
return { archived: [] };
|
|
132
|
+
}
|
|
133
|
+
if (ledger.sessions.length <= threshold) {
|
|
134
|
+
return { archived: [] };
|
|
135
|
+
}
|
|
136
|
+
const excess = ledger.sessions.length - threshold;
|
|
137
|
+
const archived = ledger.sessions.splice(0, excess);
|
|
138
|
+
return { archived };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function loadArchive(archivePath: string): LedgerSession[] {
|
|
142
|
+
const raw = safeReadJson(archivePath);
|
|
143
|
+
if (raw === null) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
if (!Array.isArray(raw)) {
|
|
147
|
+
console.warn(`[mink] Warning: corrupt token ledger archive at ${archivePath}, ignoring`);
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
return raw as LedgerSession[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function saveArchive(archivePath: string, newlyArchived: LedgerSession[]): void {
|
|
154
|
+
const existing = loadArchive(archivePath);
|
|
155
|
+
const combined = [...newlyArchived, ...existing];
|
|
156
|
+
atomicWriteJson(archivePath, combined);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Task 6: Ledger Finalizer Factory ─────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export function createLedgerFinalizer(
|
|
162
|
+
projectDir: string,
|
|
163
|
+
archiveThreshold: number = 1000
|
|
164
|
+
): SessionFinalizer {
|
|
165
|
+
const ledgerPath = join(projectDir, "token-ledger.json");
|
|
166
|
+
const archivePath = join(projectDir, "token-ledger-archive.json");
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
appendSession(summary: SessionSummary): void {
|
|
170
|
+
const ledger = loadLedger(ledgerPath);
|
|
171
|
+
appendSession(ledger, summary);
|
|
172
|
+
const { archived } = archiveIfNeeded(ledger, archiveThreshold);
|
|
173
|
+
if (archived.length > 0) {
|
|
174
|
+
saveArchive(archivePath, archived);
|
|
175
|
+
}
|
|
176
|
+
saveLedger(ledgerPath, ledger);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
updateSession(summary: SessionSummary): void {
|
|
180
|
+
const ledger = loadLedger(ledgerPath);
|
|
181
|
+
updateSession(ledger, summary);
|
|
182
|
+
saveLedger(ledgerPath, ledger);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { WasteFlag, DetectionConfig } from "../types/waste-detection";
|
|
2
|
+
import type { LedgerSession, TokenLedger } from "../types/token-ledger";
|
|
3
|
+
import type { FileIndexEntry, FileIndexHeader } from "../types/file-index";
|
|
4
|
+
import { estimateTokens } from "./token-estimate";
|
|
5
|
+
|
|
6
|
+
// ── Default Config ──────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export function defaultDetectionConfig(): DetectionConfig {
|
|
9
|
+
return {
|
|
10
|
+
actionLogBloatThreshold: 5000,
|
|
11
|
+
learningMemoryStaleDays: 14,
|
|
12
|
+
indexMissRateThreshold: 0.20,
|
|
13
|
+
missedIndexMinTokens: 500,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Detector 1: Repeated Reads ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function detectRepeatedReads(
|
|
20
|
+
sessions: LedgerSession[],
|
|
21
|
+
now: string
|
|
22
|
+
): WasteFlag[] {
|
|
23
|
+
const flags: WasteFlag[] = [];
|
|
24
|
+
|
|
25
|
+
for (const session of sessions) {
|
|
26
|
+
for (const read of session.reads) {
|
|
27
|
+
if (read.readCount > 1) {
|
|
28
|
+
const wasted = (read.readCount - 1) * read.estimatedTokens;
|
|
29
|
+
flags.push({
|
|
30
|
+
pattern: "repeated-reads",
|
|
31
|
+
description: `File "${read.filePath}" was read ${read.readCount} times in session ${session.sessionId}`,
|
|
32
|
+
estimatedTokensWasted: wasted,
|
|
33
|
+
suggestion:
|
|
34
|
+
"Use the file index description instead of re-reading, or cache the content within the session.",
|
|
35
|
+
detectedAt: now,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return flags;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Detector 2: Missed Index Opportunities ──────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function detectMissedIndexOpportunities(
|
|
47
|
+
sessions: LedgerSession[],
|
|
48
|
+
indexEntries: Record<string, FileIndexEntry>,
|
|
49
|
+
config: DetectionConfig,
|
|
50
|
+
now: string
|
|
51
|
+
): WasteFlag[] {
|
|
52
|
+
const aggregated = new Map<string, number>();
|
|
53
|
+
|
|
54
|
+
for (const session of sessions) {
|
|
55
|
+
for (const read of session.reads) {
|
|
56
|
+
if (read.estimatedTokens > config.missedIndexMinTokens) {
|
|
57
|
+
const entry = indexEntries[read.filePath];
|
|
58
|
+
if (entry && entry.description) {
|
|
59
|
+
const prev = aggregated.get(read.filePath) ?? 0;
|
|
60
|
+
aggregated.set(read.filePath, prev + read.estimatedTokens);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const flags: WasteFlag[] = [];
|
|
67
|
+
for (const [filePath, totalTokens] of aggregated) {
|
|
68
|
+
const entry = indexEntries[filePath];
|
|
69
|
+
flags.push({
|
|
70
|
+
pattern: "missed-index-opportunity",
|
|
71
|
+
description: `Read of "${filePath}" (~${totalTokens} tokens) could have used index description instead`,
|
|
72
|
+
estimatedTokensWasted: totalTokens,
|
|
73
|
+
suggestion: `Index description available: "${entry.description}". Consider using the index instead of full file reads.`,
|
|
74
|
+
detectedAt: now,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return flags;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Detector 3: Action Log Bloat ────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export function detectActionLogBloat(
|
|
84
|
+
actionLogContent: string,
|
|
85
|
+
config: DetectionConfig,
|
|
86
|
+
now: string
|
|
87
|
+
): WasteFlag | null {
|
|
88
|
+
if (!actionLogContent) return null;
|
|
89
|
+
|
|
90
|
+
const tokenCount = estimateTokens(actionLogContent, "action-log.md");
|
|
91
|
+
if (tokenCount > config.actionLogBloatThreshold) {
|
|
92
|
+
return {
|
|
93
|
+
pattern: "action-log-bloat",
|
|
94
|
+
description: `Action log is ~${tokenCount} tokens, exceeding the ${config.actionLogBloatThreshold} token threshold`,
|
|
95
|
+
estimatedTokensWasted: tokenCount - config.actionLogBloatThreshold,
|
|
96
|
+
suggestion: "Run action log consolidation to reduce size.",
|
|
97
|
+
detectedAt: now,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Detector 4: Learning Memory Staleness ───────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function detectLearningMemoryStaleness(
|
|
107
|
+
lastModifiedMs: number | null,
|
|
108
|
+
config: DetectionConfig,
|
|
109
|
+
now: string
|
|
110
|
+
): WasteFlag | null {
|
|
111
|
+
if (lastModifiedMs === null) {
|
|
112
|
+
return {
|
|
113
|
+
pattern: "learning-memory-staleness",
|
|
114
|
+
description: "Learning memory file is missing",
|
|
115
|
+
estimatedTokensWasted: 0,
|
|
116
|
+
suggestion:
|
|
117
|
+
"Create a learning memory file by running mink init, or manually create learning-memory.md.",
|
|
118
|
+
detectedAt: now,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const nowMs = Date.parse(now);
|
|
123
|
+
const ageMs = nowMs - lastModifiedMs;
|
|
124
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
125
|
+
|
|
126
|
+
if (ageDays > config.learningMemoryStaleDays) {
|
|
127
|
+
const lastUpdate = new Date(lastModifiedMs).toISOString().slice(0, 10);
|
|
128
|
+
return {
|
|
129
|
+
pattern: "learning-memory-staleness",
|
|
130
|
+
description: `Learning memory hasn't been updated in ${Math.floor(ageDays)} days (threshold: ${config.learningMemoryStaleDays} days)`,
|
|
131
|
+
estimatedTokensWasted: 0,
|
|
132
|
+
suggestion: "Review and update the learning memory to keep it current.",
|
|
133
|
+
detectedAt: now,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Detector 5: Index Miss Rate ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function detectIndexMissRate(
|
|
143
|
+
lifetimeHits: number,
|
|
144
|
+
lifetimeMisses: number,
|
|
145
|
+
config: DetectionConfig,
|
|
146
|
+
now: string
|
|
147
|
+
): WasteFlag | null {
|
|
148
|
+
const totalLookups = lifetimeHits + lifetimeMisses;
|
|
149
|
+
if (totalLookups === 0) return null;
|
|
150
|
+
|
|
151
|
+
const missRate = lifetimeMisses / totalLookups;
|
|
152
|
+
if (missRate > config.indexMissRateThreshold) {
|
|
153
|
+
const pct = Math.round(missRate * 100);
|
|
154
|
+
return {
|
|
155
|
+
pattern: "index-miss-rate",
|
|
156
|
+
description: `File index miss rate is ${pct}% (${lifetimeMisses} misses out of ${totalLookups} lookups)`,
|
|
157
|
+
estimatedTokensWasted: 0,
|
|
158
|
+
suggestion: "Run a full rescan with mink scan to update the file index.",
|
|
159
|
+
detectedAt: now,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Orchestrator ────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export function runDetection(
|
|
169
|
+
ledger: TokenLedger,
|
|
170
|
+
indexEntries: Record<string, FileIndexEntry>,
|
|
171
|
+
indexHeader: FileIndexHeader,
|
|
172
|
+
actionLogContent: string,
|
|
173
|
+
learningMemoryMtimeMs: number | null,
|
|
174
|
+
config?: Partial<DetectionConfig>,
|
|
175
|
+
now?: Date
|
|
176
|
+
): WasteFlag[] {
|
|
177
|
+
const fullConfig: DetectionConfig = {
|
|
178
|
+
...defaultDetectionConfig(),
|
|
179
|
+
...config,
|
|
180
|
+
};
|
|
181
|
+
const nowStr = (now ?? new Date()).toISOString();
|
|
182
|
+
|
|
183
|
+
const flags: WasteFlag[] = [];
|
|
184
|
+
|
|
185
|
+
flags.push(...detectRepeatedReads(ledger.sessions, nowStr));
|
|
186
|
+
flags.push(
|
|
187
|
+
...detectMissedIndexOpportunities(
|
|
188
|
+
ledger.sessions,
|
|
189
|
+
indexEntries,
|
|
190
|
+
fullConfig,
|
|
191
|
+
nowStr
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const bloatFlag = detectActionLogBloat(actionLogContent, fullConfig, nowStr);
|
|
196
|
+
if (bloatFlag) flags.push(bloatFlag);
|
|
197
|
+
|
|
198
|
+
const stalenessFlag = detectLearningMemoryStaleness(
|
|
199
|
+
learningMemoryMtimeMs,
|
|
200
|
+
fullConfig,
|
|
201
|
+
nowStr
|
|
202
|
+
);
|
|
203
|
+
if (stalenessFlag) flags.push(stalenessFlag);
|
|
204
|
+
|
|
205
|
+
const missRateFlag = detectIndexMissRate(
|
|
206
|
+
indexHeader.lifetimeHits,
|
|
207
|
+
indexHeader.lifetimeMisses,
|
|
208
|
+
fullConfig,
|
|
209
|
+
nowStr
|
|
210
|
+
);
|
|
211
|
+
if (missRateFlag) flags.push(missRateFlag);
|
|
212
|
+
|
|
213
|
+
return flags;
|
|
214
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true if the file should be excluded from write tracking.
|
|
5
|
+
* Excluded: .env* files, files inside the .mink state directory.
|
|
6
|
+
*/
|
|
7
|
+
export function isWriteExcluded(relativePath: string): boolean {
|
|
8
|
+
// Skip .mink state directory files
|
|
9
|
+
if (
|
|
10
|
+
relativePath === ".mink" ||
|
|
11
|
+
relativePath.startsWith(".mink/") ||
|
|
12
|
+
relativePath.startsWith(".mink\\")
|
|
13
|
+
) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Skip .env files
|
|
18
|
+
const name = basename(relativePath);
|
|
19
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ActionLogEntry {
|
|
2
|
+
time: string; // HH:MM (UTC)
|
|
3
|
+
action: "Session start" | "Read" | "Create" | "Edit" | "Session end";
|
|
4
|
+
files: string; // Truncated file path or "—"
|
|
5
|
+
outcome: string; // index hit/miss, description, or summary
|
|
6
|
+
tokens: string; // "~NNN" or "—"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ConsolidationConfig {
|
|
10
|
+
maxEntries: number; // default 200
|
|
11
|
+
retentionDays: number; // default 7
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ParsedSession {
|
|
15
|
+
startIndex: number; // char offset of session header
|
|
16
|
+
endIndex: number; // char offset of end of session (before next header or EOF)
|
|
17
|
+
date: string; // parsed date from session header (YYYY-MM-DD)
|
|
18
|
+
entryCount: number; // number of table data rows
|
|
19
|
+
content: string; // full text of this session block
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface BugEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
createdAt: string;
|
|
4
|
+
lastSeenAt: string;
|
|
5
|
+
errorMessage: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
lineNumber?: number;
|
|
8
|
+
rootCause: string;
|
|
9
|
+
fixDescription: string;
|
|
10
|
+
tags: string[];
|
|
11
|
+
occurrenceCount: number;
|
|
12
|
+
relatedBugIds: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BugMemory {
|
|
16
|
+
entries: BugEntry[];
|
|
17
|
+
nextId: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SimilarityMatch {
|
|
21
|
+
entry: BugEntry;
|
|
22
|
+
score: number;
|
|
23
|
+
matchReasons: string[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface GlobalConfig {
|
|
2
|
+
"wiki.path"?: string;
|
|
3
|
+
"wiki.enabled"?: string;
|
|
4
|
+
"wiki.sync-mode"?: string;
|
|
5
|
+
"wiki.git-backup"?: string;
|
|
6
|
+
"wiki.git-remote"?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ConfigKey = keyof GlobalConfig & string;
|
|
10
|
+
|
|
11
|
+
export interface ConfigKeyMeta {
|
|
12
|
+
key: ConfigKey;
|
|
13
|
+
default: string;
|
|
14
|
+
envVar: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CONFIG_KEYS: ConfigKeyMeta[] = [
|
|
19
|
+
{
|
|
20
|
+
key: "wiki.path",
|
|
21
|
+
default: "~/.mink/wiki/",
|
|
22
|
+
envVar: "MINK_WIKI_PATH",
|
|
23
|
+
description: "Wiki vault location",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "wiki.enabled",
|
|
27
|
+
default: "true",
|
|
28
|
+
envVar: "MINK_WIKI_ENABLED",
|
|
29
|
+
description: "Enable/disable the wiki feature",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "wiki.sync-mode",
|
|
33
|
+
default: "immediate",
|
|
34
|
+
envVar: "MINK_WIKI_SYNC_MODE",
|
|
35
|
+
description: "Sync mode: immediate or batched",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "wiki.git-backup",
|
|
39
|
+
default: "false",
|
|
40
|
+
envVar: "MINK_WIKI_GIT_BACKUP",
|
|
41
|
+
description: "Enable/disable auto-commit and push",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "wiki.git-remote",
|
|
45
|
+
default: "origin",
|
|
46
|
+
envVar: "MINK_WIKI_GIT_REMOTE",
|
|
47
|
+
description: "Git remote name for push",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const VALID_KEYS = new Set<string>(CONFIG_KEYS.map((k) => k.key));
|
|
52
|
+
|
|
53
|
+
export function isValidConfigKey(key: string): key is ConfigKey {
|
|
54
|
+
return VALID_KEYS.has(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getConfigKeyMeta(key: ConfigKey): ConfigKeyMeta {
|
|
58
|
+
return CONFIG_KEYS.find((k) => k.key === key)!;
|
|
59
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { LifetimeCounters, LedgerSession } from "./token-ledger";
|
|
2
|
+
import type { WasteFlag } from "./waste-detection";
|
|
3
|
+
import type { FileIndexHeader, FileIndexEntry } from "./file-index";
|
|
4
|
+
import type { BugEntry } from "./bug-memory";
|
|
5
|
+
import type { LearningMemory } from "./learning-memory";
|
|
6
|
+
import type { ParsedSession } from "./action-log";
|
|
7
|
+
import type { TaskDefinition, TaskRunRecord, DeadLetterEntry } from "./scheduler";
|
|
8
|
+
|
|
9
|
+
// ── State File Identifiers ─────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type StateFileId =
|
|
12
|
+
| "token-ledger"
|
|
13
|
+
| "file-index"
|
|
14
|
+
| "learning-memory"
|
|
15
|
+
| "bug-memory"
|
|
16
|
+
| "action-log"
|
|
17
|
+
| "scheduler-manifest"
|
|
18
|
+
| "session"
|
|
19
|
+
| "project-meta"
|
|
20
|
+
| "design-report"
|
|
21
|
+
| "project-switched";
|
|
22
|
+
|
|
23
|
+
// ── SSE Event ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface StateChangeEvent {
|
|
26
|
+
fileId: StateFileId;
|
|
27
|
+
projectId?: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── File Status ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface FileStatus {
|
|
34
|
+
name: string;
|
|
35
|
+
status: "ok" | "missing" | "corrupt";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── API Payloads ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export interface OverviewPayload {
|
|
41
|
+
project: { name: string; description: string; cwd: string } | null;
|
|
42
|
+
daemon: {
|
|
43
|
+
running: boolean;
|
|
44
|
+
pid?: number;
|
|
45
|
+
startedAt?: string;
|
|
46
|
+
uptimeMs?: number;
|
|
47
|
+
};
|
|
48
|
+
summary: {
|
|
49
|
+
totalSessions: number;
|
|
50
|
+
totalTokens: number;
|
|
51
|
+
totalReads: number;
|
|
52
|
+
totalWrites: number;
|
|
53
|
+
estimatedSavings: number;
|
|
54
|
+
};
|
|
55
|
+
stateFiles: FileStatus[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TokenLedgerPayload {
|
|
59
|
+
lifetime: LifetimeCounters;
|
|
60
|
+
sessions: LedgerSession[];
|
|
61
|
+
wasteFlags: WasteFlag[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FileIndexPayload {
|
|
65
|
+
header: FileIndexHeader;
|
|
66
|
+
entries: FileIndexEntry[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SchedulerTaskPayload {
|
|
70
|
+
definition: TaskDefinition;
|
|
71
|
+
state: TaskRunRecord | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SchedulerPayload {
|
|
75
|
+
tasks: SchedulerTaskPayload[];
|
|
76
|
+
deadLetterQueue: DeadLetterEntry[];
|
|
77
|
+
lastHeartbeat: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BugLogPayload {
|
|
81
|
+
entries: BugEntry[];
|
|
82
|
+
nextId: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ActionLogPayload {
|
|
86
|
+
sessions: ParsedSession[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ActionResult {
|
|
90
|
+
success: boolean;
|
|
91
|
+
error?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DesignImagePayload {
|
|
95
|
+
url: string;
|
|
96
|
+
route: string;
|
|
97
|
+
viewport: string;
|
|
98
|
+
section: number;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface DesignPayload {
|
|
103
|
+
images: DesignImagePayload[];
|
|
104
|
+
}
|