@snapback/cli 1.0.11 → 1.1.14
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 +9 -9
- package/dist/{analysis-Z53F5FT2.js → analysis-C6XVLBAL.js} +3 -3
- package/dist/{analysis-Z53F5FT2.js.map → analysis-C6XVLBAL.js.map} +1 -1
- package/dist/{chunk-SVJ67PPQ.js → chunk-2TOJVUVJ.js} +296 -33
- package/dist/chunk-2TOJVUVJ.js.map +1 -0
- package/dist/chunk-5EQLSU5B.js +385 -0
- package/dist/chunk-5EQLSU5B.js.map +1 -0
- package/dist/{chunk-YOVA65PS.js → chunk-A3TUM7U4.js} +320 -63
- package/dist/chunk-A3TUM7U4.js.map +1 -0
- package/dist/{chunk-ISVRGBWT.js → chunk-LEXNOXPV.js} +6030 -632
- package/dist/chunk-LEXNOXPV.js.map +1 -0
- package/dist/{chunk-G7QXHNGB.js → chunk-OJNDAPC2.js} +41 -15
- package/dist/chunk-OJNDAPC2.js.map +1 -0
- package/dist/{chunk-NKBZIXCN.js → chunk-Q5XZ3DCB.js} +5 -5
- package/dist/{chunk-NKBZIXCN.js.map → chunk-Q5XZ3DCB.js.map} +1 -1
- package/dist/chunk-QLCHTUT5.js +1067 -0
- package/dist/chunk-QLCHTUT5.js.map +1 -0
- package/dist/dist-D2SHOZMS.js +8 -0
- package/dist/{dist-7UKXVKH3.js.map → dist-D2SHOZMS.js.map} +1 -1
- package/dist/{dist-7UKXVKH3.js → dist-L76VXYJ5.js} +3 -3
- package/dist/{dist-QFS5YG5L.js.map → dist-L76VXYJ5.js.map} +1 -1
- package/dist/dist-RPM72FHJ.js +5 -0
- package/dist/{dist-WKLJSPJT.js.map → dist-RPM72FHJ.js.map} +1 -1
- package/dist/index.js +30953 -15593
- package/dist/index.js.map +1 -1
- package/dist/learning-pruner-YSZSOOOC.js +7 -0
- package/dist/learning-pruner-YSZSOOOC.js.map +1 -0
- package/dist/{secure-credentials-6UMEU22H.js → secure-credentials-A4QHHOE2.js} +14 -6
- package/dist/secure-credentials-A4QHHOE2.js.map +1 -0
- package/dist/{snapback-dir-T3CRQRY6.js → snapback-dir-6QUSO6Y3.js} +3 -3
- package/dist/{snapback-dir-T3CRQRY6.js.map → snapback-dir-6QUSO6Y3.js.map} +1 -1
- package/dist/storage-H366UNAR.js +6 -0
- package/dist/storage-H366UNAR.js.map +1 -0
- package/package.json +8 -9
- package/dist/chunk-G7QXHNGB.js.map +0 -1
- package/dist/chunk-ISVRGBWT.js.map +0 -1
- package/dist/chunk-SVJ67PPQ.js.map +0 -1
- package/dist/chunk-YOVA65PS.js.map +0 -1
- package/dist/dist-QFS5YG5L.js +0 -5
- package/dist/dist-WKLJSPJT.js +0 -8
- package/dist/secure-credentials-6UMEU22H.js.map +0 -1
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generateId } from './chunk-6MR2TINI.js';
|
|
3
|
+
import { __name, __require } from './chunk-BW7RALUZ.js';
|
|
4
|
+
import * as fs2 from 'fs';
|
|
5
|
+
import * as path3 from 'path';
|
|
6
|
+
import { writeFile } from 'atomically';
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
import * as zlib from 'zlib';
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __name2 = /* @__PURE__ */ __name((target, value) => __defProp(target, "name", {
|
|
12
|
+
value,
|
|
13
|
+
configurable: true
|
|
14
|
+
}), "__name");
|
|
15
|
+
var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
16
|
+
get: /* @__PURE__ */ __name((a, b) => (typeof __require !== "undefined" ? __require : a)[b], "get")
|
|
17
|
+
}) : x)(function(x) {
|
|
18
|
+
if (typeof __require !== "undefined") return __require.apply(this, arguments);
|
|
19
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
20
|
+
});
|
|
21
|
+
var ConfigStore = class {
|
|
22
|
+
static {
|
|
23
|
+
__name(this, "ConfigStore");
|
|
24
|
+
}
|
|
25
|
+
static {
|
|
26
|
+
__name2(this, "ConfigStore");
|
|
27
|
+
}
|
|
28
|
+
config;
|
|
29
|
+
cache = {};
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the root directory for intelligence data
|
|
35
|
+
*/
|
|
36
|
+
get rootDir() {
|
|
37
|
+
return this.config.rootDir;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a path relative to rootDir
|
|
41
|
+
*/
|
|
42
|
+
resolvePath(relativePath) {
|
|
43
|
+
return path3.join(this.config.rootDir, relativePath);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load architecture documentation
|
|
47
|
+
*/
|
|
48
|
+
loadArchitecture() {
|
|
49
|
+
if (this.cache.architecture) {
|
|
50
|
+
return this.cache.architecture;
|
|
51
|
+
}
|
|
52
|
+
const archPath = this.resolvePath("ARCHITECTURE.md");
|
|
53
|
+
if (fs2.existsSync(archPath)) {
|
|
54
|
+
this.cache.architecture = fs2.readFileSync(archPath, "utf-8");
|
|
55
|
+
} else {
|
|
56
|
+
this.cache.architecture = "";
|
|
57
|
+
}
|
|
58
|
+
return this.cache.architecture;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load constraints/rules
|
|
62
|
+
*/
|
|
63
|
+
loadConstraints() {
|
|
64
|
+
if (this.cache.constraints) {
|
|
65
|
+
return this.cache.constraints;
|
|
66
|
+
}
|
|
67
|
+
const constraintsPath = this.resolvePath(this.config.constraintsFile);
|
|
68
|
+
if (fs2.existsSync(constraintsPath)) {
|
|
69
|
+
this.cache.constraints = fs2.readFileSync(constraintsPath, "utf-8");
|
|
70
|
+
} else {
|
|
71
|
+
this.cache.constraints = "";
|
|
72
|
+
}
|
|
73
|
+
return this.cache.constraints;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load codebase patterns
|
|
77
|
+
*/
|
|
78
|
+
loadPatterns() {
|
|
79
|
+
if (this.cache.patterns) {
|
|
80
|
+
return this.cache.patterns;
|
|
81
|
+
}
|
|
82
|
+
const patternsPath = this.resolvePath(path3.join(this.config.patternsDir, "codebase-patterns.md"));
|
|
83
|
+
if (fs2.existsSync(patternsPath)) {
|
|
84
|
+
this.cache.patterns = fs2.readFileSync(patternsPath, "utf-8");
|
|
85
|
+
} else {
|
|
86
|
+
this.cache.patterns = "";
|
|
87
|
+
}
|
|
88
|
+
return this.cache.patterns;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Load any context file by name
|
|
92
|
+
*/
|
|
93
|
+
loadContextFile(filename) {
|
|
94
|
+
const filePath = this.resolvePath(filename);
|
|
95
|
+
if (fs2.existsSync(filePath)) {
|
|
96
|
+
return fs2.readFileSync(filePath, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get static context suitable for prompt caching
|
|
102
|
+
*
|
|
103
|
+
* Content changes rarely - cache for 5+ minutes.
|
|
104
|
+
* Marked with cache_control for Anthropic prompt caching.
|
|
105
|
+
*
|
|
106
|
+
* @see https://docs.anthropic.com/claude/docs/prompt-caching
|
|
107
|
+
*/
|
|
108
|
+
getStaticContext() {
|
|
109
|
+
return {
|
|
110
|
+
architecture: this.loadArchitecture(),
|
|
111
|
+
constraints: this.loadConstraints(),
|
|
112
|
+
patterns: this.loadPatterns(),
|
|
113
|
+
timestamp: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
114
|
+
_cache_control: {
|
|
115
|
+
type: "ephemeral"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear the cache (for testing or after file changes)
|
|
121
|
+
*/
|
|
122
|
+
clearCache() {
|
|
123
|
+
this.cache = {};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if a context file exists
|
|
127
|
+
*/
|
|
128
|
+
contextFileExists(filename) {
|
|
129
|
+
return fs2.existsSync(this.resolvePath(filename));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* List all context files that exist
|
|
133
|
+
*/
|
|
134
|
+
listAvailableContextFiles() {
|
|
135
|
+
return this.config.contextFiles.filter((f) => this.contextFileExists(f));
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
function loadJsonl(filepath) {
|
|
139
|
+
if (!fs2.existsSync(filepath)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return fs2.readFileSync(filepath, "utf-8").split(/\r?\n/).filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error(`[JsonlStore] Error loading ${filepath}:`, e);
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
__name(loadJsonl, "loadJsonl");
|
|
150
|
+
__name2(loadJsonl, "loadJsonl");
|
|
151
|
+
function appendJsonl(filepath, data) {
|
|
152
|
+
const dir = path3.dirname(filepath);
|
|
153
|
+
if (!fs2.existsSync(dir)) {
|
|
154
|
+
fs2.mkdirSync(dir, {
|
|
155
|
+
recursive: true
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
fs2.appendFileSync(filepath, `${JSON.stringify(data)}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
__name(appendJsonl, "appendJsonl");
|
|
162
|
+
__name2(appendJsonl, "appendJsonl");
|
|
163
|
+
async function appendJsonlAsync(filepath, data) {
|
|
164
|
+
const dir = path3.dirname(filepath);
|
|
165
|
+
if (!fs2.existsSync(dir)) {
|
|
166
|
+
fs2.mkdirSync(dir, {
|
|
167
|
+
recursive: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const existing = fs2.existsSync(filepath) ? fs2.readFileSync(filepath, "utf-8") : "";
|
|
172
|
+
await writeFile(filepath, `${existing + JSON.stringify(data)}
|
|
173
|
+
`);
|
|
174
|
+
} catch {
|
|
175
|
+
fs2.appendFileSync(filepath, `${JSON.stringify(data)}
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
__name(appendJsonlAsync, "appendJsonlAsync");
|
|
180
|
+
__name2(appendJsonlAsync, "appendJsonlAsync");
|
|
181
|
+
async function writeJsonl(filepath, records) {
|
|
182
|
+
const dir = path3.dirname(filepath);
|
|
183
|
+
if (!fs2.existsSync(dir)) {
|
|
184
|
+
fs2.mkdirSync(dir, {
|
|
185
|
+
recursive: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const content = `${records.map((r) => JSON.stringify(r)).join("\n")}
|
|
189
|
+
`;
|
|
190
|
+
try {
|
|
191
|
+
await writeFile(filepath, content);
|
|
192
|
+
} catch {
|
|
193
|
+
fs2.writeFileSync(filepath, content);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
__name(writeJsonl, "writeJsonl");
|
|
197
|
+
__name2(writeJsonl, "writeJsonl");
|
|
198
|
+
function findInJsonl(filepath, predicate) {
|
|
199
|
+
const records = loadJsonl(filepath);
|
|
200
|
+
return records.find(predicate);
|
|
201
|
+
}
|
|
202
|
+
__name(findInJsonl, "findInJsonl");
|
|
203
|
+
__name2(findInJsonl, "findInJsonl");
|
|
204
|
+
async function updateInJsonl(filepath, predicate, updater) {
|
|
205
|
+
const records = loadJsonl(filepath);
|
|
206
|
+
let updated = false;
|
|
207
|
+
const newRecords = records.map((record) => {
|
|
208
|
+
if (predicate(record)) {
|
|
209
|
+
updated = true;
|
|
210
|
+
return updater(record);
|
|
211
|
+
}
|
|
212
|
+
return record;
|
|
213
|
+
});
|
|
214
|
+
if (updated) {
|
|
215
|
+
await writeJsonl(filepath, newRecords);
|
|
216
|
+
}
|
|
217
|
+
return updated;
|
|
218
|
+
}
|
|
219
|
+
__name(updateInJsonl, "updateInJsonl");
|
|
220
|
+
__name2(updateInJsonl, "updateInJsonl");
|
|
221
|
+
function countInJsonl(filepath, predicate) {
|
|
222
|
+
const records = loadJsonl(filepath);
|
|
223
|
+
if (!predicate) {
|
|
224
|
+
return records.length;
|
|
225
|
+
}
|
|
226
|
+
return records.filter(predicate).length;
|
|
227
|
+
}
|
|
228
|
+
__name(countInJsonl, "countInJsonl");
|
|
229
|
+
__name2(countInJsonl, "countInJsonl");
|
|
230
|
+
function generateId2(prefix = "ID") {
|
|
231
|
+
return generateId(prefix);
|
|
232
|
+
}
|
|
233
|
+
__name(generateId2, "generateId");
|
|
234
|
+
__name2(generateId2, "generateId");
|
|
235
|
+
var DEFAULT_INTENT_CONFIG = {
|
|
236
|
+
maxResults: 10,
|
|
237
|
+
minScore: 0.3,
|
|
238
|
+
intentWeight: 0.4,
|
|
239
|
+
componentWeight: 0.3,
|
|
240
|
+
fileAffinityWeight: 0.2,
|
|
241
|
+
keywordWeight: 0.1,
|
|
242
|
+
includeRelated: true
|
|
243
|
+
};
|
|
244
|
+
function inferComponentType(filePath) {
|
|
245
|
+
const normalized = filePath.toLowerCase();
|
|
246
|
+
const filename = normalized.split("/").pop() ?? "";
|
|
247
|
+
if (normalized.includes("test") || normalized.endsWith(".test.ts") || normalized.endsWith(".spec.ts")) {
|
|
248
|
+
return "test";
|
|
249
|
+
}
|
|
250
|
+
if (normalized.includes("hooks/") || filename.startsWith("use-")) {
|
|
251
|
+
return "hook";
|
|
252
|
+
}
|
|
253
|
+
if (normalized.includes("components/") || normalized.endsWith(".tsx") || normalized.endsWith(".jsx")) {
|
|
254
|
+
return "component";
|
|
255
|
+
}
|
|
256
|
+
if (normalized.includes("api/") || normalized.includes("routes/") || normalized.includes("endpoints/")) {
|
|
257
|
+
return "api";
|
|
258
|
+
}
|
|
259
|
+
if (normalized.includes("migrations/") || normalized.includes("drizzle/")) {
|
|
260
|
+
return "migration";
|
|
261
|
+
}
|
|
262
|
+
if (normalized.includes("utils/") || normalized.includes("lib/")) {
|
|
263
|
+
return "util";
|
|
264
|
+
}
|
|
265
|
+
if (normalized.includes("types/") || normalized.endsWith(".d.ts")) {
|
|
266
|
+
return "type";
|
|
267
|
+
}
|
|
268
|
+
if (normalized.includes("config") || normalized.endsWith(".config.ts") || normalized.endsWith(".config.js")) {
|
|
269
|
+
return "config";
|
|
270
|
+
}
|
|
271
|
+
if (normalized.includes("tools/") || normalized.includes("mcp/")) {
|
|
272
|
+
return "tool";
|
|
273
|
+
}
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
__name(inferComponentType, "inferComponentType");
|
|
277
|
+
__name2(inferComponentType, "inferComponentType");
|
|
278
|
+
function generateFileAffinityPatterns(filePaths) {
|
|
279
|
+
const patterns = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const filePath of filePaths) {
|
|
281
|
+
const parts = filePath.split("/");
|
|
282
|
+
if (parts.length > 1) {
|
|
283
|
+
patterns.add(`${parts[0]}/**`);
|
|
284
|
+
if (parts.length > 2) {
|
|
285
|
+
patterns.add(`${parts.slice(0, 2).join("/")}/**`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const ext = filePath.split(".").pop();
|
|
289
|
+
if (ext) {
|
|
290
|
+
patterns.add(`**/*.${ext}`);
|
|
291
|
+
}
|
|
292
|
+
const filename = parts[parts.length - 1];
|
|
293
|
+
if (filename?.startsWith("use-")) {
|
|
294
|
+
patterns.add("**/use-*.ts");
|
|
295
|
+
patterns.add("**/use-*.tsx");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return Array.from(patterns);
|
|
299
|
+
}
|
|
300
|
+
__name(generateFileAffinityPatterns, "generateFileAffinityPatterns");
|
|
301
|
+
__name2(generateFileAffinityPatterns, "generateFileAffinityPatterns");
|
|
302
|
+
function matchGlob(pattern, text) {
|
|
303
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<<GLOBSTAR>>>").replace(/\*/g, "[^/]*").replace(/<<<GLOBSTAR>>>/g, ".*").replace(/\?/g, ".");
|
|
304
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
305
|
+
return regex.test(text);
|
|
306
|
+
}
|
|
307
|
+
__name(matchGlob, "matchGlob");
|
|
308
|
+
__name2(matchGlob, "matchGlob");
|
|
309
|
+
var MAGIC_BYTES = Buffer.from("SNAPBACK", "utf8");
|
|
310
|
+
var HEADER_SIZE = 16;
|
|
311
|
+
var CURRENT_VERSION = 1;
|
|
312
|
+
var FLAG_ENCRYPTED = 1;
|
|
313
|
+
function createEmptyState() {
|
|
314
|
+
return {
|
|
315
|
+
schemaVersion: 1,
|
|
316
|
+
lastModified: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
317
|
+
learnings: [],
|
|
318
|
+
violations: [],
|
|
319
|
+
patterns: [],
|
|
320
|
+
sessions: [],
|
|
321
|
+
pendingSync: [],
|
|
322
|
+
syncVersion: 0,
|
|
323
|
+
syncStatus: "pending"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
__name(createEmptyState, "createEmptyState");
|
|
327
|
+
__name2(createEmptyState, "createEmptyState");
|
|
328
|
+
var StateStore = class {
|
|
329
|
+
static {
|
|
330
|
+
__name(this, "StateStore");
|
|
331
|
+
}
|
|
332
|
+
static {
|
|
333
|
+
__name2(this, "StateStore");
|
|
334
|
+
}
|
|
335
|
+
statePath;
|
|
336
|
+
encrypt;
|
|
337
|
+
encryptionKey;
|
|
338
|
+
state;
|
|
339
|
+
dirty = false;
|
|
340
|
+
constructor(options) {
|
|
341
|
+
this.statePath = path3.join(options.snapbackDir, "state.bin");
|
|
342
|
+
this.encrypt = options.encrypt ?? false;
|
|
343
|
+
this.encryptionKey = options.encryptionKey;
|
|
344
|
+
if (this.encrypt && (!this.encryptionKey || this.encryptionKey.length !== 32)) {
|
|
345
|
+
throw new Error("Encryption requires a 32-byte key");
|
|
346
|
+
}
|
|
347
|
+
this.state = createEmptyState();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Load state from disk
|
|
351
|
+
*/
|
|
352
|
+
async load() {
|
|
353
|
+
if (!fs2.existsSync(this.statePath)) {
|
|
354
|
+
this.state = createEmptyState();
|
|
355
|
+
return this.state;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const raw = await fs2.promises.readFile(this.statePath);
|
|
359
|
+
this.state = this.deserialize(raw);
|
|
360
|
+
return this.state;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error("[StateStore] Failed to load state, starting fresh:", error);
|
|
363
|
+
this.state = createEmptyState();
|
|
364
|
+
return this.state;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Save state to disk
|
|
369
|
+
*/
|
|
370
|
+
async save() {
|
|
371
|
+
this.state.lastModified = /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString();
|
|
372
|
+
const serialized = this.serialize(this.state);
|
|
373
|
+
const dir = path3.dirname(this.statePath);
|
|
374
|
+
if (!fs2.existsSync(dir)) {
|
|
375
|
+
await fs2.promises.mkdir(dir, {
|
|
376
|
+
recursive: true
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const tempPath = `${this.statePath}.tmp`;
|
|
380
|
+
await fs2.promises.writeFile(tempPath, serialized);
|
|
381
|
+
await fs2.promises.rename(tempPath, this.statePath);
|
|
382
|
+
this.dirty = false;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get current state (readonly)
|
|
386
|
+
*/
|
|
387
|
+
getState() {
|
|
388
|
+
return this.state;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Check if state has unsaved changes
|
|
392
|
+
*/
|
|
393
|
+
isDirty() {
|
|
394
|
+
return this.dirty;
|
|
395
|
+
}
|
|
396
|
+
// ============================================================================
|
|
397
|
+
// LEARNINGS
|
|
398
|
+
// ============================================================================
|
|
399
|
+
getLearnings(tier) {
|
|
400
|
+
if (!tier) {
|
|
401
|
+
return [
|
|
402
|
+
...this.state.learnings
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
return this.state.learnings.filter((l) => l.tier === tier);
|
|
406
|
+
}
|
|
407
|
+
addLearning(learning) {
|
|
408
|
+
const newLearning = {
|
|
409
|
+
...learning,
|
|
410
|
+
id: crypto.randomUUID(),
|
|
411
|
+
createdAt: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
412
|
+
accessCount: 0,
|
|
413
|
+
appliedCount: 0,
|
|
414
|
+
relevanceScore: 1
|
|
415
|
+
};
|
|
416
|
+
this.state.learnings.push(newLearning);
|
|
417
|
+
this.dirty = true;
|
|
418
|
+
return newLearning;
|
|
419
|
+
}
|
|
420
|
+
updateLearning(id, updates) {
|
|
421
|
+
const index = this.state.learnings.findIndex((l) => l.id === id);
|
|
422
|
+
if (index === -1) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
this.state.learnings[index] = {
|
|
426
|
+
...this.state.learnings[index],
|
|
427
|
+
...updates
|
|
428
|
+
};
|
|
429
|
+
this.dirty = true;
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Archive a learning (Phase 2.6b: set archived flag, preserve data)
|
|
434
|
+
*/
|
|
435
|
+
archiveLearning(learningId) {
|
|
436
|
+
const learning = this.state.learnings.find((l) => l.id === learningId);
|
|
437
|
+
if (!learning) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
learning.archived = true;
|
|
441
|
+
learning.archivedAt = /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString();
|
|
442
|
+
this.dirty = true;
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Delete a learning permanently (Phase 2.6b: remove from state)
|
|
447
|
+
*/
|
|
448
|
+
deleteLearning(learningId) {
|
|
449
|
+
const index = this.state.learnings.findIndex((l) => l.id === learningId);
|
|
450
|
+
if (index === -1) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
this.state.learnings.splice(index, 1);
|
|
454
|
+
this.dirty = true;
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get archived learnings (Phase 2.6b)
|
|
459
|
+
*/
|
|
460
|
+
getArchivedLearnings() {
|
|
461
|
+
return this.state.learnings.filter((l) => l.archived === true);
|
|
462
|
+
}
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// INTENT-AWARE RETRIEVAL (Phase 2.6c)
|
|
465
|
+
// ============================================================================
|
|
466
|
+
/**
|
|
467
|
+
* Retrieve learnings using task-intent graph indexing
|
|
468
|
+
* Based on Cursor RAG + A-MEM research patterns
|
|
469
|
+
*
|
|
470
|
+
* @param context - Task context with intent, files, and keywords
|
|
471
|
+
* @param config - Optional retrieval configuration
|
|
472
|
+
* @returns Scored learnings ranked by relevance
|
|
473
|
+
*/
|
|
474
|
+
retrieveByIntent(context, config = {}) {
|
|
475
|
+
const fullConfig = {
|
|
476
|
+
...DEFAULT_INTENT_CONFIG,
|
|
477
|
+
...config
|
|
478
|
+
};
|
|
479
|
+
const componentType = context.componentType ?? this.inferDominantComponentType(context.files);
|
|
480
|
+
const scored = [];
|
|
481
|
+
for (const learning of this.state.learnings) {
|
|
482
|
+
if (learning.archived) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const score = this.scoreLearning(learning, context, componentType, fullConfig);
|
|
486
|
+
if (score.totalScore >= fullConfig.minScore) {
|
|
487
|
+
scored.push({
|
|
488
|
+
learningId: learning.id,
|
|
489
|
+
score: score.totalScore,
|
|
490
|
+
matchReasons: score.reasons
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
scored.sort((a, b) => b.score - a.score);
|
|
495
|
+
if (fullConfig.includeRelated) {
|
|
496
|
+
this.expandWithRelated(scored, fullConfig.maxResults);
|
|
497
|
+
}
|
|
498
|
+
return scored.slice(0, fullConfig.maxResults);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Enrich a learning with intent metadata
|
|
502
|
+
* Called when creating/updating learnings with file context
|
|
503
|
+
*/
|
|
504
|
+
enrichWithIntentMetadata(learningId, context) {
|
|
505
|
+
const learning = this.state.learnings.find((l) => l.id === learningId);
|
|
506
|
+
if (!learning) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const existingMeta = learning.intentMetadata;
|
|
510
|
+
const componentTypes = context.files.map((f) => inferComponentType(f)).filter(Boolean);
|
|
511
|
+
const fileAffinities = generateFileAffinityPatterns(context.files);
|
|
512
|
+
learning.intentMetadata = {
|
|
513
|
+
intents: existingMeta ? Array.from(/* @__PURE__ */ new Set([
|
|
514
|
+
...existingMeta.intents,
|
|
515
|
+
context.intent
|
|
516
|
+
])) : [
|
|
517
|
+
context.intent
|
|
518
|
+
],
|
|
519
|
+
componentTypes: existingMeta ? Array.from(/* @__PURE__ */ new Set([
|
|
520
|
+
...existingMeta.componentTypes,
|
|
521
|
+
...componentTypes
|
|
522
|
+
])) : componentTypes,
|
|
523
|
+
fileAffinities: existingMeta ? Array.from(/* @__PURE__ */ new Set([
|
|
524
|
+
...existingMeta.fileAffinities,
|
|
525
|
+
...fileAffinities
|
|
526
|
+
])) : fileAffinities,
|
|
527
|
+
relatedLearnings: existingMeta?.relatedLearnings ?? [],
|
|
528
|
+
intentSuccessCount: {
|
|
529
|
+
...existingMeta?.intentSuccessCount ?? {},
|
|
530
|
+
[context.intent]: (existingMeta?.intentSuccessCount?.[context.intent] ?? 0) + 1
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
this.dirty = true;
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Link two learnings together (Zettelkasten-style)
|
|
538
|
+
*/
|
|
539
|
+
linkLearnings(learningId1, learningId2) {
|
|
540
|
+
const learning1 = this.state.learnings.find((l) => l.id === learningId1);
|
|
541
|
+
const learning2 = this.state.learnings.find((l) => l.id === learningId2);
|
|
542
|
+
if (!learning1 || !learning2) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
if (!learning1.intentMetadata) {
|
|
546
|
+
learning1.intentMetadata = {
|
|
547
|
+
intents: [],
|
|
548
|
+
componentTypes: [],
|
|
549
|
+
fileAffinities: [],
|
|
550
|
+
relatedLearnings: [],
|
|
551
|
+
intentSuccessCount: {}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (!learning2.intentMetadata) {
|
|
555
|
+
learning2.intentMetadata = {
|
|
556
|
+
intents: [],
|
|
557
|
+
componentTypes: [],
|
|
558
|
+
fileAffinities: [],
|
|
559
|
+
relatedLearnings: [],
|
|
560
|
+
intentSuccessCount: {}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (!learning1.intentMetadata.relatedLearnings.includes(learningId2)) {
|
|
564
|
+
learning1.intentMetadata.relatedLearnings.push(learningId2);
|
|
565
|
+
}
|
|
566
|
+
if (!learning2.intentMetadata.relatedLearnings.includes(learningId1)) {
|
|
567
|
+
learning2.intentMetadata.relatedLearnings.push(learningId1);
|
|
568
|
+
}
|
|
569
|
+
this.dirty = true;
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Private: Score a learning against task context
|
|
574
|
+
*/
|
|
575
|
+
scoreLearning(learning, context, componentType, config) {
|
|
576
|
+
const meta = learning.intentMetadata;
|
|
577
|
+
const reasons = {
|
|
578
|
+
intentMatch: false,
|
|
579
|
+
componentMatch: false,
|
|
580
|
+
fileAffinityMatch: false,
|
|
581
|
+
keywordMatch: false,
|
|
582
|
+
relationshipMatch: false
|
|
583
|
+
};
|
|
584
|
+
let totalScore = 0;
|
|
585
|
+
if (meta?.intents.includes(context.intent)) {
|
|
586
|
+
reasons.intentMatch = true;
|
|
587
|
+
totalScore += config.intentWeight;
|
|
588
|
+
}
|
|
589
|
+
if (componentType && meta?.componentTypes.includes(componentType)) {
|
|
590
|
+
reasons.componentMatch = true;
|
|
591
|
+
totalScore += config.componentWeight;
|
|
592
|
+
}
|
|
593
|
+
if (meta?.fileAffinities.length) {
|
|
594
|
+
const matchCount = context.files.filter((file) => meta.fileAffinities.some((pattern) => matchGlob(pattern, file))).length;
|
|
595
|
+
if (matchCount > 0) {
|
|
596
|
+
reasons.fileAffinityMatch = true;
|
|
597
|
+
totalScore += config.fileAffinityWeight * (matchCount / context.files.length);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (context.keywords?.length && learning.keywords?.length) {
|
|
601
|
+
const matchCount = context.keywords.filter((kw) => learning.keywords?.includes(kw)).length;
|
|
602
|
+
if (matchCount > 0) {
|
|
603
|
+
reasons.keywordMatch = true;
|
|
604
|
+
totalScore += config.keywordWeight * (matchCount / context.keywords.length);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
totalScore,
|
|
609
|
+
reasons
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Private: Infer dominant component type from file list
|
|
614
|
+
*/
|
|
615
|
+
inferDominantComponentType(files) {
|
|
616
|
+
const types = files.map((f) => inferComponentType(f)).filter((t) => t !== void 0);
|
|
617
|
+
if (types.length === 0) {
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
const counts = /* @__PURE__ */ new Map();
|
|
621
|
+
for (const type of types) {
|
|
622
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
623
|
+
}
|
|
624
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Private: Expand results with related learnings via graph traversal
|
|
628
|
+
*/
|
|
629
|
+
expandWithRelated(scored, _maxResults) {
|
|
630
|
+
const visited = new Set(scored.map((s) => s.learningId));
|
|
631
|
+
const toAdd = [];
|
|
632
|
+
for (const item of scored) {
|
|
633
|
+
const learning = this.state.learnings.find((l) => l.id === item.learningId);
|
|
634
|
+
if (!learning?.intentMetadata?.relatedLearnings.length) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
for (const relatedId of learning.intentMetadata.relatedLearnings) {
|
|
638
|
+
if (visited.has(relatedId)) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
visited.add(relatedId);
|
|
642
|
+
toAdd.push({
|
|
643
|
+
learningId: relatedId,
|
|
644
|
+
score: item.score * 0.7,
|
|
645
|
+
matchReasons: {
|
|
646
|
+
intentMatch: false,
|
|
647
|
+
componentMatch: false,
|
|
648
|
+
fileAffinityMatch: false,
|
|
649
|
+
keywordMatch: false,
|
|
650
|
+
relationshipMatch: true
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
scored.push(...toAdd);
|
|
656
|
+
scored.sort((a, b) => b.score - a.score);
|
|
657
|
+
}
|
|
658
|
+
// ============================================================================
|
|
659
|
+
// VIOLATIONS
|
|
660
|
+
// ============================================================================
|
|
661
|
+
getViolations() {
|
|
662
|
+
return [
|
|
663
|
+
...this.state.violations
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
recordViolation(violation) {
|
|
667
|
+
const existing = this.state.violations.find((v) => v.type === violation.type && v.file === violation.file);
|
|
668
|
+
if (existing) {
|
|
669
|
+
existing.count++;
|
|
670
|
+
existing.lastSeen = /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString();
|
|
671
|
+
this.dirty = true;
|
|
672
|
+
return existing;
|
|
673
|
+
}
|
|
674
|
+
const newViolation = {
|
|
675
|
+
...violation,
|
|
676
|
+
id: crypto.randomUUID(),
|
|
677
|
+
count: 1,
|
|
678
|
+
firstSeen: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
679
|
+
lastSeen: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString()
|
|
680
|
+
};
|
|
681
|
+
this.state.violations.push(newViolation);
|
|
682
|
+
this.dirty = true;
|
|
683
|
+
return newViolation;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Remove a violation by ID
|
|
687
|
+
*
|
|
688
|
+
* @param violationId - Violation ID to remove
|
|
689
|
+
* @returns true if violation was removed, false if not found
|
|
690
|
+
*/
|
|
691
|
+
removeViolation(violationId) {
|
|
692
|
+
const index = this.state.violations.findIndex((v) => v.id === violationId);
|
|
693
|
+
if (index === -1) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
this.state.violations.splice(index, 1);
|
|
697
|
+
this.dirty = true;
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
// ============================================================================
|
|
701
|
+
// PATTERNS
|
|
702
|
+
// ============================================================================
|
|
703
|
+
getPatterns() {
|
|
704
|
+
return [
|
|
705
|
+
...this.state.patterns
|
|
706
|
+
];
|
|
707
|
+
}
|
|
708
|
+
addPattern(pattern) {
|
|
709
|
+
const newPattern = {
|
|
710
|
+
...pattern,
|
|
711
|
+
id: crypto.randomUUID(),
|
|
712
|
+
createdAt: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
713
|
+
usageCount: 0
|
|
714
|
+
};
|
|
715
|
+
this.state.patterns.push(newPattern);
|
|
716
|
+
this.dirty = true;
|
|
717
|
+
return newPattern;
|
|
718
|
+
}
|
|
719
|
+
// ============================================================================
|
|
720
|
+
// SESSIONS
|
|
721
|
+
// ============================================================================
|
|
722
|
+
getSessions() {
|
|
723
|
+
return [
|
|
724
|
+
...this.state.sessions
|
|
725
|
+
];
|
|
726
|
+
}
|
|
727
|
+
saveSession(session) {
|
|
728
|
+
const existing = session.id ? this.state.sessions.find((s) => s.id === session.id) : void 0;
|
|
729
|
+
if (existing) {
|
|
730
|
+
Object.assign(existing, session, {
|
|
731
|
+
lastCheckpoint: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString()
|
|
732
|
+
});
|
|
733
|
+
this.dirty = true;
|
|
734
|
+
return existing;
|
|
735
|
+
}
|
|
736
|
+
const newSession = {
|
|
737
|
+
id: session.id || crypto.randomUUID(),
|
|
738
|
+
taskDescription: session.taskDescription,
|
|
739
|
+
startedAt: session.startedAt,
|
|
740
|
+
snapshotId: session.snapshotId,
|
|
741
|
+
context: session.context,
|
|
742
|
+
appliedLearnings: session.appliedLearnings || [],
|
|
743
|
+
lastCheckpoint: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString()
|
|
744
|
+
};
|
|
745
|
+
this.state.sessions.push(newSession);
|
|
746
|
+
this.dirty = true;
|
|
747
|
+
return newSession;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Update applied learnings for a session (Phase 2.6a)
|
|
751
|
+
* Atomic operation for repetition avoidance
|
|
752
|
+
*/
|
|
753
|
+
updateSessionLearnings(sessionId, learningIds) {
|
|
754
|
+
const session = this.state.sessions.find((s) => s.id === sessionId);
|
|
755
|
+
if (!session) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
const existing = new Set(session.appliedLearnings || []);
|
|
759
|
+
for (const id of learningIds) {
|
|
760
|
+
existing.add(id);
|
|
761
|
+
}
|
|
762
|
+
session.appliedLearnings = Array.from(existing);
|
|
763
|
+
session.lastCheckpoint = /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString();
|
|
764
|
+
this.dirty = true;
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
// ============================================================================
|
|
768
|
+
// PROJECT CONTEXT
|
|
769
|
+
// ============================================================================
|
|
770
|
+
getProjectContext() {
|
|
771
|
+
return this.state.projectContext;
|
|
772
|
+
}
|
|
773
|
+
setProjectContext(context) {
|
|
774
|
+
this.state.projectContext = context;
|
|
775
|
+
this.dirty = true;
|
|
776
|
+
}
|
|
777
|
+
// ============================================================================
|
|
778
|
+
// SYNC QUEUE
|
|
779
|
+
// ============================================================================
|
|
780
|
+
/**
|
|
781
|
+
* Get pending sync items
|
|
782
|
+
*/
|
|
783
|
+
getPendingSync() {
|
|
784
|
+
return [
|
|
785
|
+
...this.state.pendingSync || []
|
|
786
|
+
];
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Queue an item for cloud sync
|
|
790
|
+
*/
|
|
791
|
+
queueForSync(item) {
|
|
792
|
+
const existing = this.state.pendingSync?.find((p) => p.patternKey === item.patternKey);
|
|
793
|
+
if (existing) {
|
|
794
|
+
return existing;
|
|
795
|
+
}
|
|
796
|
+
const newItem = {
|
|
797
|
+
...item,
|
|
798
|
+
id: crypto.randomUUID(),
|
|
799
|
+
queuedAt: /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
retryCount: 0
|
|
801
|
+
};
|
|
802
|
+
if (!this.state.pendingSync) {
|
|
803
|
+
this.state.pendingSync = [];
|
|
804
|
+
}
|
|
805
|
+
this.state.pendingSync.push(newItem);
|
|
806
|
+
this.dirty = true;
|
|
807
|
+
return newItem;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Remove synced items from queue
|
|
811
|
+
*/
|
|
812
|
+
removeSyncedItems(ids) {
|
|
813
|
+
if (!this.state.pendingSync) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const idSet = new Set(ids);
|
|
817
|
+
this.state.pendingSync = this.state.pendingSync.filter((p) => !idSet.has(p.id));
|
|
818
|
+
this.dirty = true;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Increment retry count for failed items
|
|
822
|
+
*/
|
|
823
|
+
incrementRetryCount(ids) {
|
|
824
|
+
if (!this.state.pendingSync) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const idSet = new Set(ids);
|
|
828
|
+
for (const item of this.state.pendingSync) {
|
|
829
|
+
if (idSet.has(item.id)) {
|
|
830
|
+
item.retryCount++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
this.dirty = true;
|
|
834
|
+
}
|
|
835
|
+
// ============================================================================
|
|
836
|
+
// SYNC METADATA (Phase 1: Intelligence New Platform)
|
|
837
|
+
// ============================================================================
|
|
838
|
+
/**
|
|
839
|
+
* Get current sync status
|
|
840
|
+
*/
|
|
841
|
+
getSyncStatus() {
|
|
842
|
+
return this.state.syncStatus;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Set sync status
|
|
846
|
+
*/
|
|
847
|
+
setSyncStatus(status) {
|
|
848
|
+
this.state.syncStatus = status;
|
|
849
|
+
this.dirty = true;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Get current sync version (for CRDT conflict resolution)
|
|
853
|
+
*/
|
|
854
|
+
getSyncVersion() {
|
|
855
|
+
return this.state.syncVersion;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Increment sync version and return new value
|
|
859
|
+
*/
|
|
860
|
+
incrementSyncVersion() {
|
|
861
|
+
this.state.syncVersion++;
|
|
862
|
+
this.dirty = true;
|
|
863
|
+
return this.state.syncVersion;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Mark state as successfully synced with timestamp
|
|
867
|
+
* Clears any previous sync error
|
|
868
|
+
*/
|
|
869
|
+
markSynced() {
|
|
870
|
+
this.state.syncStatus = "synced";
|
|
871
|
+
this.state.lastSynced = /* @__PURE__ */ (/* @__PURE__ */ new Date()).toISOString();
|
|
872
|
+
this.state.lastSyncError = void 0;
|
|
873
|
+
this.dirty = true;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get last synced timestamp
|
|
877
|
+
*/
|
|
878
|
+
getLastSynced() {
|
|
879
|
+
return this.state.lastSynced;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Set sync error status with optional message
|
|
883
|
+
*/
|
|
884
|
+
setSyncError(message) {
|
|
885
|
+
this.state.syncStatus = "error";
|
|
886
|
+
this.state.lastSyncError = message;
|
|
887
|
+
this.dirty = true;
|
|
888
|
+
}
|
|
889
|
+
// ============================================================================
|
|
890
|
+
// SERIALIZATION
|
|
891
|
+
// ============================================================================
|
|
892
|
+
serialize(state) {
|
|
893
|
+
const json = JSON.stringify(state);
|
|
894
|
+
const jsonBuffer = Buffer.from(json, "utf8");
|
|
895
|
+
const compressed = zlib.gzipSync(jsonBuffer, {
|
|
896
|
+
level: 9
|
|
897
|
+
});
|
|
898
|
+
let payload = compressed;
|
|
899
|
+
let flags = 0;
|
|
900
|
+
if (this.encrypt && this.encryptionKey) {
|
|
901
|
+
payload = this.encryptPayload(compressed);
|
|
902
|
+
flags |= FLAG_ENCRYPTED;
|
|
903
|
+
}
|
|
904
|
+
const header = Buffer.alloc(HEADER_SIZE);
|
|
905
|
+
MAGIC_BYTES.copy(header, 0);
|
|
906
|
+
header.writeUInt8(CURRENT_VERSION, 8);
|
|
907
|
+
header.writeUInt8(flags, 9);
|
|
908
|
+
return Buffer.concat([
|
|
909
|
+
header,
|
|
910
|
+
payload
|
|
911
|
+
]);
|
|
912
|
+
}
|
|
913
|
+
deserialize(raw) {
|
|
914
|
+
if (raw.length < HEADER_SIZE) {
|
|
915
|
+
throw new Error("Invalid state file: too small");
|
|
916
|
+
}
|
|
917
|
+
const magic = raw.subarray(0, 8);
|
|
918
|
+
if (!magic.equals(MAGIC_BYTES)) {
|
|
919
|
+
throw new Error("Invalid state file: bad magic bytes");
|
|
920
|
+
}
|
|
921
|
+
const version = raw.readUInt8(8);
|
|
922
|
+
const flags = raw.readUInt8(9);
|
|
923
|
+
if (version > CURRENT_VERSION) {
|
|
924
|
+
throw new Error(`State file version ${version} is newer than supported ${CURRENT_VERSION}`);
|
|
925
|
+
}
|
|
926
|
+
let payload = raw.subarray(HEADER_SIZE);
|
|
927
|
+
if (flags & FLAG_ENCRYPTED) {
|
|
928
|
+
if (!this.encryptionKey) {
|
|
929
|
+
throw new Error("State file is encrypted but no key provided");
|
|
930
|
+
}
|
|
931
|
+
payload = this.decryptPayload(payload);
|
|
932
|
+
}
|
|
933
|
+
const decompressed = zlib.gunzipSync(payload);
|
|
934
|
+
const json = decompressed.toString("utf8");
|
|
935
|
+
return JSON.parse(json);
|
|
936
|
+
}
|
|
937
|
+
encryptPayload(data) {
|
|
938
|
+
if (!this.encryptionKey) {
|
|
939
|
+
throw new Error("No encryption key");
|
|
940
|
+
}
|
|
941
|
+
const iv = crypto.randomBytes(12);
|
|
942
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
943
|
+
const encrypted = Buffer.concat([
|
|
944
|
+
cipher.update(data),
|
|
945
|
+
cipher.final()
|
|
946
|
+
]);
|
|
947
|
+
const authTag = cipher.getAuthTag();
|
|
948
|
+
return Buffer.concat([
|
|
949
|
+
iv,
|
|
950
|
+
authTag,
|
|
951
|
+
encrypted
|
|
952
|
+
]);
|
|
953
|
+
}
|
|
954
|
+
decryptPayload(data) {
|
|
955
|
+
if (!this.encryptionKey) {
|
|
956
|
+
throw new Error("No encryption key");
|
|
957
|
+
}
|
|
958
|
+
if (data.length < 28) {
|
|
959
|
+
throw new Error("Encrypted payload too small");
|
|
960
|
+
}
|
|
961
|
+
const iv = data.subarray(0, 12);
|
|
962
|
+
const authTag = data.subarray(12, 28);
|
|
963
|
+
const encrypted = data.subarray(28);
|
|
964
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
965
|
+
decipher.setAuthTag(authTag);
|
|
966
|
+
return Buffer.concat([
|
|
967
|
+
decipher.update(encrypted),
|
|
968
|
+
decipher.final()
|
|
969
|
+
]);
|
|
970
|
+
}
|
|
971
|
+
// ============================================================================
|
|
972
|
+
// MIGRATION
|
|
973
|
+
// ============================================================================
|
|
974
|
+
/**
|
|
975
|
+
* Migrate from legacy JSONL files to binary state
|
|
976
|
+
*/
|
|
977
|
+
async migrateFromJsonl(learningsDir, patternsDir) {
|
|
978
|
+
const errors = [];
|
|
979
|
+
let migrated = 0;
|
|
980
|
+
const learningFiles = [
|
|
981
|
+
"hot.jsonl",
|
|
982
|
+
"warm.jsonl",
|
|
983
|
+
"cold.jsonl",
|
|
984
|
+
"user-learnings.jsonl",
|
|
985
|
+
"learnings.jsonl"
|
|
986
|
+
];
|
|
987
|
+
for (const file of learningFiles) {
|
|
988
|
+
const filePath = path3.join(learningsDir, file);
|
|
989
|
+
if (fs2.existsSync(filePath)) {
|
|
990
|
+
try {
|
|
991
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
992
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
993
|
+
for (const line of lines) {
|
|
994
|
+
try {
|
|
995
|
+
const legacy = JSON.parse(line);
|
|
996
|
+
const tier = file.startsWith("hot") ? "hot" : file.startsWith("cold") ? "cold" : "warm";
|
|
997
|
+
this.addLearning({
|
|
998
|
+
type: legacy.type || "pattern",
|
|
999
|
+
trigger: legacy.trigger || "",
|
|
1000
|
+
action: legacy.action || "",
|
|
1001
|
+
keywords: legacy.keywords,
|
|
1002
|
+
source: legacy.source,
|
|
1003
|
+
tier: legacy.tier || tier,
|
|
1004
|
+
domain: legacy.domain
|
|
1005
|
+
});
|
|
1006
|
+
migrated++;
|
|
1007
|
+
} catch {
|
|
1008
|
+
errors.push(`Failed to parse learning: ${line.substring(0, 50)}...`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
errors.push(`Failed to read ${file}: ${e}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const violationsPath = path3.join(patternsDir, "violations.jsonl");
|
|
1017
|
+
if (fs2.existsSync(violationsPath)) {
|
|
1018
|
+
try {
|
|
1019
|
+
const content = fs2.readFileSync(violationsPath, "utf8");
|
|
1020
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
1021
|
+
for (const line of lines) {
|
|
1022
|
+
try {
|
|
1023
|
+
const legacy = JSON.parse(line);
|
|
1024
|
+
this.recordViolation({
|
|
1025
|
+
type: legacy.type || "unknown",
|
|
1026
|
+
file: legacy.file || "",
|
|
1027
|
+
description: legacy.description || legacy.whatHappened || "",
|
|
1028
|
+
reason: legacy.reason || legacy.whyItHappened,
|
|
1029
|
+
prevention: legacy.prevention || legacy.howToPrevent || ""
|
|
1030
|
+
});
|
|
1031
|
+
migrated++;
|
|
1032
|
+
} catch {
|
|
1033
|
+
errors.push(`Failed to parse violation: ${line.substring(0, 50)}...`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
errors.push(`Failed to read violations.jsonl: ${e}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
await this.save();
|
|
1041
|
+
return {
|
|
1042
|
+
migrated,
|
|
1043
|
+
errors
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
function deriveEncryptionKey(workspaceId, machineId) {
|
|
1048
|
+
return crypto.pbkdf2Sync(workspaceId, machineId, 1e5, 32, "sha256");
|
|
1049
|
+
}
|
|
1050
|
+
__name(deriveEncryptionKey, "deriveEncryptionKey");
|
|
1051
|
+
__name2(deriveEncryptionKey, "deriveEncryptionKey");
|
|
1052
|
+
function getMachineId() {
|
|
1053
|
+
const os = __require2("os");
|
|
1054
|
+
const parts = [
|
|
1055
|
+
os.hostname(),
|
|
1056
|
+
os.platform(),
|
|
1057
|
+
os.arch(),
|
|
1058
|
+
os.cpus()[0]?.model || "unknown"
|
|
1059
|
+
];
|
|
1060
|
+
return crypto.createHash("sha256").update(parts.join(":")).digest("hex").substring(0, 32);
|
|
1061
|
+
}
|
|
1062
|
+
__name(getMachineId, "getMachineId");
|
|
1063
|
+
__name2(getMachineId, "getMachineId");
|
|
1064
|
+
|
|
1065
|
+
export { ConfigStore, StateStore, appendJsonl, appendJsonlAsync, countInJsonl, deriveEncryptionKey, findInJsonl, generateId2 as generateId, getMachineId, loadJsonl, updateInJsonl, writeJsonl };
|
|
1066
|
+
//# sourceMappingURL=chunk-QLCHTUT5.js.map
|
|
1067
|
+
//# sourceMappingURL=chunk-QLCHTUT5.js.map
|