@nxuss/lemma 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/cjs/mcp/tool-profiles.js +1 -0
- package/dist/cjs/mcp/tool-profiles.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +71 -7
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +100 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +166 -26
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/SymbolSurgicalContext.d.ts +9 -0
- package/dist/cjs/utils/SymbolSurgicalContext.d.ts.map +1 -1
- package/dist/cjs/utils/SymbolSurgicalContext.js +19 -0
- package/dist/cjs/utils/SymbolSurgicalContext.js.map +1 -1
- package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/esm/mcp/tool-profiles.js +1 -0
- package/dist/esm/mcp/tool-profiles.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +71 -7
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +100 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +162 -26
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/SymbolSurgicalContext.d.ts +9 -0
- package/dist/esm/utils/SymbolSurgicalContext.d.ts.map +1 -1
- package/dist/esm/utils/SymbolSurgicalContext.js +18 -0
- package/dist/esm/utils/SymbolSurgicalContext.js.map +1 -1
- package/package.json +1 -1
|
@@ -53,6 +53,16 @@ export interface BrainEntry {
|
|
|
53
53
|
* still applies, so every entry written before this existed keeps working unchanged.
|
|
54
54
|
*/
|
|
55
55
|
symbolHashes?: Record<string, string>;
|
|
56
|
+
/**
|
|
57
|
+
* Fase D: sha256 of the same symbols as symbolHashes, but of their normalized
|
|
58
|
+
* (comment/whitespace-stripped) source — see normalizeForFreshness. Always computed
|
|
59
|
+
* alongside symbolHashes when a symbol is stored, regardless of whether semanticDiff is
|
|
60
|
+
* ever used. Lets checkEntryFreshness tell "only a comment/formatting changed" apart from
|
|
61
|
+
* a real change, but only when the caller opts in via semanticDiff — absent (entries
|
|
62
|
+
* written before this existed) means that classification isn't possible and a raw-hash
|
|
63
|
+
* mismatch falls back to stale, same as pre-Fase-D behavior.
|
|
64
|
+
*/
|
|
65
|
+
symbolNormalizedHashes?: Record<string, string>;
|
|
56
66
|
/**
|
|
57
67
|
* Sub-claims within `response`, each with its own evidence. Absent means the whole
|
|
58
68
|
* entry is one claim, judged by fileHashes/symbolHashes above (pre-existing behavior).
|
|
@@ -67,12 +77,23 @@ export interface BrainEntry {
|
|
|
67
77
|
* "reuse turned out to be wrong." Absent/0 means never downvoted.
|
|
68
78
|
*/
|
|
69
79
|
demerits?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Ids of other entries/claims this memory's conclusion was built on top of (the agent
|
|
82
|
+
* read memory A, then stored B as a conclusion derived from it). Absent means B's
|
|
83
|
+
* freshness is judged only by its own fileHashes/symbolHashes, same as before this
|
|
84
|
+
* existed. When present, checkEntryFreshness walks it transitively (bounded by
|
|
85
|
+
* MAX_DERIVED_DEPTH): if A goes stale, B is stale too even though B's own evidence
|
|
86
|
+
* never changed — because B's conclusion was never independently verified against
|
|
87
|
+
* current state, only inherited.
|
|
88
|
+
*/
|
|
89
|
+
derivedFrom?: string[];
|
|
70
90
|
}
|
|
71
91
|
export interface Claim {
|
|
72
92
|
id: string;
|
|
73
93
|
text: string;
|
|
74
94
|
fileHashes?: Record<string, string>;
|
|
75
95
|
symbolHashes?: Record<string, string>;
|
|
96
|
+
symbolNormalizedHashes?: Record<string, string>;
|
|
76
97
|
}
|
|
77
98
|
export interface ClaimInput {
|
|
78
99
|
text: string;
|
|
@@ -84,6 +105,23 @@ export interface ClaimFreshness {
|
|
|
84
105
|
text: string;
|
|
85
106
|
fresh: boolean;
|
|
86
107
|
staleFiles?: string[];
|
|
108
|
+
/** Fase D, only set under semanticDiff: symbols whose raw hash changed but only cosmetically. */
|
|
109
|
+
cosmeticChanges?: string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Result of a single id passed to `TheBrainV2.verifyByIds`. `id` may be an entry id or a
|
|
113
|
+
* claim id — the caller doesn't need to know which, since both are opaque ids handed back
|
|
114
|
+
* by a prior search_memory/store_memory call. "unknown" (never "fresh") is what a caller
|
|
115
|
+
* gets for a purged entry, a renamed symbol's old claim, or an id from another project.
|
|
116
|
+
*/
|
|
117
|
+
export interface VerifyMemoryResult {
|
|
118
|
+
id: string;
|
|
119
|
+
status: 'fresh' | 'stale' | 'unknown';
|
|
120
|
+
staleFiles?: string[];
|
|
121
|
+
/** Only set when `id` resolved to an entry that itself has per-claim tracking. */
|
|
122
|
+
claimBreakdown?: ClaimFreshness[];
|
|
123
|
+
/** Fase D, only set under semanticDiff: symbols whose raw hash changed but only cosmetically. */
|
|
124
|
+
cosmeticChanges?: string[];
|
|
87
125
|
}
|
|
88
126
|
export interface SymbolRef {
|
|
89
127
|
/** Relative or absolute; resolved against process.cwd() the same way filePaths is. */
|
|
@@ -102,6 +140,8 @@ export interface BrainSearchResult {
|
|
|
102
140
|
outcome?: 'confirmed' | 'failed';
|
|
103
141
|
claims?: ClaimFreshness[];
|
|
104
142
|
domain?: string;
|
|
143
|
+
/** Fase D, only set under semanticDiff: symbols whose raw hash changed but only cosmetically. */
|
|
144
|
+
cosmeticChanges?: string[];
|
|
105
145
|
}
|
|
106
146
|
export interface BrainSearchOptions {
|
|
107
147
|
/**
|
|
@@ -114,6 +154,14 @@ export interface BrainSearchOptions {
|
|
|
114
154
|
projectId?: string;
|
|
115
155
|
/** Prefer memories tagged with this domain. Same soft-fallback rule as projectId. */
|
|
116
156
|
domain?: string;
|
|
157
|
+
/**
|
|
158
|
+
* Fase D, opt-in and off by default: when a tracked symbol's raw hash changed, check
|
|
159
|
+
* whether its normalized (comment/whitespace-stripped) hash also changed before marking
|
|
160
|
+
* stale. Only a real change still marks stale; a comment/formatting-only edit doesn't.
|
|
161
|
+
* Requires the entry to have been stored with symbolNormalizedHashes — an older entry
|
|
162
|
+
* without it still falls back to raw-hash-only (stale), same as with the flag off.
|
|
163
|
+
*/
|
|
164
|
+
semanticDiff?: boolean;
|
|
117
165
|
}
|
|
118
166
|
export interface BrainStats {
|
|
119
167
|
totalEntries: number;
|
|
@@ -152,6 +200,47 @@ export declare function bm25Score(queryTerms: string[], docTermFreq: Record<stri
|
|
|
152
200
|
* Jaccard similarity between two term sets.
|
|
153
201
|
*/
|
|
154
202
|
export declare function jaccardSimilarity(setA: Set<string>, setB: Set<string>): number;
|
|
203
|
+
/** A `derivedFrom` id resolves to either a full entry or one of its claims. */
|
|
204
|
+
export type FreshnessNode = {
|
|
205
|
+
kind: 'entry';
|
|
206
|
+
entry: BrainEntry;
|
|
207
|
+
} | {
|
|
208
|
+
kind: 'claim';
|
|
209
|
+
claim: Claim;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Looks up a `derivedFrom` id against both entries and claims in one pass. `claimIndex`
|
|
213
|
+
* is built once per outer call (search/verifyByIds) and threaded through recursion,
|
|
214
|
+
* never rebuilt per level — rebuilding per level would turn a bounded-depth walk into
|
|
215
|
+
* O(depth * entries) work for no reason.
|
|
216
|
+
*/
|
|
217
|
+
export declare function resolveFreshnessNode(entries: Map<string, BrainEntry>, claimIndex: Map<string, Claim>, id: string): FreshnessNode | undefined;
|
|
218
|
+
/** Builds an id -> Claim index across every entry's claims, for resolveFreshnessNode. */
|
|
219
|
+
export declare function buildClaimIndex(entries: Map<string, BrainEntry>): Map<string, Claim>;
|
|
220
|
+
/**
|
|
221
|
+
* Bound on how many `derivedFrom` hops checkEntryFreshness will walk. Procedence chains
|
|
222
|
+
* are meant to be short (a conclusion built on a conclusion, maybe twice); an unbounded
|
|
223
|
+
* walk would turn every search() result into a full graph traversal.
|
|
224
|
+
*/
|
|
225
|
+
export declare const MAX_DERIVED_DEPTH = 3;
|
|
226
|
+
/**
|
|
227
|
+
* True only if every file/symbol this entry was recorded against still hashes the same,
|
|
228
|
+
* AND (when `derivedFrom` is present and a resolver was passed) every memory this one was
|
|
229
|
+
* built on top of is itself still fresh, walked up to MAX_DERIVED_DEPTH hops. A file that
|
|
230
|
+
* also has a tracked symbol is judged by the symbol's hash, not the whole file's — an edit
|
|
231
|
+
* elsewhere in the same file (a different function, an import, a comment) must not
|
|
232
|
+
* invalidate a memory that was only ever about one specific symbol.
|
|
233
|
+
*/
|
|
234
|
+
export declare function checkEntryFreshness(entry: BrainEntry, resolveNode?: (id: string) => FreshnessNode | undefined, depth?: number, visited?: Set<string>, semanticDiff?: boolean): {
|
|
235
|
+
fresh: boolean;
|
|
236
|
+
staleFiles: string[];
|
|
237
|
+
cosmeticChanges?: string[];
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Same hash-compare as checkEntryFreshness, scoped to one claim's own evidence — a claim
|
|
241
|
+
* with no fileHashes/symbolHashes at all is always fresh (nothing tracked to go stale).
|
|
242
|
+
*/
|
|
243
|
+
export declare function checkClaimFreshness(claim: Claim, semanticDiff?: boolean): ClaimFreshness;
|
|
155
244
|
export declare class TheBrainV2 {
|
|
156
245
|
private entries;
|
|
157
246
|
private invertedIndex;
|
|
@@ -171,7 +260,7 @@ export declare class TheBrainV2 {
|
|
|
171
260
|
* Store a query+response pair in the brain.
|
|
172
261
|
* Returns false if detected as duplicate (>= dupThreshold similarity).
|
|
173
262
|
*/
|
|
174
|
-
store(query: string, response: string, provider?: string, dupThreshold?: number, filePaths?: string[], projectId?: string, outcome?: 'confirmed' | 'failed', symbolRefs?: SymbolRef[], claimInputs?: ClaimInput[], domain?: string): {
|
|
263
|
+
store(query: string, response: string, provider?: string, dupThreshold?: number, filePaths?: string[], projectId?: string, outcome?: 'confirmed' | 'failed', symbolRefs?: SymbolRef[], claimInputs?: ClaimInput[], domain?: string, derivedFrom?: string[]): {
|
|
175
264
|
stored: boolean;
|
|
176
265
|
reason: string;
|
|
177
266
|
duplicate?: BrainSearchResult;
|
|
@@ -229,6 +318,16 @@ export declare class TheBrainV2 {
|
|
|
229
318
|
ok: boolean;
|
|
230
319
|
message: string;
|
|
231
320
|
};
|
|
321
|
+
/**
|
|
322
|
+
* Revalidate ids from a prior search_memory/store_memory result via hash-compare only —
|
|
323
|
+
* no BM25, no `search()`. `ids` may mix entry ids and claim ids freely; each resolves
|
|
324
|
+
* independently and unknown ids fail closed to "unknown", never "fresh" (an id may be
|
|
325
|
+
* unknown because the entry was purged, or because the symbol it named was renamed —
|
|
326
|
+
* either way there is nothing left to vouch for it).
|
|
327
|
+
*/
|
|
328
|
+
verifyByIds(ids: string[], options?: {
|
|
329
|
+
semanticDiff?: boolean;
|
|
330
|
+
}): VerifyMemoryResult[];
|
|
232
331
|
getStats(): BrainStats;
|
|
233
332
|
clear(): void;
|
|
234
333
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TheBrainV2.d.ts","sourceRoot":"","sources":["../../../src/subconscious/TheBrainV2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAInB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"TheBrainV2.d.ts","sourceRoot":"","sources":["../../../src/subconscious/TheBrainV2.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,12 +14,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.TheBrainV2 = exports.BloomFilter = void 0;
|
|
17
|
+
exports.TheBrainV2 = exports.MAX_DERIVED_DEPTH = exports.BloomFilter = void 0;
|
|
18
18
|
exports.deriveProjectId = deriveProjectId;
|
|
19
19
|
exports.tokenize = tokenize;
|
|
20
20
|
exports.termFrequencies = termFrequencies;
|
|
21
21
|
exports.bm25Score = bm25Score;
|
|
22
22
|
exports.jaccardSimilarity = jaccardSimilarity;
|
|
23
|
+
exports.resolveFreshnessNode = resolveFreshnessNode;
|
|
24
|
+
exports.buildClaimIndex = buildClaimIndex;
|
|
25
|
+
exports.checkEntryFreshness = checkEntryFreshness;
|
|
26
|
+
exports.checkClaimFreshness = checkClaimFreshness;
|
|
23
27
|
exports.getBrain = getBrain;
|
|
24
28
|
const fs_1 = __importDefault(require("fs"));
|
|
25
29
|
const path_1 = __importDefault(require("path"));
|
|
@@ -294,33 +298,80 @@ function hashSymbolsForFreshness(refs) {
|
|
|
294
298
|
for (const ref of refs) {
|
|
295
299
|
const abs = path_1.default.resolve(ref.filePath);
|
|
296
300
|
const key = `${abs}::${ref.symbolName}`;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
hashes[key] = currentSymbolHashPair(abs, ref.symbolName).raw;
|
|
302
|
+
}
|
|
303
|
+
return hashes;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Fase D companion to hashSymbolsForFreshness: same symbols, hashed after
|
|
307
|
+
* normalizeForFreshness strips comments/whitespace. Always computed alongside the raw hash
|
|
308
|
+
* (cheap — same parse) regardless of whether semanticDiff is ever turned on, so an entry
|
|
309
|
+
* stored with the flag off is still classifiable later if someone turns it on.
|
|
310
|
+
*/
|
|
311
|
+
function hashSymbolsNormalizedForFreshness(refs) {
|
|
312
|
+
const hashes = {};
|
|
313
|
+
for (const ref of refs) {
|
|
314
|
+
const abs = path_1.default.resolve(ref.filePath);
|
|
315
|
+
const key = `${abs}::${ref.symbolName}`;
|
|
316
|
+
hashes[key] = currentSymbolHashPair(abs, ref.symbolName).normalized;
|
|
304
317
|
}
|
|
305
318
|
return hashes;
|
|
306
319
|
}
|
|
307
|
-
function
|
|
320
|
+
function currentSymbolHashPair(abs, symbolName) {
|
|
308
321
|
try {
|
|
309
322
|
const ctx = (0, SymbolSurgicalContext_1.getSymbolSurgicalContext)(path_1.default.dirname(abs), path_1.default.basename(abs), symbolName);
|
|
310
|
-
return
|
|
323
|
+
return {
|
|
324
|
+
raw: crypto_1.default.createHash('sha256').update(ctx.implementation).digest('hex'),
|
|
325
|
+
normalized: crypto_1.default.createHash('sha256').update((0, SymbolSurgicalContext_1.normalizeForFreshness)(ctx.implementation)).digest('hex'),
|
|
326
|
+
};
|
|
311
327
|
}
|
|
312
328
|
catch {
|
|
313
|
-
return 'MISSING';
|
|
329
|
+
return { raw: 'MISSING', normalized: 'MISSING' };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Looks up a `derivedFrom` id against both entries and claims in one pass. `claimIndex`
|
|
334
|
+
* is built once per outer call (search/verifyByIds) and threaded through recursion,
|
|
335
|
+
* never rebuilt per level — rebuilding per level would turn a bounded-depth walk into
|
|
336
|
+
* O(depth * entries) work for no reason.
|
|
337
|
+
*/
|
|
338
|
+
function resolveFreshnessNode(entries, claimIndex, id) {
|
|
339
|
+
const entry = entries.get(id);
|
|
340
|
+
if (entry)
|
|
341
|
+
return { kind: 'entry', entry };
|
|
342
|
+
const claim = claimIndex.get(id);
|
|
343
|
+
if (claim)
|
|
344
|
+
return { kind: 'claim', claim };
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
/** Builds an id -> Claim index across every entry's claims, for resolveFreshnessNode. */
|
|
348
|
+
function buildClaimIndex(entries) {
|
|
349
|
+
const index = new Map();
|
|
350
|
+
for (const entry of entries.values()) {
|
|
351
|
+
if (!entry.claims)
|
|
352
|
+
continue;
|
|
353
|
+
for (const claim of entry.claims)
|
|
354
|
+
index.set(claim.id, claim);
|
|
314
355
|
}
|
|
356
|
+
return index;
|
|
315
357
|
}
|
|
316
358
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* comment) must not invalidate a memory that was only ever about one specific symbol.
|
|
359
|
+
* Bound on how many `derivedFrom` hops checkEntryFreshness will walk. Procedence chains
|
|
360
|
+
* are meant to be short (a conclusion built on a conclusion, maybe twice); an unbounded
|
|
361
|
+
* walk would turn every search() result into a full graph traversal.
|
|
321
362
|
*/
|
|
322
|
-
|
|
363
|
+
exports.MAX_DERIVED_DEPTH = 3;
|
|
364
|
+
/**
|
|
365
|
+
* True only if every file/symbol this entry was recorded against still hashes the same,
|
|
366
|
+
* AND (when `derivedFrom` is present and a resolver was passed) every memory this one was
|
|
367
|
+
* built on top of is itself still fresh, walked up to MAX_DERIVED_DEPTH hops. A file that
|
|
368
|
+
* also has a tracked symbol is judged by the symbol's hash, not the whole file's — an edit
|
|
369
|
+
* elsewhere in the same file (a different function, an import, a comment) must not
|
|
370
|
+
* invalidate a memory that was only ever about one specific symbol.
|
|
371
|
+
*/
|
|
372
|
+
function checkEntryFreshness(entry, resolveNode, depth = 0, visited = new Set(), semanticDiff = false) {
|
|
323
373
|
const staleFiles = [];
|
|
374
|
+
const cosmeticChanges = [];
|
|
324
375
|
const relLabel = (abs) => {
|
|
325
376
|
const rel = path_1.default.relative(process.cwd(), abs);
|
|
326
377
|
return rel.startsWith('..') ? abs : rel;
|
|
@@ -346,12 +397,41 @@ function checkEntryFreshness(entry) {
|
|
|
346
397
|
const sep = key.lastIndexOf('::');
|
|
347
398
|
const abs = key.substring(0, sep);
|
|
348
399
|
const symbolName = key.substring(sep + 2);
|
|
349
|
-
|
|
350
|
-
|
|
400
|
+
const label = `${relLabel(abs)}::${symbolName}`;
|
|
401
|
+
const currentPair = currentSymbolHashPair(abs, symbolName);
|
|
402
|
+
if (currentPair.raw === storedHash)
|
|
403
|
+
continue;
|
|
404
|
+
if (semanticDiff) {
|
|
405
|
+
const storedNormalized = entry.symbolNormalizedHashes?.[key];
|
|
406
|
+
if (storedNormalized && currentPair.normalized === storedNormalized) {
|
|
407
|
+
cosmeticChanges.push(label); // comment/whitespace-only — not stale
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
351
410
|
}
|
|
411
|
+
staleFiles.push(label);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (entry.derivedFrom && entry.derivedFrom.length > 0 && resolveNode && depth < exports.MAX_DERIVED_DEPTH) {
|
|
415
|
+
for (const depId of entry.derivedFrom) {
|
|
416
|
+
if (visited.has(depId))
|
|
417
|
+
continue; // cycle guard — already accounted for in this chain
|
|
418
|
+
const node = resolveNode(depId);
|
|
419
|
+
if (!node) {
|
|
420
|
+
// Can't verify a dependency that no longer resolves — fail closed, same invariant
|
|
421
|
+
// as verify_memory's unknown ids.
|
|
422
|
+
staleFiles.push(`derived:${depId} (unknown)`);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const nextVisited = new Set(visited);
|
|
426
|
+
nextVisited.add(depId);
|
|
427
|
+
const depFresh = node.kind === 'entry'
|
|
428
|
+
? checkEntryFreshness(node.entry, resolveNode, depth + 1, nextVisited, semanticDiff).fresh
|
|
429
|
+
: checkClaimFreshness(node.claim, semanticDiff).fresh;
|
|
430
|
+
if (!depFresh)
|
|
431
|
+
staleFiles.push(`derived:${depId}`);
|
|
352
432
|
}
|
|
353
433
|
}
|
|
354
|
-
return { fresh: staleFiles.length === 0, staleFiles };
|
|
434
|
+
return { fresh: staleFiles.length === 0, staleFiles, ...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}) };
|
|
355
435
|
}
|
|
356
436
|
/** Builds the persisted `Claim[]` for an entry from the caller-supplied claim inputs. */
|
|
357
437
|
function buildClaims(inputs) {
|
|
@@ -362,14 +442,16 @@ function buildClaims(inputs) {
|
|
|
362
442
|
text: c.text.trim(),
|
|
363
443
|
fileHashes: c.filePaths && c.filePaths.length > 0 ? hashFilesForFreshness(c.filePaths) : undefined,
|
|
364
444
|
symbolHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsForFreshness(c.symbols) : undefined,
|
|
445
|
+
symbolNormalizedHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsNormalizedForFreshness(c.symbols) : undefined,
|
|
365
446
|
}));
|
|
366
447
|
}
|
|
367
448
|
/**
|
|
368
449
|
* Same hash-compare as checkEntryFreshness, scoped to one claim's own evidence — a claim
|
|
369
450
|
* with no fileHashes/symbolHashes at all is always fresh (nothing tracked to go stale).
|
|
370
451
|
*/
|
|
371
|
-
function checkClaimFreshness(claim) {
|
|
452
|
+
function checkClaimFreshness(claim, semanticDiff = false) {
|
|
372
453
|
const staleFiles = [];
|
|
454
|
+
const cosmeticChanges = [];
|
|
373
455
|
const relLabel = (abs) => {
|
|
374
456
|
const rel = path_1.default.relative(process.cwd(), abs);
|
|
375
457
|
return rel.startsWith('..') ? abs : rel;
|
|
@@ -395,12 +477,27 @@ function checkClaimFreshness(claim) {
|
|
|
395
477
|
const sep = key.lastIndexOf('::');
|
|
396
478
|
const abs = key.substring(0, sep);
|
|
397
479
|
const symbolName = key.substring(sep + 2);
|
|
398
|
-
|
|
399
|
-
|
|
480
|
+
const label = `${relLabel(abs)}::${symbolName}`;
|
|
481
|
+
const currentPair = currentSymbolHashPair(abs, symbolName);
|
|
482
|
+
if (currentPair.raw === storedHash)
|
|
483
|
+
continue;
|
|
484
|
+
if (semanticDiff) {
|
|
485
|
+
const storedNormalized = claim.symbolNormalizedHashes?.[key];
|
|
486
|
+
if (storedNormalized && currentPair.normalized === storedNormalized) {
|
|
487
|
+
cosmeticChanges.push(label);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
400
490
|
}
|
|
491
|
+
staleFiles.push(label);
|
|
401
492
|
}
|
|
402
493
|
}
|
|
403
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
id: claim.id,
|
|
496
|
+
text: claim.text,
|
|
497
|
+
fresh: staleFiles.length === 0,
|
|
498
|
+
...(staleFiles.length > 0 ? { staleFiles } : {}),
|
|
499
|
+
...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
500
|
+
};
|
|
404
501
|
}
|
|
405
502
|
// ─── TheBrainV2 ──────────────────────────────────────────────────────────────
|
|
406
503
|
class TheBrainV2 {
|
|
@@ -513,7 +610,7 @@ class TheBrainV2 {
|
|
|
513
610
|
* Store a query+response pair in the brain.
|
|
514
611
|
* Returns false if detected as duplicate (>= dupThreshold similarity).
|
|
515
612
|
*/
|
|
516
|
-
store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain) {
|
|
613
|
+
store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain, derivedFrom) {
|
|
517
614
|
// Quick bloom check
|
|
518
615
|
const queryKey = query.trim().toLowerCase().substring(0, 200);
|
|
519
616
|
if (this.bloom.has(queryKey)) {
|
|
@@ -570,8 +667,10 @@ class TheBrainV2 {
|
|
|
570
667
|
fileHashes: filePaths && filePaths.length > 0 ? hashFilesForFreshness(filePaths) : undefined,
|
|
571
668
|
outcome,
|
|
572
669
|
symbolHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsForFreshness(symbolRefs) : undefined,
|
|
670
|
+
symbolNormalizedHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsNormalizedForFreshness(symbolRefs) : undefined,
|
|
573
671
|
claims: claimInputs && claimInputs.length > 0 ? buildClaims(claimInputs) : undefined,
|
|
574
672
|
domain,
|
|
673
|
+
derivedFrom: derivedFrom && derivedFrom.length > 0 ? derivedFrom : undefined,
|
|
575
674
|
};
|
|
576
675
|
this.entries.set(id, entry);
|
|
577
676
|
// Update inverted index
|
|
@@ -727,12 +826,15 @@ class TheBrainV2 {
|
|
|
727
826
|
});
|
|
728
827
|
// 6. Sort and filter
|
|
729
828
|
combined.sort((a, b) => b.similarity - a.similarity);
|
|
829
|
+
const claimIndex = buildClaimIndex(this.entries);
|
|
830
|
+
const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
|
|
831
|
+
const semanticDiff = options.semanticDiff === true;
|
|
730
832
|
const results = combined
|
|
731
833
|
.filter(r => r.similarity >= minSimilarity)
|
|
732
834
|
.slice(0, limit)
|
|
733
835
|
.map(r => {
|
|
734
|
-
const { fresh, staleFiles } = checkEntryFreshness(r.entry);
|
|
735
|
-
const claims = r.entry.claims?.map(checkClaimFreshness);
|
|
836
|
+
const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(r.entry, resolveNode, 0, new Set(), semanticDiff);
|
|
837
|
+
const claims = r.entry.claims?.map((c) => checkClaimFreshness(c, semanticDiff));
|
|
736
838
|
return {
|
|
737
839
|
id: r.entry.id,
|
|
738
840
|
query: r.entry.query,
|
|
@@ -745,6 +847,7 @@ class TheBrainV2 {
|
|
|
745
847
|
...(r.entry.outcome ? { outcome: r.entry.outcome } : {}),
|
|
746
848
|
...(claims && claims.length > 0 ? { claims } : {}),
|
|
747
849
|
...(r.entry.domain ? { domain: r.entry.domain } : {}),
|
|
850
|
+
...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
748
851
|
};
|
|
749
852
|
});
|
|
750
853
|
// 7. Update hit counters — only for searches a caller actually asked a question with.
|
|
@@ -815,6 +918,43 @@ class TheBrainV2 {
|
|
|
815
918
|
this.scheduleSave();
|
|
816
919
|
return { ok: true, message: `Recorded negative feedback on entry "${id}" (demerits: ${entry.demerits}). It will rank lower and be evicted sooner.` };
|
|
817
920
|
}
|
|
921
|
+
// ─── Batch verification ─────────────────────────────────────────────────────
|
|
922
|
+
/**
|
|
923
|
+
* Revalidate ids from a prior search_memory/store_memory result via hash-compare only —
|
|
924
|
+
* no BM25, no `search()`. `ids` may mix entry ids and claim ids freely; each resolves
|
|
925
|
+
* independently and unknown ids fail closed to "unknown", never "fresh" (an id may be
|
|
926
|
+
* unknown because the entry was purged, or because the symbol it named was renamed —
|
|
927
|
+
* either way there is nothing left to vouch for it).
|
|
928
|
+
*/
|
|
929
|
+
verifyByIds(ids, options = {}) {
|
|
930
|
+
const claimIndex = buildClaimIndex(this.entries);
|
|
931
|
+
const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
|
|
932
|
+
const semanticDiff = options.semanticDiff === true;
|
|
933
|
+
return ids.map((id) => {
|
|
934
|
+
const entry = this.entries.get(id);
|
|
935
|
+
if (entry) {
|
|
936
|
+
const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(entry, resolveNode, 0, new Set(), semanticDiff);
|
|
937
|
+
return {
|
|
938
|
+
id,
|
|
939
|
+
status: fresh ? 'fresh' : 'stale',
|
|
940
|
+
...(staleFiles.length > 0 ? { staleFiles } : {}),
|
|
941
|
+
...(entry.claims ? { claimBreakdown: entry.claims.map((c) => checkClaimFreshness(c, semanticDiff)) } : {}),
|
|
942
|
+
...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const claim = claimIndex.get(id);
|
|
946
|
+
if (claim) {
|
|
947
|
+
const cf = checkClaimFreshness(claim, semanticDiff);
|
|
948
|
+
return {
|
|
949
|
+
id,
|
|
950
|
+
status: cf.fresh ? 'fresh' : 'stale',
|
|
951
|
+
...(cf.staleFiles ? { staleFiles: cf.staleFiles } : {}),
|
|
952
|
+
...(cf.cosmeticChanges ? { cosmeticChanges: cf.cosmeticChanges } : {}),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return { id, status: 'unknown' };
|
|
956
|
+
});
|
|
957
|
+
}
|
|
818
958
|
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
819
959
|
getStats() {
|
|
820
960
|
return {
|