vektor-slipstream 1.2.2 → 1.3.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/axon.js +389 -0
- package/cerebellum.js +438 -0
- package/cortex.js +221 -0
- package/index.js +480 -0
- package/package.json +114 -90
- package/token.js +322 -0
- package/vektor-slipstream.dxt +0 -0
package/cerebellum.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cloak_cerebellum.js
|
|
3
|
+
* Pre-write enforcer — checks every write against known error patterns in
|
|
4
|
+
* Vektor's causal graph. Auto-creates [RESOLVED_BY] edges when a fix matches
|
|
5
|
+
* an open error node. Zero extra tool calls from Claude.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities:
|
|
8
|
+
* 1. CHECK — before a write, recall causal error patterns and warn if matched
|
|
9
|
+
* 2. RESOLVE — if new write content matches an error pattern, auto-write
|
|
10
|
+
* [RESOLVED_BY] causal edge to close the loop
|
|
11
|
+
*
|
|
12
|
+
* Architecture: CLOAK layer → Vektor causal graph (MAGMA §3.4)
|
|
13
|
+
* Research: MAGMA arXiv:2601.03236, OpenWolf cerebrum Do-Not-Repeat pattern
|
|
14
|
+
*
|
|
15
|
+
* Key design decision: cerebellum NEVER blocks a write, it only warns.
|
|
16
|
+
* Claude Code must remain in control — cerebellum is advisory, not a gatekeeper.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Similarity threshold for pattern matching
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const SIMILARITY_THRESHOLD = 0.72;
|
|
27
|
+
|
|
28
|
+
// Max error patterns to check per write
|
|
29
|
+
const MAX_PATTERNS_TO_CHECK = 8;
|
|
30
|
+
|
|
31
|
+
// Minimum content length to bother checking — skip tiny edits (console.log,
|
|
32
|
+
// closing braces, single-line comments). Not worth a Vektor round-trip.
|
|
33
|
+
const MIN_CHECK_LENGTH = 50;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// LRU warning cache — prevents slamming Vektor on rapid multi-file writes
|
|
37
|
+
// (MultiEdit, code generation loops, etc.)
|
|
38
|
+
//
|
|
39
|
+
// Key: SHA-1 of content.slice(0,300) — cheap, collision-safe for this use
|
|
40
|
+
// Value: { warnings, cachedAt }
|
|
41
|
+
// TTL: 60 seconds — patterns don't change that fast within a session
|
|
42
|
+
// Max: 100 entries — bounded memory, evicts oldest on overflow
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const CACHE_TTL_MS = 60_000;
|
|
45
|
+
const CACHE_MAX = 100;
|
|
46
|
+
const _warningCache = new Map(); // key → { warnings, cachedAt }
|
|
47
|
+
|
|
48
|
+
function _cacheKey(content) {
|
|
49
|
+
return crypto.createHash('sha1').update(content.slice(0, 300)).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _cacheGet(key) {
|
|
53
|
+
const entry = _warningCache.get(key);
|
|
54
|
+
if (!entry) return null;
|
|
55
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
|
56
|
+
_warningCache.delete(key);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return entry.warnings;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _cacheSet(key, warnings) {
|
|
63
|
+
// Evict oldest entry if at capacity
|
|
64
|
+
if (_warningCache.size >= CACHE_MAX) {
|
|
65
|
+
_warningCache.delete(_warningCache.keys().next().value);
|
|
66
|
+
}
|
|
67
|
+
_warningCache.set(key, { warnings, cachedAt: Date.now() });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Pattern storage key prefix — namespaces cerebellum nodes in Vektor
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
const ERROR_PREFIX = '[CLOAK_CEREBELLUM_ERROR]';
|
|
74
|
+
const RESOLVED_PREFIX = '[CLOAK_CEREBELLUM_RESOLVED]';
|
|
75
|
+
const DNR_PREFIX = '[CLOAK_CEREBELLUM_DNR]'; // Do-Not-Repeat
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Core: check a write against known error patterns
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* checkWrite({ content, filePath, memory })
|
|
83
|
+
* Called from the PreToolUse hook before every write.
|
|
84
|
+
* Returns warnings if the write matches known error patterns.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} content - The content being written
|
|
87
|
+
* @param {string} filePath - The file being written to
|
|
88
|
+
* @param {object} memory - Vektor memory instance
|
|
89
|
+
* @returns {object} - { warnings: [], shouldProceed: true }
|
|
90
|
+
*/
|
|
91
|
+
async function checkWrite({ content, filePath, memory } = {}) {
|
|
92
|
+
if (!memory) throw new Error('cloak_cerebellum: memory instance is required');
|
|
93
|
+
if (!content) return { warnings: [], shouldProceed: true, cached: false };
|
|
94
|
+
|
|
95
|
+
// Bypass: tiny writes aren't worth a Vektor round-trip
|
|
96
|
+
// (closing braces, console.logs, single-line cosmetic edits)
|
|
97
|
+
if (content.length < MIN_CHECK_LENGTH) {
|
|
98
|
+
return { warnings: [], shouldProceed: true, bypassed: 'too_small' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Cache hit: same content checked recently — return instantly, no Vektor call
|
|
102
|
+
const key = _cacheKey(content);
|
|
103
|
+
const cached = _cacheGet(key);
|
|
104
|
+
if (cached !== null) {
|
|
105
|
+
return {
|
|
106
|
+
warnings : cached,
|
|
107
|
+
shouldProceed: true,
|
|
108
|
+
hasBlockers : cached.some(w => w.severity === 'block'),
|
|
109
|
+
cached : true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const warnings = [];
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Query causal error graph for patterns similar to this write
|
|
117
|
+
const errorPatterns = await memory.recall(
|
|
118
|
+
`${ERROR_PREFIX} ${content.slice(0, 300)}`,
|
|
119
|
+
{ limit: MAX_PATTERNS_TO_CHECK }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!errorPatterns || errorPatterns.length === 0) {
|
|
123
|
+
return { warnings: [], shouldProceed: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const pattern of errorPatterns) {
|
|
127
|
+
if (pattern.score < SIMILARITY_THRESHOLD) continue;
|
|
128
|
+
|
|
129
|
+
// Parse the stored pattern
|
|
130
|
+
const parsed = parsePatternNode(pattern.content);
|
|
131
|
+
if (!parsed) continue;
|
|
132
|
+
|
|
133
|
+
// Skip if this pattern is already resolved
|
|
134
|
+
if (parsed.resolvedBy) continue;
|
|
135
|
+
|
|
136
|
+
warnings.push({
|
|
137
|
+
patternId : pattern.id,
|
|
138
|
+
description : parsed.description,
|
|
139
|
+
originalFile: parsed.file,
|
|
140
|
+
severity : parsed.severity || 'warn',
|
|
141
|
+
hint : parsed.fix || 'See error pattern for details',
|
|
142
|
+
score : Math.round(pattern.score * 100) / 100,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Also check Do-Not-Repeat rules (always enforced regardless of similarity)
|
|
147
|
+
const dnrRules = await memory.recall(
|
|
148
|
+
`${DNR_PREFIX}`,
|
|
149
|
+
{ limit: 5 }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (dnrRules) {
|
|
153
|
+
for (const rule of dnrRules) {
|
|
154
|
+
const parsed = parseDNRNode(rule.content);
|
|
155
|
+
if (!parsed) continue;
|
|
156
|
+
|
|
157
|
+
// Check if the pattern keyword appears in the write content
|
|
158
|
+
if (content.toLowerCase().includes(parsed.keyword.toLowerCase())) {
|
|
159
|
+
warnings.push({
|
|
160
|
+
patternId : rule.id,
|
|
161
|
+
description : `Do-Not-Repeat: ${parsed.rule}`,
|
|
162
|
+
severity : 'block',
|
|
163
|
+
hint : parsed.alternative || 'Avoid this pattern',
|
|
164
|
+
score : 1.0,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} catch (err) {
|
|
171
|
+
// Non-fatal — Vektor query failure should not block writes
|
|
172
|
+
console.warn('[cloak_cerebellum] Pattern check failed:', err.message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Cache the result for 60s — rapid multi-file writes skip Vektor entirely
|
|
176
|
+
_cacheSet(key, warnings);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
warnings,
|
|
180
|
+
shouldProceed: true,
|
|
181
|
+
hasBlockers : warnings.some(w => w.severity === 'block'),
|
|
182
|
+
cached : false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Core: record a new error pattern into the causal graph
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* recordError({ description, filePath, errorType, fix, severity, memory })
|
|
192
|
+
* Store a new error pattern so future writes can be warned.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} description - Human-readable description of the error
|
|
195
|
+
* @param {string} filePath - File where error occurred
|
|
196
|
+
* @param {string} errorType - e.g. 'null_reference', 'type_mismatch'
|
|
197
|
+
* @param {string} fix - What fixed it (optional)
|
|
198
|
+
* @param {'warn'|'block'} severity
|
|
199
|
+
* @param {object} memory - Vektor memory instance
|
|
200
|
+
* @returns {object} - { id, written }
|
|
201
|
+
*/
|
|
202
|
+
async function recordError({ description, filePath, errorType, fix, severity = 'warn', memory } = {}) {
|
|
203
|
+
if (!memory) throw new Error('cloak_cerebellum: memory instance is required');
|
|
204
|
+
if (!description) throw new Error('cloak_cerebellum: description is required');
|
|
205
|
+
|
|
206
|
+
const patternStr = [
|
|
207
|
+
ERROR_PREFIX,
|
|
208
|
+
`error_type:${errorType || 'unknown'}`,
|
|
209
|
+
`file:${filePath || 'unknown'}`,
|
|
210
|
+
`description:${description}`,
|
|
211
|
+
`severity:${severity}`,
|
|
212
|
+
fix ? `fix:${fix}` : null,
|
|
213
|
+
`recorded_at:${new Date().toISOString()}`,
|
|
214
|
+
`resolved:false`,
|
|
215
|
+
].filter(Boolean).join(' | ');
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await memory.remember(patternStr);
|
|
219
|
+
return { written: true, pattern: patternStr };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[cloak_cerebellum] Failed to record error:', err.message);
|
|
222
|
+
return { written: false, error: err.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Core: auto-resolve — detect when a write fixes an open error
|
|
228
|
+
// Called from PostToolUse after a successful write.
|
|
229
|
+
// This is the key improvement over OpenWolf — resolution is automatic,
|
|
230
|
+
// not dependent on Claude remembering to call a separate tool.
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* autoResolve({ content, filePath, memory })
|
|
235
|
+
* After a successful write, check if it resolves any open error patterns.
|
|
236
|
+
* If yes, write a [RESOLVED_BY] causal edge to close the loop.
|
|
237
|
+
* This feeds the decay weighting in MAGMA's causal graph.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} content - Content that was written
|
|
240
|
+
* @param {string} filePath - File that was written
|
|
241
|
+
* @param {object} memory - Vektor memory instance
|
|
242
|
+
* @returns {object} - { resolved: [], written: number }
|
|
243
|
+
*/
|
|
244
|
+
async function autoResolve({ content, filePath, memory } = {}) {
|
|
245
|
+
if (!memory || !filePath) return { resolved: [], written: 0 };
|
|
246
|
+
|
|
247
|
+
const resolved = [];
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// Query by FILE PATH not content — avoids the semantic gap where
|
|
251
|
+
// "Null reference on user.id" and "if (user && user.id)" have
|
|
252
|
+
// completely different embeddings despite one fixing the other.
|
|
253
|
+
// Pull all open errors on this file, then let REM cycle verify.
|
|
254
|
+
const candidates = await memory.recall(
|
|
255
|
+
`${ERROR_PREFIX} file:${filePath}`,
|
|
256
|
+
{ limit: MAX_PATTERNS_TO_CHECK }
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!candidates || candidates.length === 0) return { resolved: [], written: 0 };
|
|
260
|
+
|
|
261
|
+
for (const candidate of candidates) {
|
|
262
|
+
// File path match is already a strong signal — use lower threshold
|
|
263
|
+
// than content matching. 0.5 avoids completely unrelated file path
|
|
264
|
+
// substring matches while catching real file-based errors.
|
|
265
|
+
if (candidate.score < 0.5) continue;
|
|
266
|
+
|
|
267
|
+
const parsed = parsePatternNode(candidate.content);
|
|
268
|
+
if (!parsed || parsed.resolvedBy) continue; // already resolved
|
|
269
|
+
|
|
270
|
+
// Only auto-resolve if the stored error references this exact file
|
|
271
|
+
if (parsed.file && parsed.file !== filePath &&
|
|
272
|
+
parsed.file !== require('path').basename(filePath)) continue;
|
|
273
|
+
|
|
274
|
+
// Write resolution node — this is the [RESOLVED_BY] causal edge
|
|
275
|
+
const resolutionStr = [
|
|
276
|
+
RESOLVED_PREFIX,
|
|
277
|
+
`resolves_pattern:${candidate.id}`,
|
|
278
|
+
`original_error:${parsed.description}`,
|
|
279
|
+
`resolved_by_file:${filePath || 'unknown'}`,
|
|
280
|
+
`resolved_at:${new Date().toISOString()}`,
|
|
281
|
+
`[RESOLVED_BY]`, // explicit causal edge marker for HippoRAG decay weighting
|
|
282
|
+
].join(' | ');
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await memory.remember(resolutionStr);
|
|
286
|
+
resolved.push({
|
|
287
|
+
patternId : candidate.id,
|
|
288
|
+
description: parsed.description,
|
|
289
|
+
resolution : resolutionStr,
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.warn('[cloak_cerebellum] Failed to write resolution:', err.message);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.warn('[cloak_cerebellum] Auto-resolve query failed:', err.message);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { resolved, written: resolved.length };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Do-Not-Repeat rules — project-level conventions
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* addDNRRule({ keyword, rule, alternative, memory })
|
|
309
|
+
* Store a Do-Not-Repeat rule. Checked on every write regardless of similarity.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} keyword - Trigger keyword/pattern to watch for
|
|
312
|
+
* @param {string} rule - Human description of what not to do
|
|
313
|
+
* @param {string} alternative - What to do instead
|
|
314
|
+
* @param {object} memory
|
|
315
|
+
*/
|
|
316
|
+
async function addDNRRule({ keyword, rule, alternative, memory } = {}) {
|
|
317
|
+
if (!memory) throw new Error('cloak_cerebellum: memory instance is required');
|
|
318
|
+
if (!keyword) throw new Error('cloak_cerebellum: keyword is required');
|
|
319
|
+
if (!rule) throw new Error('cloak_cerebellum: rule is required');
|
|
320
|
+
|
|
321
|
+
const dnrStr = [
|
|
322
|
+
DNR_PREFIX,
|
|
323
|
+
`keyword:${keyword}`,
|
|
324
|
+
`rule:${rule}`,
|
|
325
|
+
alternative ? `alternative:${alternative}` : null,
|
|
326
|
+
`added_at:${new Date().toISOString()}`,
|
|
327
|
+
].filter(Boolean).join(' | ');
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await memory.remember(dnrStr);
|
|
331
|
+
return { written: true };
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error('[cloak_cerebellum] Failed to add DNR rule:', err.message);
|
|
334
|
+
return { written: false, error: err.message };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Claude Code hook integration
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* handleHookPayload({ payload, memory })
|
|
344
|
+
* Routes Claude Code PreToolUse/PostToolUse hooks to checkWrite / autoResolve.
|
|
345
|
+
*/
|
|
346
|
+
async function handleHookPayload({ payload, memory } = {}) {
|
|
347
|
+
if (!memory || !payload) return null;
|
|
348
|
+
|
|
349
|
+
const hookName = payload?.hook_event_name || payload?.event;
|
|
350
|
+
const tool = payload?.tool_name;
|
|
351
|
+
const input = payload?.tool_input;
|
|
352
|
+
const output = payload?.tool_response;
|
|
353
|
+
|
|
354
|
+
const isWrite = ['Write', 'Edit', 'MultiEdit', 'Bash'].includes(tool);
|
|
355
|
+
|
|
356
|
+
switch (hookName) {
|
|
357
|
+
case 'PreToolUse':
|
|
358
|
+
case 'pre_tool_use': {
|
|
359
|
+
if (!isWrite) return null;
|
|
360
|
+
|
|
361
|
+
// Bug fix: MultiEdit sends input.edits = [{ old_string, new_string }]
|
|
362
|
+
// not a flat input.new_string. Concatenate all new_string blocks so
|
|
363
|
+
// cerebellum actually sees the content being written in refactors.
|
|
364
|
+
let content = input?.new_string || input?.content || input?.command || '';
|
|
365
|
+
if (!content && input?.edits && Array.isArray(input.edits)) {
|
|
366
|
+
content = input.edits.map(e => e.new_string || '').filter(Boolean).join('\n');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const filePath = input?.file_path || input?.path || '';
|
|
370
|
+
return checkWrite({ content, filePath, memory });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case 'PostToolUse':
|
|
374
|
+
case 'post_tool_use': {
|
|
375
|
+
if (!isWrite) return null;
|
|
376
|
+
const success = !output?.error;
|
|
377
|
+
if (!success) return null;
|
|
378
|
+
|
|
379
|
+
// Same MultiEdit fix on the post-write resolve path
|
|
380
|
+
let content = input?.new_string || input?.content || '';
|
|
381
|
+
if (!content && input?.edits && Array.isArray(input.edits)) {
|
|
382
|
+
content = input.edits.map(e => e.new_string || '').filter(Boolean).join('\n');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const filePath = input?.file_path || input?.path || '';
|
|
386
|
+
return autoResolve({ content, filePath, memory });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
default:
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Parsers — extract structured data from Vektor node strings
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
function parsePatternNode(str) {
|
|
398
|
+
if (!str || !str.includes(ERROR_PREFIX)) return null;
|
|
399
|
+
const parts = str.split(' | ');
|
|
400
|
+
const get = key => {
|
|
401
|
+
const part = parts.find(p => p.startsWith(`${key}:`));
|
|
402
|
+
return part ? part.slice(key.length + 1) : null;
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
errorType : get('error_type'),
|
|
406
|
+
file : get('file'),
|
|
407
|
+
description: get('description'),
|
|
408
|
+
severity : get('severity'),
|
|
409
|
+
fix : get('fix'),
|
|
410
|
+
resolvedBy: str.includes(RESOLVED_PREFIX) || get('resolved') === 'true' ? true : null,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function parseDNRNode(str) {
|
|
415
|
+
if (!str || !str.includes(DNR_PREFIX)) return null;
|
|
416
|
+
const parts = str.split(' | ');
|
|
417
|
+
const get = key => {
|
|
418
|
+
const part = parts.find(p => p.startsWith(`${key}:`));
|
|
419
|
+
return part ? part.slice(key.length + 1) : null;
|
|
420
|
+
};
|
|
421
|
+
return {
|
|
422
|
+
keyword : get('keyword'),
|
|
423
|
+
rule : get('rule'),
|
|
424
|
+
alternative: get('alternative'),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
checkWrite,
|
|
430
|
+
recordError,
|
|
431
|
+
autoResolve,
|
|
432
|
+
addDNRRule,
|
|
433
|
+
handleHookPayload,
|
|
434
|
+
SIMILARITY_THRESHOLD,
|
|
435
|
+
ERROR_PREFIX,
|
|
436
|
+
RESOLVED_PREFIX,
|
|
437
|
+
DNR_PREFIX,
|
|
438
|
+
};
|
package/cortex.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cloak_cortex.js
|
|
3
|
+
* File anatomy scanner — builds a token-aware project index in Vektor's entity graph.
|
|
4
|
+
* Called once on init, then on significant file changes (not per-session).
|
|
5
|
+
*
|
|
6
|
+
* Writes to Vektor as entity nodes:
|
|
7
|
+
* "file:src/server.ts | purpose: Express HTTP server | tokens: ~520 | last_scanned: <iso>"
|
|
8
|
+
*
|
|
9
|
+
* Architecture: CLOAK layer → Vektor entity graph (MAGMA §3.2)
|
|
10
|
+
* Research: MAGMA arXiv:2601.03236, OpenWolf anatomy pattern
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Token estimation (OpenWolf ratios — accurate to ±15%)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const TOKEN_RATIOS = {
|
|
22
|
+
code : 3.5, // .js .ts .py .go .rs etc.
|
|
23
|
+
prose: 4.0, // .md .txt .rst
|
|
24
|
+
mixed: 3.75, // .json .yaml .toml .html
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CODE_EXTS = new Set(['.js','.ts','.jsx','.tsx','.py','.go','.rs','.rb','.java','.c','.cpp','.cs','.php','.swift','.kt']);
|
|
28
|
+
const PROSE_EXTS = new Set(['.md','.txt','.rst','.mdx']);
|
|
29
|
+
const SKIP_DIRS = new Set(['node_modules','.git','dist','build','.next','__pycache__','.cache','coverage','.wolf']);
|
|
30
|
+
const SKIP_FILES = new Set(['.DS_Store','package-lock.json','yarn.lock','pnpm-lock.yaml']);
|
|
31
|
+
|
|
32
|
+
function estimateTokens(filePath, bytes) {
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (CODE_EXTS.has(ext)) return Math.round(bytes / TOKEN_RATIOS.code);
|
|
35
|
+
if (PROSE_EXTS.has(ext)) return Math.round(bytes / TOKEN_RATIOS.prose);
|
|
36
|
+
return Math.round(bytes / TOKEN_RATIOS.mixed);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// File scanner
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function scanDirectory(dirPath, results = [], rootPath = dirPath, visited = new Set()) {
|
|
43
|
+
// Only resolve realpath for symlinks — not for every directory.
|
|
44
|
+
// Running fs.realpathSync on all directories is ~4x slower on large codebases
|
|
45
|
+
// (especially Windows NTFS) and the visited Set grows unnecessarily large.
|
|
46
|
+
// Normal directories cannot create cycles; only symlinks can.
|
|
47
|
+
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (SKIP_FILES.has(entry.name)) continue;
|
|
57
|
+
|
|
58
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
59
|
+
const relPath = path.relative(rootPath, fullPath);
|
|
60
|
+
|
|
61
|
+
if (entry.isSymbolicLink()) {
|
|
62
|
+
// Symlinks only: resolve real path and check for cycles
|
|
63
|
+
let realPath;
|
|
64
|
+
try { realPath = fs.realpathSync(fullPath); } catch { continue; }
|
|
65
|
+
|
|
66
|
+
if (visited.has(realPath)) continue; // cycle detected — skip
|
|
67
|
+
|
|
68
|
+
let stat;
|
|
69
|
+
try { stat = fs.statSync(fullPath); } catch { continue; }
|
|
70
|
+
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
73
|
+
visited.add(realPath); // mark before recursing
|
|
74
|
+
scanDirectory(fullPath, results, rootPath, visited);
|
|
75
|
+
}
|
|
76
|
+
} else if (stat.isFile()) {
|
|
77
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
78
|
+
const tokens = estimateTokens(fullPath, stat.size);
|
|
79
|
+
results.push({ relPath, ext, tokens, mtime: stat.mtimeMs, bytes: stat.size });
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
// Normal directory — no realpathSync needed, no cycle possible
|
|
86
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
87
|
+
scanDirectory(fullPath, results, rootPath, visited);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!entry.isFile()) continue;
|
|
93
|
+
|
|
94
|
+
let stat;
|
|
95
|
+
try { stat = fs.statSync(fullPath); } catch { continue; }
|
|
96
|
+
|
|
97
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
98
|
+
const tokens = estimateTokens(fullPath, stat.size);
|
|
99
|
+
const mtime = stat.mtimeMs;
|
|
100
|
+
|
|
101
|
+
results.push({ relPath, ext, tokens, mtime, bytes: stat.size });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// isSymlinkDir removed — logic inlined above for clarity
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Build Vektor memory strings for entity graph
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
function buildEntityString(file, projectName) {
|
|
113
|
+
return [
|
|
114
|
+
`[CLOAK_CORTEX] project:${projectName}`,
|
|
115
|
+
`file:${file.relPath}`,
|
|
116
|
+
`tokens:~${file.tokens}`,
|
|
117
|
+
`size:${file.bytes}b`,
|
|
118
|
+
`last_modified:${new Date(file.mtime).toISOString()}`,
|
|
119
|
+
].join(' | ');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Load/save scan cache (.wolf/cortex-cache.json) to avoid rescanning unchanged files
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
function loadCache(projectPath) {
|
|
126
|
+
const cachePath = path.join(projectPath, '.wolf', 'cortex-cache.json');
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
129
|
+
} catch {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function saveCache(projectPath, cache) {
|
|
135
|
+
const wolfDir = path.join(projectPath, '.wolf');
|
|
136
|
+
if (!fs.existsSync(wolfDir)) fs.mkdirSync(wolfDir, { recursive: true });
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
path.join(wolfDir, 'cortex-cache.json'),
|
|
139
|
+
JSON.stringify(cache, null, 2)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Main export
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* runCortex({ projectPath, memory, force })
|
|
149
|
+
*
|
|
150
|
+
* @param {string} projectPath - Absolute path to project root
|
|
151
|
+
* @param {object} memory - Vektor memory instance (vektor-slipstream)
|
|
152
|
+
* @param {boolean} force - Re-scan all files even if unchanged
|
|
153
|
+
* @returns {object} - { scanned, skipped, written, totalTokens, anatomy }
|
|
154
|
+
*/
|
|
155
|
+
async function runCortex({ projectPath, memory, force = false } = {}) {
|
|
156
|
+
if (!projectPath) throw new Error('cloak_cortex: projectPath is required');
|
|
157
|
+
if (!memory) throw new Error('cloak_cortex: memory instance is required');
|
|
158
|
+
|
|
159
|
+
const projectName = path.basename(projectPath);
|
|
160
|
+
const cache = force ? {} : loadCache(projectPath);
|
|
161
|
+
const files = scanDirectory(projectPath);
|
|
162
|
+
|
|
163
|
+
const stats = { scanned: 0, skipped: 0, written: 0, totalTokens: 0 };
|
|
164
|
+
const anatomy = [];
|
|
165
|
+
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
stats.totalTokens += file.tokens;
|
|
168
|
+
anatomy.push({ path: file.relPath, tokens: file.tokens });
|
|
169
|
+
|
|
170
|
+
// Skip if file hasn't changed since last scan
|
|
171
|
+
const cacheKey = file.relPath;
|
|
172
|
+
if (!force && cache[cacheKey] && cache[cacheKey].mtime === file.mtime) {
|
|
173
|
+
stats.skipped++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stats.scanned++;
|
|
178
|
+
|
|
179
|
+
const entityStr = buildEntityString(file, projectName);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await memory.remember(entityStr);
|
|
183
|
+
cache[cacheKey] = { mtime: file.mtime, tokens: file.tokens };
|
|
184
|
+
stats.written++;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[cloak_cortex] Failed to write entity for ${file.relPath}:`, err.message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Write project-level summary node
|
|
191
|
+
const summaryStr = [
|
|
192
|
+
`[CLOAK_CORTEX_SUMMARY] project:${projectName}`,
|
|
193
|
+
`total_files:${files.length}`,
|
|
194
|
+
`total_tokens:~${stats.totalTokens}`,
|
|
195
|
+
`scanned_at:${new Date().toISOString()}`,
|
|
196
|
+
].join(' | ');
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await memory.remember(summaryStr);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error('[cloak_cortex] Failed to write summary node:', err.message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
saveCache(projectPath, cache);
|
|
205
|
+
|
|
206
|
+
return { ...stats, anatomy };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* getAnatomy({ projectPath })
|
|
211
|
+
* Returns the cached anatomy without hitting Vektor — for fast pre-read hints.
|
|
212
|
+
*/
|
|
213
|
+
function getAnatomy(projectPath) {
|
|
214
|
+
const cache = loadCache(projectPath);
|
|
215
|
+
return Object.entries(cache).map(([relPath, data]) => ({
|
|
216
|
+
path: relPath,
|
|
217
|
+
tokens: data.tokens,
|
|
218
|
+
})).sort((a, b) => b.tokens - a.tokens);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { runCortex, getAnatomy, scanDirectory, estimateTokens };
|