autonomous-flow-daemon 1.6.0 → 1.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/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Project Pattern Correlation Engine (v1.7)
|
|
3
|
+
*
|
|
4
|
+
* Aggregates antibodies across multiple scopes (federated projects) to surface
|
|
5
|
+
* "Global Hotspot" patterns — mistake types that recur in 2+ distinct projects.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Query antibodies grouped by (pattern_type, scope)
|
|
9
|
+
* 2. Tokenize each pattern_type (lowercase, split on separators, remove stop words)
|
|
10
|
+
* 3. Cluster variants with Jaccard similarity ≥ threshold (greedy, representative-based)
|
|
11
|
+
* 4. For each cluster, aggregate distinct scopes + total occurrences
|
|
12
|
+
* 5. Return clusters with scopeCount ≥ minScopes, ranked by scopeCount DESC
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { Database } from "bun:sqlite";
|
|
18
|
+
import { VALIDATORS_DIR } from "../daemon/types";
|
|
19
|
+
|
|
20
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface GlobalHotspot {
|
|
23
|
+
/** Most prevalent pattern_type in this cluster */
|
|
24
|
+
canonicalType: string;
|
|
25
|
+
/** All pattern_type variants grouped in this cluster */
|
|
26
|
+
variants: string[];
|
|
27
|
+
/** Number of distinct project scopes this appeared in */
|
|
28
|
+
scopeCount: number;
|
|
29
|
+
/** Names of the scopes */
|
|
30
|
+
scopes: string[];
|
|
31
|
+
/** Total antibody occurrences across all scopes */
|
|
32
|
+
totalOccurrences: number;
|
|
33
|
+
/** Whether a local validator already covers this pattern */
|
|
34
|
+
alreadyCovered: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Cluster coherence: average pairwise Jaccard similarity of variants.
|
|
37
|
+
* Range 0–1 (1 = all variants are identical).
|
|
38
|
+
*/
|
|
39
|
+
confidence: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CorrelationOptions {
|
|
43
|
+
/** Minimum distinct scopes to qualify as a global hotspot (default: 2) */
|
|
44
|
+
minScopes?: number;
|
|
45
|
+
/** Jaccard similarity threshold for grouping variant pattern_types (default: 0.4) */
|
|
46
|
+
similarityThreshold?: number;
|
|
47
|
+
/** Maximum number of hotspots to return (default: 10) */
|
|
48
|
+
limit?: number;
|
|
49
|
+
/** Include antibodies with scope = "local" in the analysis (default: false) */
|
|
50
|
+
includeLocal?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CorrelationResult {
|
|
54
|
+
hotspots: GlobalHotspot[];
|
|
55
|
+
/** Total distinct scopes seen in the dataset */
|
|
56
|
+
totalScopes: number;
|
|
57
|
+
analysisWindow: {
|
|
58
|
+
scopes: string[];
|
|
59
|
+
antibodyCount: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Tokenization & similarity ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Low-signal words that are stripped before similarity comparison.
|
|
67
|
+
* Keeping these would artificially inflate similarity between unrelated patterns.
|
|
68
|
+
*/
|
|
69
|
+
const STOP_TOKENS = new Set([
|
|
70
|
+
"error", "issue", "guard", "check", "prevent", "detect", "pattern",
|
|
71
|
+
"file", "content", "bad", "invalid", "missing",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
function tokenize(patternType: string): Set<string> {
|
|
75
|
+
return new Set(
|
|
76
|
+
patternType
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.split(/[-_\s/]+/)
|
|
79
|
+
.filter(t => t.length >= 3 && !STOP_TOKENS.has(t)),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function jaccardSimilarity(a: Set<string>, b: Set<string>): number {
|
|
84
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
85
|
+
let intersection = 0;
|
|
86
|
+
for (const t of a) { if (b.has(t)) intersection++; }
|
|
87
|
+
const union = new Set([...a, ...b]).size;
|
|
88
|
+
return union === 0 ? 0 : intersection / union;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Core aggregation ─────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Main entry point. Returns global hotspot patterns with scope correlation.
|
|
95
|
+
*/
|
|
96
|
+
export function correlatePatterns(db: Database, opts: CorrelationOptions = {}): CorrelationResult {
|
|
97
|
+
const minScopes = opts.minScopes ?? 2;
|
|
98
|
+
const threshold = opts.similarityThreshold ?? 0.4;
|
|
99
|
+
const limit = opts.limit ?? 10;
|
|
100
|
+
const includeLocal = opts.includeLocal ?? false;
|
|
101
|
+
|
|
102
|
+
const whereClause = includeLocal ? "" : "WHERE scope != 'local'";
|
|
103
|
+
|
|
104
|
+
const rows = db.prepare(`
|
|
105
|
+
SELECT pattern_type, scope, COUNT(*) AS cnt
|
|
106
|
+
FROM antibodies
|
|
107
|
+
${whereClause}
|
|
108
|
+
GROUP BY pattern_type, scope
|
|
109
|
+
ORDER BY cnt DESC
|
|
110
|
+
`).all() as { pattern_type: string; scope: string; cnt: number }[];
|
|
111
|
+
|
|
112
|
+
if (rows.length === 0) {
|
|
113
|
+
return { hotspots: [], totalScopes: 0, analysisWindow: { scopes: [], antibodyCount: 0 } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const allScopes = new Set(rows.map(r => r.scope));
|
|
117
|
+
const totalAntibodies = rows.reduce((sum, r) => sum + r.cnt, 0);
|
|
118
|
+
|
|
119
|
+
// Build per-type aggregation map
|
|
120
|
+
const typeMap = new Map<string, { scopes: Set<string>; total: number }>();
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
let entry = typeMap.get(row.pattern_type);
|
|
123
|
+
if (!entry) {
|
|
124
|
+
entry = { scopes: new Set(), total: 0 };
|
|
125
|
+
typeMap.set(row.pattern_type, entry);
|
|
126
|
+
}
|
|
127
|
+
entry.scopes.add(row.scope);
|
|
128
|
+
entry.total += row.cnt;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Pre-compute token sets for each pattern_type
|
|
132
|
+
const tokenCache = new Map<string, Set<string>>();
|
|
133
|
+
for (const pt of typeMap.keys()) {
|
|
134
|
+
tokenCache.set(pt, tokenize(pt));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Greedy clustering: assign each type to the first cluster whose representative
|
|
138
|
+
// has Jaccard similarity ≥ threshold, otherwise start a new cluster.
|
|
139
|
+
const clusters: string[][] = [];
|
|
140
|
+
for (const t of typeMap.keys()) {
|
|
141
|
+
let placed = false;
|
|
142
|
+
for (const cluster of clusters) {
|
|
143
|
+
const repTokens = tokenCache.get(cluster[0])!;
|
|
144
|
+
const sim = jaccardSimilarity(tokenCache.get(t)!, repTokens);
|
|
145
|
+
if (sim >= threshold) {
|
|
146
|
+
cluster.push(t);
|
|
147
|
+
placed = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!placed) clusters.push([t]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build hotspot from each cluster
|
|
155
|
+
const coveredTargets = getExistingValidatorTargets();
|
|
156
|
+
const hotspots: GlobalHotspot[] = [];
|
|
157
|
+
|
|
158
|
+
for (const variants of clusters) {
|
|
159
|
+
// Merge scopes and totals across all variants in the cluster
|
|
160
|
+
const mergedScopes = new Set<string>();
|
|
161
|
+
let total = 0;
|
|
162
|
+
for (const v of variants) {
|
|
163
|
+
const entry = typeMap.get(v)!;
|
|
164
|
+
for (const s of entry.scopes) mergedScopes.add(s);
|
|
165
|
+
total += entry.total;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (mergedScopes.size < minScopes) continue;
|
|
169
|
+
|
|
170
|
+
// Sort variants by total occurrences descending (canonical = most prevalent)
|
|
171
|
+
variants.sort((a, b) => (typeMap.get(b)?.total ?? 0) - (typeMap.get(a)?.total ?? 0));
|
|
172
|
+
const canonicalType = variants[0];
|
|
173
|
+
|
|
174
|
+
// Confidence = average pairwise Jaccard within the cluster
|
|
175
|
+
let confidence = 1;
|
|
176
|
+
if (variants.length > 1) {
|
|
177
|
+
let simSum = 0;
|
|
178
|
+
let pairs = 0;
|
|
179
|
+
for (let i = 0; i < variants.length; i++) {
|
|
180
|
+
for (let j = i + 1; j < variants.length; j++) {
|
|
181
|
+
simSum += jaccardSimilarity(tokenCache.get(variants[i])!, tokenCache.get(variants[j])!);
|
|
182
|
+
pairs++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
confidence = pairs > 0 ? simSum / pairs : 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Coverage: any existing validator whose filename tokens overlap with canonicalType tokens
|
|
189
|
+
const canonTokens = tokenCache.get(canonicalType) ?? tokenize(canonicalType);
|
|
190
|
+
const alreadyCovered = [...coveredTargets].some(target => {
|
|
191
|
+
const targetTokens = tokenize(target.replace(/\.(ts|json|md|js)$/, ""));
|
|
192
|
+
return jaccardSimilarity(canonTokens, targetTokens) >= 0.4;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
hotspots.push({
|
|
196
|
+
canonicalType,
|
|
197
|
+
variants,
|
|
198
|
+
scopeCount: mergedScopes.size,
|
|
199
|
+
scopes: [...mergedScopes].sort(),
|
|
200
|
+
totalOccurrences: total,
|
|
201
|
+
alreadyCovered,
|
|
202
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
hotspots.sort((a, b) => b.scopeCount - a.scopeCount || b.totalOccurrences - a.totalOccurrences);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
hotspots: hotspots.slice(0, limit),
|
|
210
|
+
totalScopes: allScopes.size,
|
|
211
|
+
analysisWindow: { scopes: [...allScopes].sort(), antibodyCount: totalAntibodies },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Hotspot lookup for suggest integration ───────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Find a matching GlobalHotspot for a given mistakeType string.
|
|
219
|
+
* Used by `afd suggest --cross` to annotate suggestions as "Community Verified".
|
|
220
|
+
*/
|
|
221
|
+
export function findMatchingHotspot(
|
|
222
|
+
mistakeType: string,
|
|
223
|
+
hotspots: GlobalHotspot[],
|
|
224
|
+
threshold = 0.35,
|
|
225
|
+
): GlobalHotspot | null {
|
|
226
|
+
const tokens = tokenize(mistakeType);
|
|
227
|
+
let best: GlobalHotspot | null = null;
|
|
228
|
+
let bestSim = threshold;
|
|
229
|
+
|
|
230
|
+
for (const h of hotspots) {
|
|
231
|
+
for (const v of h.variants) {
|
|
232
|
+
const sim = jaccardSimilarity(tokens, tokenize(v));
|
|
233
|
+
if (sim > bestSim) {
|
|
234
|
+
bestSim = sim;
|
|
235
|
+
best = h;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return best;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Validator coverage helper ────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
function getExistingValidatorTargets(): Set<string> {
|
|
245
|
+
const targets = new Set<string>();
|
|
246
|
+
if (!existsSync(VALIDATORS_DIR)) return targets;
|
|
247
|
+
|
|
248
|
+
let files: string[];
|
|
249
|
+
try {
|
|
250
|
+
files = readdirSync(VALIDATORS_DIR).filter(f => f.endsWith(".js"));
|
|
251
|
+
} catch {
|
|
252
|
+
return targets;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
try {
|
|
257
|
+
const code = readFileSync(join(VALIDATORS_DIR, file), "utf-8");
|
|
258
|
+
const matches = code.matchAll(/endsWith\(["']([^"']+)["']\)/g);
|
|
259
|
+
for (const m of matches) targets.add(m[1]);
|
|
260
|
+
} catch {
|
|
261
|
+
// skip unreadable files
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return targets;
|
|
265
|
+
}
|
package/src/core/db.ts
CHANGED
|
@@ -1,117 +1,145 @@
|
|
|
1
|
-
import { mkdirSync } from "fs";
|
|
2
|
-
import { Database } from "bun:sqlite";
|
|
3
|
-
import { resolveWorkspacePaths } from "../constants";
|
|
4
|
-
|
|
5
|
-
export function initDb(): Database {
|
|
6
|
-
const paths = resolveWorkspacePaths();
|
|
7
|
-
mkdirSync(paths.afdDir, { recursive: true });
|
|
8
|
-
const db = new Database(paths.dbFile);
|
|
9
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
10
|
-
|
|
11
|
-
db.exec(`
|
|
12
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
13
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14
|
-
type TEXT NOT NULL,
|
|
15
|
-
path TEXT NOT NULL,
|
|
16
|
-
timestamp INTEGER NOT NULL
|
|
17
|
-
)
|
|
18
|
-
`);
|
|
19
|
-
|
|
20
|
-
db.exec(`
|
|
21
|
-
CREATE TABLE IF NOT EXISTS antibodies (
|
|
22
|
-
id TEXT PRIMARY KEY,
|
|
23
|
-
pattern_type TEXT NOT NULL,
|
|
24
|
-
file_target TEXT NOT NULL,
|
|
25
|
-
patch_op TEXT NOT NULL,
|
|
26
|
-
dormant INTEGER NOT NULL DEFAULT 0,
|
|
27
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
28
|
-
)
|
|
29
|
-
`);
|
|
30
|
-
|
|
31
|
-
// Migration: add dormant column if missing (existing DBs)
|
|
32
|
-
try {
|
|
33
|
-
db.exec("ALTER TABLE antibodies ADD COLUMN dormant INTEGER NOT NULL DEFAULT 0");
|
|
34
|
-
} catch {
|
|
35
|
-
// Column already exists — safe to ignore
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
`);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
db.exec(`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1
|
+
import { mkdirSync } from "fs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
4
|
+
|
|
5
|
+
export function initDb(): Database {
|
|
6
|
+
const paths = resolveWorkspacePaths();
|
|
7
|
+
mkdirSync(paths.afdDir, { recursive: true });
|
|
8
|
+
const db = new Database(paths.dbFile);
|
|
9
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
10
|
+
|
|
11
|
+
db.exec(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
13
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14
|
+
type TEXT NOT NULL,
|
|
15
|
+
path TEXT NOT NULL,
|
|
16
|
+
timestamp INTEGER NOT NULL
|
|
17
|
+
)
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS antibodies (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
pattern_type TEXT NOT NULL,
|
|
24
|
+
file_target TEXT NOT NULL,
|
|
25
|
+
patch_op TEXT NOT NULL,
|
|
26
|
+
dormant INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
// Migration: add dormant column if missing (existing DBs)
|
|
32
|
+
try {
|
|
33
|
+
db.exec("ALTER TABLE antibodies ADD COLUMN dormant INTEGER NOT NULL DEFAULT 0");
|
|
34
|
+
} catch {
|
|
35
|
+
// Column already exists — safe to ignore
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Migration: federation columns (v1.7)
|
|
39
|
+
try { db.exec("ALTER TABLE antibodies ADD COLUMN scope TEXT NOT NULL DEFAULT 'local'"); } catch { /* exists */ }
|
|
40
|
+
try { db.exec("ALTER TABLE antibodies ADD COLUMN ab_version INTEGER NOT NULL DEFAULT 1"); } catch { /* exists */ }
|
|
41
|
+
try { db.exec("ALTER TABLE antibodies ADD COLUMN updated_at TEXT NOT NULL DEFAULT (datetime('now'))"); } catch { /* exists */ }
|
|
42
|
+
|
|
43
|
+
db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS unlink_log (
|
|
45
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
+
file_path TEXT NOT NULL,
|
|
47
|
+
timestamp INTEGER NOT NULL
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
// ── Hologram Stats: lifetime (single row) + daily (7-day rolling) ──
|
|
52
|
+
db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS hologram_lifetime (
|
|
54
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
55
|
+
total_requests INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
total_original_chars INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
total_hologram_chars INTEGER NOT NULL DEFAULT 0
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
db.exec(`INSERT OR IGNORE INTO hologram_lifetime (id) VALUES (1)`);
|
|
61
|
+
|
|
62
|
+
db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS hologram_daily (
|
|
64
|
+
date TEXT PRIMARY KEY,
|
|
65
|
+
requests INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
original_chars INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
hologram_chars INTEGER NOT NULL DEFAULT 0
|
|
68
|
+
)
|
|
69
|
+
`);
|
|
70
|
+
// Purge entries older than 7 days
|
|
71
|
+
db.exec(`DELETE FROM hologram_daily WHERE date < date('now', '-7 days')`);
|
|
72
|
+
|
|
73
|
+
// ── Context Savings: workspace-map and pinpoint read tracking ──
|
|
74
|
+
db.exec(`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS ctx_savings_daily (
|
|
76
|
+
date TEXT NOT NULL,
|
|
77
|
+
type TEXT NOT NULL,
|
|
78
|
+
requests INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
original_chars INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
saved_chars INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
PRIMARY KEY (date, type)
|
|
82
|
+
)
|
|
83
|
+
`);
|
|
84
|
+
db.exec(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS ctx_savings_lifetime (
|
|
86
|
+
type TEXT NOT NULL PRIMARY KEY,
|
|
87
|
+
total_requests INTEGER NOT NULL DEFAULT 0,
|
|
88
|
+
total_original_chars INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
total_saved_chars INTEGER NOT NULL DEFAULT 0
|
|
90
|
+
)
|
|
91
|
+
`);
|
|
92
|
+
db.exec(`INSERT OR IGNORE INTO ctx_savings_lifetime (type) VALUES ('wsmap')`);
|
|
93
|
+
db.exec(`INSERT OR IGNORE INTO ctx_savings_lifetime (type) VALUES ('pinpoint')`);
|
|
94
|
+
db.exec(`DELETE FROM ctx_savings_daily WHERE date < date('now', '-7 days')`);
|
|
95
|
+
|
|
96
|
+
// ── Telemetry: feature usage tracking ──
|
|
97
|
+
db.exec(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS telemetry (
|
|
99
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
category TEXT NOT NULL,
|
|
101
|
+
action TEXT NOT NULL,
|
|
102
|
+
detail TEXT,
|
|
103
|
+
duration_ms REAL,
|
|
104
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
105
|
+
)
|
|
106
|
+
`);
|
|
107
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_cat_ts ON telemetry(category, timestamp)`);
|
|
108
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_action ON telemetry(action)`);
|
|
109
|
+
// Purge raw telemetry older than 30 days
|
|
110
|
+
db.exec(`DELETE FROM telemetry WHERE timestamp < unixepoch() * 1000 - 30 * 86400000`);
|
|
111
|
+
|
|
112
|
+
// ── Mistake History: passive defense tracking ──
|
|
113
|
+
db.exec(`
|
|
114
|
+
CREATE TABLE IF NOT EXISTS mistake_history (
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
file_path TEXT NOT NULL,
|
|
117
|
+
mistake_type TEXT NOT NULL,
|
|
118
|
+
description TEXT NOT NULL,
|
|
119
|
+
antibody_id TEXT,
|
|
120
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
121
|
+
)
|
|
122
|
+
`);
|
|
123
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_path ON mistake_history(file_path)`);
|
|
124
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_ts ON mistake_history(timestamp)`);
|
|
125
|
+
db.exec(`DELETE FROM mistake_history WHERE timestamp < unixepoch() * 1000 - 90 * 86400000`);
|
|
126
|
+
|
|
127
|
+
// Migration: move data from old hologram_stats table if it exists
|
|
128
|
+
try {
|
|
129
|
+
const old = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_stats WHERE id = 1").get() as {
|
|
130
|
+
total_requests: number; total_original_chars: number; total_hologram_chars: number;
|
|
131
|
+
} | null;
|
|
132
|
+
if (old && old.total_requests > 0) {
|
|
133
|
+
db.transaction(() => {
|
|
134
|
+
db.prepare(
|
|
135
|
+
"UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
|
|
136
|
+
).run(old.total_requests, old.total_original_chars, old.total_hologram_chars);
|
|
137
|
+
db.exec("DROP TABLE hologram_stats");
|
|
138
|
+
})();
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Old table doesn't exist — clean install
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return db;
|
|
145
|
+
}
|
package/src/core/discovery.ts
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smart Discovery — scans project root for AI-agent config patterns.
|
|
3
|
-
*
|
|
4
|
-
* Runs ONCE at startup. O(n) existsSync calls on a known candidate list.
|
|
5
|
-
* No directory traversal, no glob — just fast stat checks (< 5ms).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync } from "fs";
|
|
9
|
-
|
|
10
|
-
/** All known AI-agent config patterns to probe. */
|
|
11
|
-
const DISCOVERY_CANDIDATES = [
|
|
12
|
-
// Claude Code ecosystem
|
|
13
|
-
".claude/",
|
|
14
|
-
"CLAUDE.md",
|
|
15
|
-
".claudeignore",
|
|
16
|
-
// Cursor ecosystem
|
|
17
|
-
".cursorrules",
|
|
18
|
-
".cursorignore",
|
|
19
|
-
// Git essentials
|
|
20
|
-
".gitignore",
|
|
21
|
-
// MCP configs
|
|
22
|
-
"mcp-config.json",
|
|
23
|
-
".mcp.json",
|
|
24
|
-
// Generic AI config directories
|
|
25
|
-
".ai/",
|
|
26
|
-
// Custom rules (various tools)
|
|
27
|
-
".customrules",
|
|
28
|
-
".windsurfrules",
|
|
29
|
-
// Copilot
|
|
30
|
-
".github/copilot-instructions.md",
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
|
-
export interface DiscoveryResult {
|
|
34
|
-
/** All targets that exist on disk (merged with defaults, deduplicated). */
|
|
35
|
-
targets: string[];
|
|
36
|
-
/** How many were found via smart discovery (beyond the hardcoded defaults). */
|
|
37
|
-
discoveredCount: number;
|
|
38
|
-
/** Elapsed time in ms. */
|
|
39
|
-
elapsedMs: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Discover AI-context files in the project root.
|
|
44
|
-
* Merges found targets with the provided defaults, deduplicates, and returns.
|
|
45
|
-
*/
|
|
46
|
-
export function discoverWatchTargets(defaults: readonly string[]): DiscoveryResult {
|
|
47
|
-
const t0 = performance.now();
|
|
48
|
-
const seen = new Set<string>(defaults);
|
|
49
|
-
let discoveredCount = 0;
|
|
50
|
-
|
|
51
|
-
for (const candidate of DISCOVERY_CANDIDATES) {
|
|
52
|
-
if (seen.has(candidate)) continue;
|
|
53
|
-
if (existsSync(candidate)) {
|
|
54
|
-
seen.add(candidate);
|
|
55
|
-
discoveredCount++;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const elapsedMs = Math.round((performance.now() - t0) * 100) / 100;
|
|
60
|
-
return {
|
|
61
|
-
targets: [...seen],
|
|
62
|
-
discoveredCount,
|
|
63
|
-
elapsedMs,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Smart Discovery — scans project root for AI-agent config patterns.
|
|
3
|
+
*
|
|
4
|
+
* Runs ONCE at startup. O(n) existsSync calls on a known candidate list.
|
|
5
|
+
* No directory traversal, no glob — just fast stat checks (< 5ms).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
|
|
10
|
+
/** All known AI-agent config patterns to probe. */
|
|
11
|
+
const DISCOVERY_CANDIDATES = [
|
|
12
|
+
// Claude Code ecosystem
|
|
13
|
+
".claude/",
|
|
14
|
+
"CLAUDE.md",
|
|
15
|
+
".claudeignore",
|
|
16
|
+
// Cursor ecosystem
|
|
17
|
+
".cursorrules",
|
|
18
|
+
".cursorignore",
|
|
19
|
+
// Git essentials
|
|
20
|
+
".gitignore",
|
|
21
|
+
// MCP configs
|
|
22
|
+
"mcp-config.json",
|
|
23
|
+
".mcp.json",
|
|
24
|
+
// Generic AI config directories
|
|
25
|
+
".ai/",
|
|
26
|
+
// Custom rules (various tools)
|
|
27
|
+
".customrules",
|
|
28
|
+
".windsurfrules",
|
|
29
|
+
// Copilot
|
|
30
|
+
".github/copilot-instructions.md",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export interface DiscoveryResult {
|
|
34
|
+
/** All targets that exist on disk (merged with defaults, deduplicated). */
|
|
35
|
+
targets: string[];
|
|
36
|
+
/** How many were found via smart discovery (beyond the hardcoded defaults). */
|
|
37
|
+
discoveredCount: number;
|
|
38
|
+
/** Elapsed time in ms. */
|
|
39
|
+
elapsedMs: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Discover AI-context files in the project root.
|
|
44
|
+
* Merges found targets with the provided defaults, deduplicates, and returns.
|
|
45
|
+
*/
|
|
46
|
+
export function discoverWatchTargets(defaults: readonly string[]): DiscoveryResult {
|
|
47
|
+
const t0 = performance.now();
|
|
48
|
+
const seen = new Set<string>(defaults);
|
|
49
|
+
let discoveredCount = 0;
|
|
50
|
+
|
|
51
|
+
for (const candidate of DISCOVERY_CANDIDATES) {
|
|
52
|
+
if (seen.has(candidate)) continue;
|
|
53
|
+
if (existsSync(candidate)) {
|
|
54
|
+
seen.add(candidate);
|
|
55
|
+
discoveredCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const elapsedMs = Math.round((performance.now() - t0) * 100) / 100;
|
|
60
|
+
return {
|
|
61
|
+
targets: [...seen],
|
|
62
|
+
discoveredCount,
|
|
63
|
+
elapsedMs,
|
|
64
|
+
};
|
|
65
|
+
}
|