@nxuss/lemma 1.14.0 → 1.16.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 +14 -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 +95 -10
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +128 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +299 -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 +95 -10
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +128 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +294 -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,69 @@ 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
|
+
export interface BlastRadiusHit {
|
|
227
|
+
id: string;
|
|
228
|
+
kind: 'entry' | 'claim';
|
|
229
|
+
/** entry.query, or the claim's own text when kind === 'claim'. */
|
|
230
|
+
query: string;
|
|
231
|
+
outcome?: 'confirmed' | 'failed';
|
|
232
|
+
/** 'direct' = tracks the edited symbol/file itself; 'derived' = depends (via derivedFrom) on something that does. */
|
|
233
|
+
via: 'direct' | 'derived';
|
|
234
|
+
}
|
|
235
|
+
export interface BlastRadiusResult {
|
|
236
|
+
hits: BlastRadiusHit[];
|
|
237
|
+
/** Subset of hits with outcome === 'failed' — the escalated case worth a stronger warning. */
|
|
238
|
+
failedHits: BlastRadiusHit[];
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Everything in the Brain that would go stale (directly or transitively via derivedFrom) if
|
|
242
|
+
* the given symbol/file changes right now — computed BEFORE the write happens, so a write
|
|
243
|
+
* tool can warn instead of only ever reporting staleness the next time someone searches.
|
|
244
|
+
* Purely advisory: never blocks, never mutates anything, matches the fail-open/additive
|
|
245
|
+
* invariant every prior phase of proof-carrying context has kept.
|
|
246
|
+
*/
|
|
247
|
+
export declare function findBlastRadius(entries: Map<string, BrainEntry>, filePath: string, symbolName?: string): BlastRadiusResult;
|
|
248
|
+
/**
|
|
249
|
+
* True only if every file/symbol this entry was recorded against still hashes the same,
|
|
250
|
+
* AND (when `derivedFrom` is present and a resolver was passed) every memory this one was
|
|
251
|
+
* built on top of is itself still fresh, walked up to MAX_DERIVED_DEPTH hops. A file that
|
|
252
|
+
* also has a tracked symbol is judged by the symbol's hash, not the whole file's — an edit
|
|
253
|
+
* elsewhere in the same file (a different function, an import, a comment) must not
|
|
254
|
+
* invalidate a memory that was only ever about one specific symbol.
|
|
255
|
+
*/
|
|
256
|
+
export declare function checkEntryFreshness(entry: BrainEntry, resolveNode?: (id: string) => FreshnessNode | undefined, depth?: number, visited?: Set<string>, semanticDiff?: boolean): {
|
|
257
|
+
fresh: boolean;
|
|
258
|
+
staleFiles: string[];
|
|
259
|
+
cosmeticChanges?: string[];
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Same hash-compare as checkEntryFreshness, scoped to one claim's own evidence — a claim
|
|
263
|
+
* with no fileHashes/symbolHashes at all is always fresh (nothing tracked to go stale).
|
|
264
|
+
*/
|
|
265
|
+
export declare function checkClaimFreshness(claim: Claim, semanticDiff?: boolean): ClaimFreshness;
|
|
155
266
|
export declare class TheBrainV2 {
|
|
156
267
|
private entries;
|
|
157
268
|
private invertedIndex;
|
|
@@ -171,7 +282,7 @@ export declare class TheBrainV2 {
|
|
|
171
282
|
* Store a query+response pair in the brain.
|
|
172
283
|
* Returns false if detected as duplicate (>= dupThreshold similarity).
|
|
173
284
|
*/
|
|
174
|
-
store(query: string, response: string, provider?: string, dupThreshold?: number, filePaths?: string[], projectId?: string, outcome?: 'confirmed' | 'failed', symbolRefs?: SymbolRef[], claimInputs?: ClaimInput[], domain?: string): {
|
|
285
|
+
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
286
|
stored: boolean;
|
|
176
287
|
reason: string;
|
|
177
288
|
duplicate?: BrainSearchResult;
|
|
@@ -229,6 +340,22 @@ export declare class TheBrainV2 {
|
|
|
229
340
|
ok: boolean;
|
|
230
341
|
message: string;
|
|
231
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Revalidate ids from a prior search_memory/store_memory result via hash-compare only —
|
|
345
|
+
* no BM25, no `search()`. `ids` may mix entry ids and claim ids freely; each resolves
|
|
346
|
+
* independently and unknown ids fail closed to "unknown", never "fresh" (an id may be
|
|
347
|
+
* unknown because the entry was purged, or because the symbol it named was renamed —
|
|
348
|
+
* either way there is nothing left to vouch for it).
|
|
349
|
+
*/
|
|
350
|
+
verifyByIds(ids: string[], options?: {
|
|
351
|
+
semanticDiff?: boolean;
|
|
352
|
+
}): VerifyMemoryResult[];
|
|
353
|
+
/**
|
|
354
|
+
* What in the Brain would go stale, directly or transitively, if `filePath`/`symbolName`
|
|
355
|
+
* changes right now. Called by write tools BEFORE they'd otherwise find out (only on the
|
|
356
|
+
* next search) — see findBlastRadius for the algorithm and cost.
|
|
357
|
+
*/
|
|
358
|
+
findBlastRadius(filePath: string, symbolName?: string): BlastRadiusResult;
|
|
232
359
|
getStats(): BrainStats;
|
|
233
360
|
clear(): void;
|
|
234
361
|
}
|
|
@@ -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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"}
|
|
@@ -13,7 +13,7 @@ import fs from 'fs';
|
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import os from 'os';
|
|
15
15
|
import crypto from 'crypto';
|
|
16
|
-
import { getSymbolSurgicalContext } from '../utils/SymbolSurgicalContext.js';
|
|
16
|
+
import { getSymbolSurgicalContext, normalizeForFreshness } from '../utils/SymbolSurgicalContext.js';
|
|
17
17
|
// ─── BM25 Constants ───────────────────────────────────────────────────────────
|
|
18
18
|
const BM25_K1 = 1.5; // Term saturation (1.2-2.0)
|
|
19
19
|
const BM25_B = 0.75; // Length normalization (0-1)
|
|
@@ -281,33 +281,203 @@ function hashSymbolsForFreshness(refs) {
|
|
|
281
281
|
for (const ref of refs) {
|
|
282
282
|
const abs = path.resolve(ref.filePath);
|
|
283
283
|
const key = `${abs}::${ref.symbolName}`;
|
|
284
|
-
|
|
285
|
-
const ctx = getSymbolSurgicalContext(path.dirname(abs), path.basename(abs), ref.symbolName);
|
|
286
|
-
hashes[key] = crypto.createHash('sha256').update(ctx.implementation).digest('hex');
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
hashes[key] = 'MISSING';
|
|
290
|
-
}
|
|
284
|
+
hashes[key] = currentSymbolHashPair(abs, ref.symbolName).raw;
|
|
291
285
|
}
|
|
292
286
|
return hashes;
|
|
293
287
|
}
|
|
294
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Fase D companion to hashSymbolsForFreshness: same symbols, hashed after
|
|
290
|
+
* normalizeForFreshness strips comments/whitespace. Always computed alongside the raw hash
|
|
291
|
+
* (cheap — same parse) regardless of whether semanticDiff is ever turned on, so an entry
|
|
292
|
+
* stored with the flag off is still classifiable later if someone turns it on.
|
|
293
|
+
*/
|
|
294
|
+
function hashSymbolsNormalizedForFreshness(refs) {
|
|
295
|
+
const hashes = {};
|
|
296
|
+
for (const ref of refs) {
|
|
297
|
+
const abs = path.resolve(ref.filePath);
|
|
298
|
+
const key = `${abs}::${ref.symbolName}`;
|
|
299
|
+
hashes[key] = currentSymbolHashPair(abs, ref.symbolName).normalized;
|
|
300
|
+
}
|
|
301
|
+
return hashes;
|
|
302
|
+
}
|
|
303
|
+
function currentSymbolHashPair(abs, symbolName) {
|
|
295
304
|
try {
|
|
296
305
|
const ctx = getSymbolSurgicalContext(path.dirname(abs), path.basename(abs), symbolName);
|
|
297
|
-
return
|
|
306
|
+
return {
|
|
307
|
+
raw: crypto.createHash('sha256').update(ctx.implementation).digest('hex'),
|
|
308
|
+
normalized: crypto.createHash('sha256').update(normalizeForFreshness(ctx.implementation)).digest('hex'),
|
|
309
|
+
};
|
|
298
310
|
}
|
|
299
311
|
catch {
|
|
300
|
-
return 'MISSING';
|
|
312
|
+
return { raw: 'MISSING', normalized: 'MISSING' };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Looks up a `derivedFrom` id against both entries and claims in one pass. `claimIndex`
|
|
317
|
+
* is built once per outer call (search/verifyByIds) and threaded through recursion,
|
|
318
|
+
* never rebuilt per level — rebuilding per level would turn a bounded-depth walk into
|
|
319
|
+
* O(depth * entries) work for no reason.
|
|
320
|
+
*/
|
|
321
|
+
export function resolveFreshnessNode(entries, claimIndex, id) {
|
|
322
|
+
const entry = entries.get(id);
|
|
323
|
+
if (entry)
|
|
324
|
+
return { kind: 'entry', entry };
|
|
325
|
+
const claim = claimIndex.get(id);
|
|
326
|
+
if (claim)
|
|
327
|
+
return { kind: 'claim', claim };
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
/** Builds an id -> Claim index across every entry's claims, for resolveFreshnessNode. */
|
|
331
|
+
export function buildClaimIndex(entries) {
|
|
332
|
+
const index = new Map();
|
|
333
|
+
for (const entry of entries.values()) {
|
|
334
|
+
if (!entry.claims)
|
|
335
|
+
continue;
|
|
336
|
+
for (const claim of entry.claims)
|
|
337
|
+
index.set(claim.id, claim);
|
|
338
|
+
}
|
|
339
|
+
return index;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Bound on how many `derivedFrom` hops checkEntryFreshness will walk. Procedence chains
|
|
343
|
+
* are meant to be short (a conclusion built on a conclusion, maybe twice); an unbounded
|
|
344
|
+
* walk would turn every search() result into a full graph traversal.
|
|
345
|
+
*/
|
|
346
|
+
export const MAX_DERIVED_DEPTH = 3;
|
|
347
|
+
/**
|
|
348
|
+
* Maps "abs::symbolName" -> the entries/claims whose symbolHashes track that exact symbol.
|
|
349
|
+
* Rebuilt fresh per call (same pattern as buildClaimIndex) — cheap at the 8,000-entry cap
|
|
350
|
+
* (measured ~0.04ms per lookup at full capacity), not worth persisting or maintaining
|
|
351
|
+
* incrementally.
|
|
352
|
+
*/
|
|
353
|
+
function buildSymbolIndex(entries) {
|
|
354
|
+
const index = new Map();
|
|
355
|
+
const add = (key, hit) => {
|
|
356
|
+
const list = index.get(key);
|
|
357
|
+
if (list)
|
|
358
|
+
list.push(hit);
|
|
359
|
+
else
|
|
360
|
+
index.set(key, [hit]);
|
|
361
|
+
};
|
|
362
|
+
for (const entry of entries.values()) {
|
|
363
|
+
if (entry.symbolHashes) {
|
|
364
|
+
for (const key of Object.keys(entry.symbolHashes)) {
|
|
365
|
+
add(key, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (entry.claims) {
|
|
369
|
+
for (const claim of entry.claims) {
|
|
370
|
+
if (!claim.symbolHashes)
|
|
371
|
+
continue;
|
|
372
|
+
for (const key of Object.keys(claim.symbolHashes)) {
|
|
373
|
+
add(key, { id: claim.id, kind: 'claim', query: claim.text, outcome: entry.outcome, via: 'direct' });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return index;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Maps an absolute file path -> ids of entries whole-file-tracked against it (fileHashes),
|
|
382
|
+
* for write tools that don't parse AST and so never know which symbol they touched.
|
|
383
|
+
*/
|
|
384
|
+
function buildFileIndex(entries) {
|
|
385
|
+
const index = new Map();
|
|
386
|
+
const add = (key, hit) => {
|
|
387
|
+
const list = index.get(key);
|
|
388
|
+
if (list)
|
|
389
|
+
list.push(hit);
|
|
390
|
+
else
|
|
391
|
+
index.set(key, [hit]);
|
|
392
|
+
};
|
|
393
|
+
for (const entry of entries.values()) {
|
|
394
|
+
if (entry.fileHashes) {
|
|
395
|
+
for (const abs of Object.keys(entry.fileHashes)) {
|
|
396
|
+
add(abs, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// A file-level fallback should also catch entries that only track a *symbol* in that
|
|
400
|
+
// file — an untargeted write to the file can plausibly touch it too, and over-reporting
|
|
401
|
+
// here is the safe direction (this only ever produces an advisory note, never blocks).
|
|
402
|
+
if (entry.symbolHashes) {
|
|
403
|
+
const absPaths = new Set(Object.keys(entry.symbolHashes).map((k) => k.substring(0, k.lastIndexOf('::'))));
|
|
404
|
+
for (const abs of absPaths) {
|
|
405
|
+
add(abs, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
301
408
|
}
|
|
409
|
+
return index;
|
|
410
|
+
}
|
|
411
|
+
/** Maps a dependency id -> ids of entries whose derivedFrom includes it (Fase B, reversed). */
|
|
412
|
+
function buildDerivedFromReverseIndex(entries) {
|
|
413
|
+
const index = new Map();
|
|
414
|
+
for (const entry of entries.values()) {
|
|
415
|
+
if (!entry.derivedFrom)
|
|
416
|
+
continue;
|
|
417
|
+
for (const depId of entry.derivedFrom) {
|
|
418
|
+
const list = index.get(depId);
|
|
419
|
+
if (list)
|
|
420
|
+
list.push(entry.id);
|
|
421
|
+
else
|
|
422
|
+
index.set(depId, [entry.id]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return index;
|
|
302
426
|
}
|
|
303
427
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
428
|
+
* Everything in the Brain that would go stale (directly or transitively via derivedFrom) if
|
|
429
|
+
* the given symbol/file changes right now — computed BEFORE the write happens, so a write
|
|
430
|
+
* tool can warn instead of only ever reporting staleness the next time someone searches.
|
|
431
|
+
* Purely advisory: never blocks, never mutates anything, matches the fail-open/additive
|
|
432
|
+
* invariant every prior phase of proof-carrying context has kept.
|
|
308
433
|
*/
|
|
309
|
-
function
|
|
434
|
+
export function findBlastRadius(entries, filePath, symbolName) {
|
|
435
|
+
const abs = path.resolve(filePath);
|
|
436
|
+
const symbolIndex = buildSymbolIndex(entries);
|
|
437
|
+
const fileIndex = buildFileIndex(entries);
|
|
438
|
+
const reverseDerived = buildDerivedFromReverseIndex(entries);
|
|
439
|
+
const directHits = symbolName ? symbolIndex.get(`${abs}::${symbolName}`) || [] : fileIndex.get(abs) || [];
|
|
440
|
+
const hitsById = new Map();
|
|
441
|
+
for (const hit of directHits)
|
|
442
|
+
hitsById.set(hit.id, hit);
|
|
443
|
+
// BFS the reverse derivedFrom graph from every direct hit's entry id, bounded to
|
|
444
|
+
// MAX_DERIVED_DEPTH hops — same bound and cycle-guard as checkEntryFreshness's forward
|
|
445
|
+
// walk, just traversed the other direction (dependents, not dependencies).
|
|
446
|
+
let frontier = directHits.map((h) => h.id);
|
|
447
|
+
const visited = new Set(frontier);
|
|
448
|
+
for (let depth = 0; depth < MAX_DERIVED_DEPTH && frontier.length > 0; depth++) {
|
|
449
|
+
const next = [];
|
|
450
|
+
for (const id of frontier) {
|
|
451
|
+
const dependents = reverseDerived.get(id) || [];
|
|
452
|
+
for (const depId of dependents) {
|
|
453
|
+
if (visited.has(depId))
|
|
454
|
+
continue;
|
|
455
|
+
visited.add(depId);
|
|
456
|
+
next.push(depId);
|
|
457
|
+
if (!hitsById.has(depId)) {
|
|
458
|
+
const entry = entries.get(depId);
|
|
459
|
+
if (entry) {
|
|
460
|
+
hitsById.set(depId, { id: depId, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'derived' });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
frontier = next;
|
|
466
|
+
}
|
|
467
|
+
const hits = Array.from(hitsById.values());
|
|
468
|
+
return { hits, failedHits: hits.filter((h) => h.outcome === 'failed') };
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* True only if every file/symbol this entry was recorded against still hashes the same,
|
|
472
|
+
* AND (when `derivedFrom` is present and a resolver was passed) every memory this one was
|
|
473
|
+
* built on top of is itself still fresh, walked up to MAX_DERIVED_DEPTH hops. A file that
|
|
474
|
+
* also has a tracked symbol is judged by the symbol's hash, not the whole file's — an edit
|
|
475
|
+
* elsewhere in the same file (a different function, an import, a comment) must not
|
|
476
|
+
* invalidate a memory that was only ever about one specific symbol.
|
|
477
|
+
*/
|
|
478
|
+
export function checkEntryFreshness(entry, resolveNode, depth = 0, visited = new Set(), semanticDiff = false) {
|
|
310
479
|
const staleFiles = [];
|
|
480
|
+
const cosmeticChanges = [];
|
|
311
481
|
const relLabel = (abs) => {
|
|
312
482
|
const rel = path.relative(process.cwd(), abs);
|
|
313
483
|
return rel.startsWith('..') ? abs : rel;
|
|
@@ -333,12 +503,41 @@ function checkEntryFreshness(entry) {
|
|
|
333
503
|
const sep = key.lastIndexOf('::');
|
|
334
504
|
const abs = key.substring(0, sep);
|
|
335
505
|
const symbolName = key.substring(sep + 2);
|
|
336
|
-
|
|
337
|
-
|
|
506
|
+
const label = `${relLabel(abs)}::${symbolName}`;
|
|
507
|
+
const currentPair = currentSymbolHashPair(abs, symbolName);
|
|
508
|
+
if (currentPair.raw === storedHash)
|
|
509
|
+
continue;
|
|
510
|
+
if (semanticDiff) {
|
|
511
|
+
const storedNormalized = entry.symbolNormalizedHashes?.[key];
|
|
512
|
+
if (storedNormalized && currentPair.normalized === storedNormalized) {
|
|
513
|
+
cosmeticChanges.push(label); // comment/whitespace-only — not stale
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
338
516
|
}
|
|
517
|
+
staleFiles.push(label);
|
|
339
518
|
}
|
|
340
519
|
}
|
|
341
|
-
|
|
520
|
+
if (entry.derivedFrom && entry.derivedFrom.length > 0 && resolveNode && depth < MAX_DERIVED_DEPTH) {
|
|
521
|
+
for (const depId of entry.derivedFrom) {
|
|
522
|
+
if (visited.has(depId))
|
|
523
|
+
continue; // cycle guard — already accounted for in this chain
|
|
524
|
+
const node = resolveNode(depId);
|
|
525
|
+
if (!node) {
|
|
526
|
+
// Can't verify a dependency that no longer resolves — fail closed, same invariant
|
|
527
|
+
// as verify_memory's unknown ids.
|
|
528
|
+
staleFiles.push(`derived:${depId} (unknown)`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const nextVisited = new Set(visited);
|
|
532
|
+
nextVisited.add(depId);
|
|
533
|
+
const depFresh = node.kind === 'entry'
|
|
534
|
+
? checkEntryFreshness(node.entry, resolveNode, depth + 1, nextVisited, semanticDiff).fresh
|
|
535
|
+
: checkClaimFreshness(node.claim, semanticDiff).fresh;
|
|
536
|
+
if (!depFresh)
|
|
537
|
+
staleFiles.push(`derived:${depId}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return { fresh: staleFiles.length === 0, staleFiles, ...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}) };
|
|
342
541
|
}
|
|
343
542
|
/** Builds the persisted `Claim[]` for an entry from the caller-supplied claim inputs. */
|
|
344
543
|
function buildClaims(inputs) {
|
|
@@ -349,14 +548,16 @@ function buildClaims(inputs) {
|
|
|
349
548
|
text: c.text.trim(),
|
|
350
549
|
fileHashes: c.filePaths && c.filePaths.length > 0 ? hashFilesForFreshness(c.filePaths) : undefined,
|
|
351
550
|
symbolHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsForFreshness(c.symbols) : undefined,
|
|
551
|
+
symbolNormalizedHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsNormalizedForFreshness(c.symbols) : undefined,
|
|
352
552
|
}));
|
|
353
553
|
}
|
|
354
554
|
/**
|
|
355
555
|
* Same hash-compare as checkEntryFreshness, scoped to one claim's own evidence — a claim
|
|
356
556
|
* with no fileHashes/symbolHashes at all is always fresh (nothing tracked to go stale).
|
|
357
557
|
*/
|
|
358
|
-
function checkClaimFreshness(claim) {
|
|
558
|
+
export function checkClaimFreshness(claim, semanticDiff = false) {
|
|
359
559
|
const staleFiles = [];
|
|
560
|
+
const cosmeticChanges = [];
|
|
360
561
|
const relLabel = (abs) => {
|
|
361
562
|
const rel = path.relative(process.cwd(), abs);
|
|
362
563
|
return rel.startsWith('..') ? abs : rel;
|
|
@@ -382,12 +583,27 @@ function checkClaimFreshness(claim) {
|
|
|
382
583
|
const sep = key.lastIndexOf('::');
|
|
383
584
|
const abs = key.substring(0, sep);
|
|
384
585
|
const symbolName = key.substring(sep + 2);
|
|
385
|
-
|
|
386
|
-
|
|
586
|
+
const label = `${relLabel(abs)}::${symbolName}`;
|
|
587
|
+
const currentPair = currentSymbolHashPair(abs, symbolName);
|
|
588
|
+
if (currentPair.raw === storedHash)
|
|
589
|
+
continue;
|
|
590
|
+
if (semanticDiff) {
|
|
591
|
+
const storedNormalized = claim.symbolNormalizedHashes?.[key];
|
|
592
|
+
if (storedNormalized && currentPair.normalized === storedNormalized) {
|
|
593
|
+
cosmeticChanges.push(label);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
387
596
|
}
|
|
597
|
+
staleFiles.push(label);
|
|
388
598
|
}
|
|
389
599
|
}
|
|
390
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
id: claim.id,
|
|
602
|
+
text: claim.text,
|
|
603
|
+
fresh: staleFiles.length === 0,
|
|
604
|
+
...(staleFiles.length > 0 ? { staleFiles } : {}),
|
|
605
|
+
...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
606
|
+
};
|
|
391
607
|
}
|
|
392
608
|
// ─── TheBrainV2 ──────────────────────────────────────────────────────────────
|
|
393
609
|
export class TheBrainV2 {
|
|
@@ -500,7 +716,7 @@ export class TheBrainV2 {
|
|
|
500
716
|
* Store a query+response pair in the brain.
|
|
501
717
|
* Returns false if detected as duplicate (>= dupThreshold similarity).
|
|
502
718
|
*/
|
|
503
|
-
store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain) {
|
|
719
|
+
store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain, derivedFrom) {
|
|
504
720
|
// Quick bloom check
|
|
505
721
|
const queryKey = query.trim().toLowerCase().substring(0, 200);
|
|
506
722
|
if (this.bloom.has(queryKey)) {
|
|
@@ -557,8 +773,10 @@ export class TheBrainV2 {
|
|
|
557
773
|
fileHashes: filePaths && filePaths.length > 0 ? hashFilesForFreshness(filePaths) : undefined,
|
|
558
774
|
outcome,
|
|
559
775
|
symbolHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsForFreshness(symbolRefs) : undefined,
|
|
776
|
+
symbolNormalizedHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsNormalizedForFreshness(symbolRefs) : undefined,
|
|
560
777
|
claims: claimInputs && claimInputs.length > 0 ? buildClaims(claimInputs) : undefined,
|
|
561
778
|
domain,
|
|
779
|
+
derivedFrom: derivedFrom && derivedFrom.length > 0 ? derivedFrom : undefined,
|
|
562
780
|
};
|
|
563
781
|
this.entries.set(id, entry);
|
|
564
782
|
// Update inverted index
|
|
@@ -714,12 +932,15 @@ export class TheBrainV2 {
|
|
|
714
932
|
});
|
|
715
933
|
// 6. Sort and filter
|
|
716
934
|
combined.sort((a, b) => b.similarity - a.similarity);
|
|
935
|
+
const claimIndex = buildClaimIndex(this.entries);
|
|
936
|
+
const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
|
|
937
|
+
const semanticDiff = options.semanticDiff === true;
|
|
717
938
|
const results = combined
|
|
718
939
|
.filter(r => r.similarity >= minSimilarity)
|
|
719
940
|
.slice(0, limit)
|
|
720
941
|
.map(r => {
|
|
721
|
-
const { fresh, staleFiles } = checkEntryFreshness(r.entry);
|
|
722
|
-
const claims = r.entry.claims?.map(checkClaimFreshness);
|
|
942
|
+
const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(r.entry, resolveNode, 0, new Set(), semanticDiff);
|
|
943
|
+
const claims = r.entry.claims?.map((c) => checkClaimFreshness(c, semanticDiff));
|
|
723
944
|
return {
|
|
724
945
|
id: r.entry.id,
|
|
725
946
|
query: r.entry.query,
|
|
@@ -732,6 +953,7 @@ export class TheBrainV2 {
|
|
|
732
953
|
...(r.entry.outcome ? { outcome: r.entry.outcome } : {}),
|
|
733
954
|
...(claims && claims.length > 0 ? { claims } : {}),
|
|
734
955
|
...(r.entry.domain ? { domain: r.entry.domain } : {}),
|
|
956
|
+
...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
735
957
|
};
|
|
736
958
|
});
|
|
737
959
|
// 7. Update hit counters — only for searches a caller actually asked a question with.
|
|
@@ -802,6 +1024,52 @@ export class TheBrainV2 {
|
|
|
802
1024
|
this.scheduleSave();
|
|
803
1025
|
return { ok: true, message: `Recorded negative feedback on entry "${id}" (demerits: ${entry.demerits}). It will rank lower and be evicted sooner.` };
|
|
804
1026
|
}
|
|
1027
|
+
// ─── Batch verification ─────────────────────────────────────────────────────
|
|
1028
|
+
/**
|
|
1029
|
+
* Revalidate ids from a prior search_memory/store_memory result via hash-compare only —
|
|
1030
|
+
* no BM25, no `search()`. `ids` may mix entry ids and claim ids freely; each resolves
|
|
1031
|
+
* independently and unknown ids fail closed to "unknown", never "fresh" (an id may be
|
|
1032
|
+
* unknown because the entry was purged, or because the symbol it named was renamed —
|
|
1033
|
+
* either way there is nothing left to vouch for it).
|
|
1034
|
+
*/
|
|
1035
|
+
verifyByIds(ids, options = {}) {
|
|
1036
|
+
const claimIndex = buildClaimIndex(this.entries);
|
|
1037
|
+
const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
|
|
1038
|
+
const semanticDiff = options.semanticDiff === true;
|
|
1039
|
+
return ids.map((id) => {
|
|
1040
|
+
const entry = this.entries.get(id);
|
|
1041
|
+
if (entry) {
|
|
1042
|
+
const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(entry, resolveNode, 0, new Set(), semanticDiff);
|
|
1043
|
+
return {
|
|
1044
|
+
id,
|
|
1045
|
+
status: fresh ? 'fresh' : 'stale',
|
|
1046
|
+
...(staleFiles.length > 0 ? { staleFiles } : {}),
|
|
1047
|
+
...(entry.claims ? { claimBreakdown: entry.claims.map((c) => checkClaimFreshness(c, semanticDiff)) } : {}),
|
|
1048
|
+
...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
const claim = claimIndex.get(id);
|
|
1052
|
+
if (claim) {
|
|
1053
|
+
const cf = checkClaimFreshness(claim, semanticDiff);
|
|
1054
|
+
return {
|
|
1055
|
+
id,
|
|
1056
|
+
status: cf.fresh ? 'fresh' : 'stale',
|
|
1057
|
+
...(cf.staleFiles ? { staleFiles: cf.staleFiles } : {}),
|
|
1058
|
+
...(cf.cosmeticChanges ? { cosmeticChanges: cf.cosmeticChanges } : {}),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
return { id, status: 'unknown' };
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
// ─── Blast radius (Fase E) ──────────────────────────────────────────────────
|
|
1065
|
+
/**
|
|
1066
|
+
* What in the Brain would go stale, directly or transitively, if `filePath`/`symbolName`
|
|
1067
|
+
* changes right now. Called by write tools BEFORE they'd otherwise find out (only on the
|
|
1068
|
+
* next search) — see findBlastRadius for the algorithm and cost.
|
|
1069
|
+
*/
|
|
1070
|
+
findBlastRadius(filePath, symbolName) {
|
|
1071
|
+
return findBlastRadius(this.entries, filePath, symbolName);
|
|
1072
|
+
}
|
|
805
1073
|
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
806
1074
|
getStats() {
|
|
807
1075
|
return {
|