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.
Files changed (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. 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
- db.exec(`
39
- CREATE TABLE IF NOT EXISTS unlink_log (
40
- id INTEGER PRIMARY KEY AUTOINCREMENT,
41
- file_path TEXT NOT NULL,
42
- timestamp INTEGER NOT NULL
43
- )
44
- `);
45
-
46
- // ── Hologram Stats: lifetime (single row) + daily (7-day rolling) ──
47
- db.exec(`
48
- CREATE TABLE IF NOT EXISTS hologram_lifetime (
49
- id INTEGER PRIMARY KEY CHECK (id = 1),
50
- total_requests INTEGER NOT NULL DEFAULT 0,
51
- total_original_chars INTEGER NOT NULL DEFAULT 0,
52
- total_hologram_chars INTEGER NOT NULL DEFAULT 0
53
- )
54
- `);
55
- db.exec(`INSERT OR IGNORE INTO hologram_lifetime (id) VALUES (1)`);
56
-
57
- db.exec(`
58
- CREATE TABLE IF NOT EXISTS hologram_daily (
59
- date TEXT PRIMARY KEY,
60
- requests INTEGER NOT NULL DEFAULT 0,
61
- original_chars INTEGER NOT NULL DEFAULT 0,
62
- hologram_chars INTEGER NOT NULL DEFAULT 0
63
- )
64
- `);
65
- // Purge entries older than 7 days
66
- db.exec(`DELETE FROM hologram_daily WHERE date < date('now', '-7 days')`);
67
-
68
- // ── Telemetry: feature usage tracking ──
69
- db.exec(`
70
- CREATE TABLE IF NOT EXISTS telemetry (
71
- id INTEGER PRIMARY KEY AUTOINCREMENT,
72
- category TEXT NOT NULL,
73
- action TEXT NOT NULL,
74
- detail TEXT,
75
- duration_ms REAL,
76
- timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
77
- )
78
- `);
79
- db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_cat_ts ON telemetry(category, timestamp)`);
80
- db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_action ON telemetry(action)`);
81
- // Purge raw telemetry older than 30 days
82
- db.exec(`DELETE FROM telemetry WHERE timestamp < unixepoch() * 1000 - 30 * 86400000`);
83
-
84
- // ── Mistake History: passive defense tracking ──
85
- db.exec(`
86
- CREATE TABLE IF NOT EXISTS mistake_history (
87
- id INTEGER PRIMARY KEY AUTOINCREMENT,
88
- file_path TEXT NOT NULL,
89
- mistake_type TEXT NOT NULL,
90
- description TEXT NOT NULL,
91
- antibody_id TEXT,
92
- timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
93
- )
94
- `);
95
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_path ON mistake_history(file_path)`);
96
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_ts ON mistake_history(timestamp)`);
97
- db.exec(`DELETE FROM mistake_history WHERE timestamp < unixepoch() * 1000 - 30 * 86400000`);
98
-
99
- // Migration: move data from old hologram_stats table if it exists
100
- try {
101
- const old = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_stats WHERE id = 1").get() as {
102
- total_requests: number; total_original_chars: number; total_hologram_chars: number;
103
- } | null;
104
- if (old && old.total_requests > 0) {
105
- db.transaction(() => {
106
- db.prepare(
107
- "UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
108
- ).run(old.total_requests, old.total_original_chars, old.total_hologram_chars);
109
- db.exec("DROP TABLE hologram_stats");
110
- })();
111
- }
112
- } catch {
113
- // Old table doesn't exist — clean install
114
- }
115
-
116
- return db;
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
+ }
@@ -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
+ }