@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.
@@ -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":"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"}
1
+ {"version":3,"file":"TheBrainV2.d.ts","sourceRoot":"","sources":["../../../src/subconscious/TheBrainV2.ts"],"names":[],"mappings":"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"}
@@ -14,12 +14,17 @@ 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.findBlastRadius = findBlastRadius;
26
+ exports.checkEntryFreshness = checkEntryFreshness;
27
+ exports.checkClaimFreshness = checkClaimFreshness;
23
28
  exports.getBrain = getBrain;
24
29
  const fs_1 = __importDefault(require("fs"));
25
30
  const path_1 = __importDefault(require("path"));
@@ -294,33 +299,203 @@ function hashSymbolsForFreshness(refs) {
294
299
  for (const ref of refs) {
295
300
  const abs = path_1.default.resolve(ref.filePath);
296
301
  const key = `${abs}::${ref.symbolName}`;
297
- try {
298
- const ctx = (0, SymbolSurgicalContext_1.getSymbolSurgicalContext)(path_1.default.dirname(abs), path_1.default.basename(abs), ref.symbolName);
299
- hashes[key] = crypto_1.default.createHash('sha256').update(ctx.implementation).digest('hex');
300
- }
301
- catch {
302
- hashes[key] = 'MISSING';
303
- }
302
+ hashes[key] = currentSymbolHashPair(abs, ref.symbolName).raw;
304
303
  }
305
304
  return hashes;
306
305
  }
307
- function currentSymbolHash(abs, symbolName) {
306
+ /**
307
+ * Fase D companion to hashSymbolsForFreshness: same symbols, hashed after
308
+ * normalizeForFreshness strips comments/whitespace. Always computed alongside the raw hash
309
+ * (cheap — same parse) regardless of whether semanticDiff is ever turned on, so an entry
310
+ * stored with the flag off is still classifiable later if someone turns it on.
311
+ */
312
+ function hashSymbolsNormalizedForFreshness(refs) {
313
+ const hashes = {};
314
+ for (const ref of refs) {
315
+ const abs = path_1.default.resolve(ref.filePath);
316
+ const key = `${abs}::${ref.symbolName}`;
317
+ hashes[key] = currentSymbolHashPair(abs, ref.symbolName).normalized;
318
+ }
319
+ return hashes;
320
+ }
321
+ function currentSymbolHashPair(abs, symbolName) {
308
322
  try {
309
323
  const ctx = (0, SymbolSurgicalContext_1.getSymbolSurgicalContext)(path_1.default.dirname(abs), path_1.default.basename(abs), symbolName);
310
- return crypto_1.default.createHash('sha256').update(ctx.implementation).digest('hex');
324
+ return {
325
+ raw: crypto_1.default.createHash('sha256').update(ctx.implementation).digest('hex'),
326
+ normalized: crypto_1.default.createHash('sha256').update((0, SymbolSurgicalContext_1.normalizeForFreshness)(ctx.implementation)).digest('hex'),
327
+ };
311
328
  }
312
329
  catch {
313
- return 'MISSING';
330
+ return { raw: 'MISSING', normalized: 'MISSING' };
331
+ }
332
+ }
333
+ /**
334
+ * Looks up a `derivedFrom` id against both entries and claims in one pass. `claimIndex`
335
+ * is built once per outer call (search/verifyByIds) and threaded through recursion,
336
+ * never rebuilt per level — rebuilding per level would turn a bounded-depth walk into
337
+ * O(depth * entries) work for no reason.
338
+ */
339
+ function resolveFreshnessNode(entries, claimIndex, id) {
340
+ const entry = entries.get(id);
341
+ if (entry)
342
+ return { kind: 'entry', entry };
343
+ const claim = claimIndex.get(id);
344
+ if (claim)
345
+ return { kind: 'claim', claim };
346
+ return undefined;
347
+ }
348
+ /** Builds an id -> Claim index across every entry's claims, for resolveFreshnessNode. */
349
+ function buildClaimIndex(entries) {
350
+ const index = new Map();
351
+ for (const entry of entries.values()) {
352
+ if (!entry.claims)
353
+ continue;
354
+ for (const claim of entry.claims)
355
+ index.set(claim.id, claim);
356
+ }
357
+ return index;
358
+ }
359
+ /**
360
+ * Bound on how many `derivedFrom` hops checkEntryFreshness will walk. Procedence chains
361
+ * are meant to be short (a conclusion built on a conclusion, maybe twice); an unbounded
362
+ * walk would turn every search() result into a full graph traversal.
363
+ */
364
+ exports.MAX_DERIVED_DEPTH = 3;
365
+ /**
366
+ * Maps "abs::symbolName" -> the entries/claims whose symbolHashes track that exact symbol.
367
+ * Rebuilt fresh per call (same pattern as buildClaimIndex) — cheap at the 8,000-entry cap
368
+ * (measured ~0.04ms per lookup at full capacity), not worth persisting or maintaining
369
+ * incrementally.
370
+ */
371
+ function buildSymbolIndex(entries) {
372
+ const index = new Map();
373
+ const add = (key, hit) => {
374
+ const list = index.get(key);
375
+ if (list)
376
+ list.push(hit);
377
+ else
378
+ index.set(key, [hit]);
379
+ };
380
+ for (const entry of entries.values()) {
381
+ if (entry.symbolHashes) {
382
+ for (const key of Object.keys(entry.symbolHashes)) {
383
+ add(key, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
384
+ }
385
+ }
386
+ if (entry.claims) {
387
+ for (const claim of entry.claims) {
388
+ if (!claim.symbolHashes)
389
+ continue;
390
+ for (const key of Object.keys(claim.symbolHashes)) {
391
+ add(key, { id: claim.id, kind: 'claim', query: claim.text, outcome: entry.outcome, via: 'direct' });
392
+ }
393
+ }
394
+ }
395
+ }
396
+ return index;
397
+ }
398
+ /**
399
+ * Maps an absolute file path -> ids of entries whole-file-tracked against it (fileHashes),
400
+ * for write tools that don't parse AST and so never know which symbol they touched.
401
+ */
402
+ function buildFileIndex(entries) {
403
+ const index = new Map();
404
+ const add = (key, hit) => {
405
+ const list = index.get(key);
406
+ if (list)
407
+ list.push(hit);
408
+ else
409
+ index.set(key, [hit]);
410
+ };
411
+ for (const entry of entries.values()) {
412
+ if (entry.fileHashes) {
413
+ for (const abs of Object.keys(entry.fileHashes)) {
414
+ add(abs, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
415
+ }
416
+ }
417
+ // A file-level fallback should also catch entries that only track a *symbol* in that
418
+ // file — an untargeted write to the file can plausibly touch it too, and over-reporting
419
+ // here is the safe direction (this only ever produces an advisory note, never blocks).
420
+ if (entry.symbolHashes) {
421
+ const absPaths = new Set(Object.keys(entry.symbolHashes).map((k) => k.substring(0, k.lastIndexOf('::'))));
422
+ for (const abs of absPaths) {
423
+ add(abs, { id: entry.id, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'direct' });
424
+ }
425
+ }
314
426
  }
427
+ return index;
428
+ }
429
+ /** Maps a dependency id -> ids of entries whose derivedFrom includes it (Fase B, reversed). */
430
+ function buildDerivedFromReverseIndex(entries) {
431
+ const index = new Map();
432
+ for (const entry of entries.values()) {
433
+ if (!entry.derivedFrom)
434
+ continue;
435
+ for (const depId of entry.derivedFrom) {
436
+ const list = index.get(depId);
437
+ if (list)
438
+ list.push(entry.id);
439
+ else
440
+ index.set(depId, [entry.id]);
441
+ }
442
+ }
443
+ return index;
315
444
  }
316
445
  /**
317
- * True only if every file/symbol this entry was recorded against still hashes the same.
318
- * A file that also has a tracked symbol is judged by the symbol's hash, not the whole
319
- * file's an edit elsewhere in the same file (a different function, an import, a
320
- * comment) must not invalidate a memory that was only ever about one specific symbol.
446
+ * Everything in the Brain that would go stale (directly or transitively via derivedFrom) if
447
+ * the given symbol/file changes right now computed BEFORE the write happens, so a write
448
+ * tool can warn instead of only ever reporting staleness the next time someone searches.
449
+ * Purely advisory: never blocks, never mutates anything, matches the fail-open/additive
450
+ * invariant every prior phase of proof-carrying context has kept.
321
451
  */
322
- function checkEntryFreshness(entry) {
452
+ function findBlastRadius(entries, filePath, symbolName) {
453
+ const abs = path_1.default.resolve(filePath);
454
+ const symbolIndex = buildSymbolIndex(entries);
455
+ const fileIndex = buildFileIndex(entries);
456
+ const reverseDerived = buildDerivedFromReverseIndex(entries);
457
+ const directHits = symbolName ? symbolIndex.get(`${abs}::${symbolName}`) || [] : fileIndex.get(abs) || [];
458
+ const hitsById = new Map();
459
+ for (const hit of directHits)
460
+ hitsById.set(hit.id, hit);
461
+ // BFS the reverse derivedFrom graph from every direct hit's entry id, bounded to
462
+ // MAX_DERIVED_DEPTH hops — same bound and cycle-guard as checkEntryFreshness's forward
463
+ // walk, just traversed the other direction (dependents, not dependencies).
464
+ let frontier = directHits.map((h) => h.id);
465
+ const visited = new Set(frontier);
466
+ for (let depth = 0; depth < exports.MAX_DERIVED_DEPTH && frontier.length > 0; depth++) {
467
+ const next = [];
468
+ for (const id of frontier) {
469
+ const dependents = reverseDerived.get(id) || [];
470
+ for (const depId of dependents) {
471
+ if (visited.has(depId))
472
+ continue;
473
+ visited.add(depId);
474
+ next.push(depId);
475
+ if (!hitsById.has(depId)) {
476
+ const entry = entries.get(depId);
477
+ if (entry) {
478
+ hitsById.set(depId, { id: depId, kind: 'entry', query: entry.query, outcome: entry.outcome, via: 'derived' });
479
+ }
480
+ }
481
+ }
482
+ }
483
+ frontier = next;
484
+ }
485
+ const hits = Array.from(hitsById.values());
486
+ return { hits, failedHits: hits.filter((h) => h.outcome === 'failed') };
487
+ }
488
+ /**
489
+ * True only if every file/symbol this entry was recorded against still hashes the same,
490
+ * AND (when `derivedFrom` is present and a resolver was passed) every memory this one was
491
+ * built on top of is itself still fresh, walked up to MAX_DERIVED_DEPTH hops. A file that
492
+ * also has a tracked symbol is judged by the symbol's hash, not the whole file's — an edit
493
+ * elsewhere in the same file (a different function, an import, a comment) must not
494
+ * invalidate a memory that was only ever about one specific symbol.
495
+ */
496
+ function checkEntryFreshness(entry, resolveNode, depth = 0, visited = new Set(), semanticDiff = false) {
323
497
  const staleFiles = [];
498
+ const cosmeticChanges = [];
324
499
  const relLabel = (abs) => {
325
500
  const rel = path_1.default.relative(process.cwd(), abs);
326
501
  return rel.startsWith('..') ? abs : rel;
@@ -346,12 +521,41 @@ function checkEntryFreshness(entry) {
346
521
  const sep = key.lastIndexOf('::');
347
522
  const abs = key.substring(0, sep);
348
523
  const symbolName = key.substring(sep + 2);
349
- if (currentSymbolHash(abs, symbolName) !== storedHash) {
350
- staleFiles.push(`${relLabel(abs)}::${symbolName}`);
524
+ const label = `${relLabel(abs)}::${symbolName}`;
525
+ const currentPair = currentSymbolHashPair(abs, symbolName);
526
+ if (currentPair.raw === storedHash)
527
+ continue;
528
+ if (semanticDiff) {
529
+ const storedNormalized = entry.symbolNormalizedHashes?.[key];
530
+ if (storedNormalized && currentPair.normalized === storedNormalized) {
531
+ cosmeticChanges.push(label); // comment/whitespace-only — not stale
532
+ continue;
533
+ }
351
534
  }
535
+ staleFiles.push(label);
352
536
  }
353
537
  }
354
- return { fresh: staleFiles.length === 0, staleFiles };
538
+ if (entry.derivedFrom && entry.derivedFrom.length > 0 && resolveNode && depth < exports.MAX_DERIVED_DEPTH) {
539
+ for (const depId of entry.derivedFrom) {
540
+ if (visited.has(depId))
541
+ continue; // cycle guard — already accounted for in this chain
542
+ const node = resolveNode(depId);
543
+ if (!node) {
544
+ // Can't verify a dependency that no longer resolves — fail closed, same invariant
545
+ // as verify_memory's unknown ids.
546
+ staleFiles.push(`derived:${depId} (unknown)`);
547
+ continue;
548
+ }
549
+ const nextVisited = new Set(visited);
550
+ nextVisited.add(depId);
551
+ const depFresh = node.kind === 'entry'
552
+ ? checkEntryFreshness(node.entry, resolveNode, depth + 1, nextVisited, semanticDiff).fresh
553
+ : checkClaimFreshness(node.claim, semanticDiff).fresh;
554
+ if (!depFresh)
555
+ staleFiles.push(`derived:${depId}`);
556
+ }
557
+ }
558
+ return { fresh: staleFiles.length === 0, staleFiles, ...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}) };
355
559
  }
356
560
  /** Builds the persisted `Claim[]` for an entry from the caller-supplied claim inputs. */
357
561
  function buildClaims(inputs) {
@@ -362,14 +566,16 @@ function buildClaims(inputs) {
362
566
  text: c.text.trim(),
363
567
  fileHashes: c.filePaths && c.filePaths.length > 0 ? hashFilesForFreshness(c.filePaths) : undefined,
364
568
  symbolHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsForFreshness(c.symbols) : undefined,
569
+ symbolNormalizedHashes: c.symbols && c.symbols.length > 0 ? hashSymbolsNormalizedForFreshness(c.symbols) : undefined,
365
570
  }));
366
571
  }
367
572
  /**
368
573
  * Same hash-compare as checkEntryFreshness, scoped to one claim's own evidence — a claim
369
574
  * with no fileHashes/symbolHashes at all is always fresh (nothing tracked to go stale).
370
575
  */
371
- function checkClaimFreshness(claim) {
576
+ function checkClaimFreshness(claim, semanticDiff = false) {
372
577
  const staleFiles = [];
578
+ const cosmeticChanges = [];
373
579
  const relLabel = (abs) => {
374
580
  const rel = path_1.default.relative(process.cwd(), abs);
375
581
  return rel.startsWith('..') ? abs : rel;
@@ -395,12 +601,27 @@ function checkClaimFreshness(claim) {
395
601
  const sep = key.lastIndexOf('::');
396
602
  const abs = key.substring(0, sep);
397
603
  const symbolName = key.substring(sep + 2);
398
- if (currentSymbolHash(abs, symbolName) !== storedHash) {
399
- staleFiles.push(`${relLabel(abs)}::${symbolName}`);
604
+ const label = `${relLabel(abs)}::${symbolName}`;
605
+ const currentPair = currentSymbolHashPair(abs, symbolName);
606
+ if (currentPair.raw === storedHash)
607
+ continue;
608
+ if (semanticDiff) {
609
+ const storedNormalized = claim.symbolNormalizedHashes?.[key];
610
+ if (storedNormalized && currentPair.normalized === storedNormalized) {
611
+ cosmeticChanges.push(label);
612
+ continue;
613
+ }
400
614
  }
615
+ staleFiles.push(label);
401
616
  }
402
617
  }
403
- return { id: claim.id, text: claim.text, fresh: staleFiles.length === 0, ...(staleFiles.length > 0 ? { staleFiles } : {}) };
618
+ return {
619
+ id: claim.id,
620
+ text: claim.text,
621
+ fresh: staleFiles.length === 0,
622
+ ...(staleFiles.length > 0 ? { staleFiles } : {}),
623
+ ...(cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
624
+ };
404
625
  }
405
626
  // ─── TheBrainV2 ──────────────────────────────────────────────────────────────
406
627
  class TheBrainV2 {
@@ -513,7 +734,7 @@ class TheBrainV2 {
513
734
  * Store a query+response pair in the brain.
514
735
  * Returns false if detected as duplicate (>= dupThreshold similarity).
515
736
  */
516
- store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain) {
737
+ store(query, response, provider = 'generic', dupThreshold = 0.92, filePaths, projectId = deriveProjectId(), outcome, symbolRefs, claimInputs, domain, derivedFrom) {
517
738
  // Quick bloom check
518
739
  const queryKey = query.trim().toLowerCase().substring(0, 200);
519
740
  if (this.bloom.has(queryKey)) {
@@ -570,8 +791,10 @@ class TheBrainV2 {
570
791
  fileHashes: filePaths && filePaths.length > 0 ? hashFilesForFreshness(filePaths) : undefined,
571
792
  outcome,
572
793
  symbolHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsForFreshness(symbolRefs) : undefined,
794
+ symbolNormalizedHashes: symbolRefs && symbolRefs.length > 0 ? hashSymbolsNormalizedForFreshness(symbolRefs) : undefined,
573
795
  claims: claimInputs && claimInputs.length > 0 ? buildClaims(claimInputs) : undefined,
574
796
  domain,
797
+ derivedFrom: derivedFrom && derivedFrom.length > 0 ? derivedFrom : undefined,
575
798
  };
576
799
  this.entries.set(id, entry);
577
800
  // Update inverted index
@@ -727,12 +950,15 @@ class TheBrainV2 {
727
950
  });
728
951
  // 6. Sort and filter
729
952
  combined.sort((a, b) => b.similarity - a.similarity);
953
+ const claimIndex = buildClaimIndex(this.entries);
954
+ const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
955
+ const semanticDiff = options.semanticDiff === true;
730
956
  const results = combined
731
957
  .filter(r => r.similarity >= minSimilarity)
732
958
  .slice(0, limit)
733
959
  .map(r => {
734
- const { fresh, staleFiles } = checkEntryFreshness(r.entry);
735
- const claims = r.entry.claims?.map(checkClaimFreshness);
960
+ const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(r.entry, resolveNode, 0, new Set(), semanticDiff);
961
+ const claims = r.entry.claims?.map((c) => checkClaimFreshness(c, semanticDiff));
736
962
  return {
737
963
  id: r.entry.id,
738
964
  query: r.entry.query,
@@ -745,6 +971,7 @@ class TheBrainV2 {
745
971
  ...(r.entry.outcome ? { outcome: r.entry.outcome } : {}),
746
972
  ...(claims && claims.length > 0 ? { claims } : {}),
747
973
  ...(r.entry.domain ? { domain: r.entry.domain } : {}),
974
+ ...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
748
975
  };
749
976
  });
750
977
  // 7. Update hit counters — only for searches a caller actually asked a question with.
@@ -815,6 +1042,52 @@ class TheBrainV2 {
815
1042
  this.scheduleSave();
816
1043
  return { ok: true, message: `Recorded negative feedback on entry "${id}" (demerits: ${entry.demerits}). It will rank lower and be evicted sooner.` };
817
1044
  }
1045
+ // ─── Batch verification ─────────────────────────────────────────────────────
1046
+ /**
1047
+ * Revalidate ids from a prior search_memory/store_memory result via hash-compare only —
1048
+ * no BM25, no `search()`. `ids` may mix entry ids and claim ids freely; each resolves
1049
+ * independently and unknown ids fail closed to "unknown", never "fresh" (an id may be
1050
+ * unknown because the entry was purged, or because the symbol it named was renamed —
1051
+ * either way there is nothing left to vouch for it).
1052
+ */
1053
+ verifyByIds(ids, options = {}) {
1054
+ const claimIndex = buildClaimIndex(this.entries);
1055
+ const resolveNode = (id) => resolveFreshnessNode(this.entries, claimIndex, id);
1056
+ const semanticDiff = options.semanticDiff === true;
1057
+ return ids.map((id) => {
1058
+ const entry = this.entries.get(id);
1059
+ if (entry) {
1060
+ const { fresh, staleFiles, cosmeticChanges } = checkEntryFreshness(entry, resolveNode, 0, new Set(), semanticDiff);
1061
+ return {
1062
+ id,
1063
+ status: fresh ? 'fresh' : 'stale',
1064
+ ...(staleFiles.length > 0 ? { staleFiles } : {}),
1065
+ ...(entry.claims ? { claimBreakdown: entry.claims.map((c) => checkClaimFreshness(c, semanticDiff)) } : {}),
1066
+ ...(cosmeticChanges && cosmeticChanges.length > 0 ? { cosmeticChanges } : {}),
1067
+ };
1068
+ }
1069
+ const claim = claimIndex.get(id);
1070
+ if (claim) {
1071
+ const cf = checkClaimFreshness(claim, semanticDiff);
1072
+ return {
1073
+ id,
1074
+ status: cf.fresh ? 'fresh' : 'stale',
1075
+ ...(cf.staleFiles ? { staleFiles: cf.staleFiles } : {}),
1076
+ ...(cf.cosmeticChanges ? { cosmeticChanges: cf.cosmeticChanges } : {}),
1077
+ };
1078
+ }
1079
+ return { id, status: 'unknown' };
1080
+ });
1081
+ }
1082
+ // ─── Blast radius (Fase E) ──────────────────────────────────────────────────
1083
+ /**
1084
+ * What in the Brain would go stale, directly or transitively, if `filePath`/`symbolName`
1085
+ * changes right now. Called by write tools BEFORE they'd otherwise find out (only on the
1086
+ * next search) — see findBlastRadius for the algorithm and cost.
1087
+ */
1088
+ findBlastRadius(filePath, symbolName) {
1089
+ return findBlastRadius(this.entries, filePath, symbolName);
1090
+ }
818
1091
  // ─── Stats ────────────────────────────────────────────────────────────────
819
1092
  getStats() {
820
1093
  return {