@soleri/core 9.3.1 → 9.5.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/dist/brain/intelligence.d.ts +5 -0
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +115 -26
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/learning-radar.d.ts +3 -3
- package/dist/brain/learning-radar.d.ts.map +1 -1
- package/dist/brain/learning-radar.js +8 -4
- package/dist/brain/learning-radar.js.map +1 -1
- package/dist/control/intent-router.d.ts +2 -2
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +35 -1
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +10 -2
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +23 -1
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/schema.d.ts +1 -1
- package/dist/curator/schema.d.ts.map +1 -1
- package/dist/curator/schema.js +8 -0
- package/dist/curator/schema.js.map +1 -1
- package/dist/domain-packs/types.d.ts +6 -0
- package/dist/domain-packs/types.d.ts.map +1 -1
- package/dist/domain-packs/types.js +1 -0
- package/dist/domain-packs/types.js.map +1 -1
- package/dist/engine/module-manifest.js +3 -3
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts +9 -0
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +59 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/facades/types.d.ts +5 -1
- package/dist/facades/types.d.ts.map +1 -1
- package/dist/facades/types.js.map +1 -1
- package/dist/hooks/candidate-scorer.d.ts +28 -0
- package/dist/hooks/candidate-scorer.d.ts.map +1 -0
- package/dist/hooks/candidate-scorer.js +20 -0
- package/dist/hooks/candidate-scorer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operator/operator-context-store.d.ts +54 -0
- package/dist/operator/operator-context-store.d.ts.map +1 -0
- package/dist/operator/operator-context-store.js +434 -0
- package/dist/operator/operator-context-store.js.map +1 -0
- package/dist/operator/operator-context-types.d.ts +101 -0
- package/dist/operator/operator-context-types.d.ts.map +1 -0
- package/dist/operator/operator-context-types.js +27 -0
- package/dist/operator/operator-context-types.js.map +1 -0
- package/dist/packs/index.d.ts +2 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +3 -0
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/types.d.ts +8 -2
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +6 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts +12 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +54 -16
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +6 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +21 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +62 -3
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/task-complexity-assessor.d.ts.map +1 -1
- package/dist/planning/task-complexity-assessor.js.map +1 -1
- package/dist/plugins/types.d.ts +18 -18
- package/dist/runtime/admin-ops.d.ts +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +100 -3
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +19 -9
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +35 -7
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +4 -2
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +8 -2
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/curator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/curator-facade.js +13 -0
- package/dist/runtime/facades/curator-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +10 -12
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +36 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +20 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +71 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/plan-feedback-helper.d.ts +21 -0
- package/dist/runtime/plan-feedback-helper.d.ts.map +1 -0
- package/dist/runtime/plan-feedback-helper.js +52 -0
- package/dist/runtime/plan-feedback-helper.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +73 -34
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +9 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +13 -7
- package/dist/skills/sync-skills.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain-intelligence.test.ts +30 -0
- package/src/brain/brain.ts +120 -46
- package/src/brain/extraction-quality.test.ts +323 -0
- package/src/brain/intelligence.ts +175 -64
- package/src/brain/learning-radar.ts +8 -5
- package/src/brain/second-brain-features.test.ts +1 -1
- package/src/chat/agent-loop.ts +1 -1
- package/src/chat/notifications.ts +4 -0
- package/src/control/intent-router.test.ts +73 -3
- package/src/control/intent-router.ts +48 -9
- package/src/control/types.ts +13 -2
- package/src/curator/curator.test.ts +92 -0
- package/src/curator/curator.ts +162 -18
- package/src/curator/schema.ts +8 -0
- package/src/domain-packs/types.ts +8 -0
- package/src/engine/module-manifest.test.ts +8 -2
- package/src/engine/module-manifest.ts +3 -3
- package/src/engine/register-engine.test.ts +73 -1
- package/src/engine/register-engine.ts +61 -1
- package/src/facades/types.ts +5 -0
- package/src/hooks/candidate-scorer.test.ts +76 -0
- package/src/hooks/candidate-scorer.ts +39 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +24 -0
- package/src/llm/llm-client.ts +1 -0
- package/src/operator/operator-context-store.test.ts +698 -0
- package/src/operator/operator-context-store.ts +569 -0
- package/src/operator/operator-context-types.ts +139 -0
- package/src/packs/index.ts +3 -1
- package/src/packs/lockfile.ts +3 -0
- package/src/packs/types.ts +9 -0
- package/src/persistence/sqlite-provider.ts +1 -0
- package/src/planning/github-projection.ts +48 -44
- package/src/planning/plan-lifecycle.ts +93 -22
- package/src/planning/planner-types.ts +6 -0
- package/src/planning/planner.ts +74 -4
- package/src/planning/task-complexity-assessor.test.ts +6 -2
- package/src/planning/task-complexity-assessor.ts +1 -4
- package/src/queue/pipeline-runner.ts +4 -0
- package/src/runtime/admin-ops.test.ts +139 -6
- package/src/runtime/admin-ops.ts +104 -3
- package/src/runtime/admin-setup-ops.ts +30 -10
- package/src/runtime/capture-ops.test.ts +84 -0
- package/src/runtime/capture-ops.ts +35 -7
- package/src/runtime/curator-extra-ops.test.ts +7 -0
- package/src/runtime/curator-extra-ops.ts +10 -1
- package/src/runtime/facades/admin-facade.test.ts +1 -1
- package/src/runtime/facades/brain-facade.ts +6 -3
- package/src/runtime/facades/control-facade.ts +10 -2
- package/src/runtime/facades/curator-facade.test.ts +7 -0
- package/src/runtime/facades/curator-facade.ts +18 -0
- package/src/runtime/facades/memory-facade.test.ts +14 -12
- package/src/runtime/facades/memory-facade.ts +197 -12
- package/src/runtime/facades/orchestrate-facade.ts +33 -1
- package/src/runtime/facades/plan-facade.test.ts +213 -0
- package/src/runtime/facades/plan-facade.ts +23 -4
- package/src/runtime/orchestrate-ops.test.ts +202 -2
- package/src/runtime/orchestrate-ops.ts +88 -7
- package/src/runtime/plan-feedback-helper.test.ts +173 -0
- package/src/runtime/plan-feedback-helper.ts +63 -0
- package/src/runtime/planning-extra-ops.test.ts +43 -1
- package/src/runtime/planning-extra-ops.ts +96 -33
- package/src/runtime/runtime.test.ts +50 -2
- package/src/runtime/runtime.ts +117 -89
- package/src/runtime/session-briefing.test.ts +1 -0
- package/src/runtime/session-briefing.ts +10 -1
- package/src/runtime/shutdown-registry.test.ts +151 -0
- package/src/runtime/shutdown-registry.ts +85 -0
- package/src/runtime/types.ts +7 -1
- package/src/skills/sync-skills.ts +14 -7
- package/src/transport/http-server.ts +50 -3
- package/src/transport/ws-server.ts +8 -0
- package/src/vault/linking.test.ts +12 -0
- package/src/vault/linking.ts +90 -44
- package/src/vault/vault-maintenance.ts +11 -18
- package/src/vault/vault-memories.ts +21 -13
- package/src/vault/vault-schema.ts +21 -0
- package/src/vault/vault.ts +8 -3
- package/vitest.config.ts +1 -0
package/src/brain/brain.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
cosineSimilarity,
|
|
11
11
|
jaccardSimilarity,
|
|
12
12
|
} from '../text/similarity.js';
|
|
13
|
+
import { rowToEntry } from '../vault/vault-entries.js';
|
|
13
14
|
import type {
|
|
14
15
|
ScoringWeights,
|
|
15
16
|
ScoreBreakdown,
|
|
@@ -58,7 +59,7 @@ export class Brain {
|
|
|
58
59
|
constructor(vault: Vault, vaultManager?: VaultManager) {
|
|
59
60
|
this.vault = vault;
|
|
60
61
|
this.vaultManager = vaultManager;
|
|
61
|
-
this.
|
|
62
|
+
this.loadVocabularyFromDb();
|
|
62
63
|
this.recomputeWeights();
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -98,9 +99,15 @@ export class Brain {
|
|
|
98
99
|
const queryDomain = options?.domain;
|
|
99
100
|
const now = Math.floor(Date.now() / 1000);
|
|
100
101
|
|
|
102
|
+
// Compute queryVec once for all entries (was previously recomputed per entry in scoreEntry)
|
|
103
|
+
const queryVec =
|
|
104
|
+
this.vocabulary.size > 0 && queryTokens.length > 0
|
|
105
|
+
? calculateTfIdf(queryTokens, this.vocabulary)
|
|
106
|
+
: null;
|
|
107
|
+
|
|
101
108
|
const ranked = rawResults.map((result) => {
|
|
102
109
|
const entry = result.entry;
|
|
103
|
-
const breakdown = this.scoreEntry(entry, queryTokens, queryTags, queryDomain, now);
|
|
110
|
+
const breakdown = this.scoreEntry(entry, queryTokens, queryTags, queryDomain, now, queryVec);
|
|
104
111
|
return { entry, score: breakdown.total, breakdown };
|
|
105
112
|
});
|
|
106
113
|
|
|
@@ -142,14 +149,17 @@ export class Brain {
|
|
|
142
149
|
/**
|
|
143
150
|
* Two-pass retrieval — Pass 2: Load.
|
|
144
151
|
* Returns full entries for specific IDs (from a previous scan).
|
|
152
|
+
* Uses a single WHERE id IN (...) query instead of per-ID lookups.
|
|
145
153
|
*/
|
|
146
154
|
loadEntries(ids: string[]): IntelligenceEntry[] {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
if (ids.length === 0) return [];
|
|
156
|
+
const provider = this.vault.getProvider();
|
|
157
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
158
|
+
const rows = provider.all<Record<string, unknown>>(
|
|
159
|
+
`SELECT * FROM entries WHERE id IN (${placeholders})`,
|
|
160
|
+
ids,
|
|
161
|
+
);
|
|
162
|
+
return rows.map(rowToEntry);
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
/** Rough token estimate for an entry (chars / 4). */
|
|
@@ -345,22 +355,57 @@ export class Brain {
|
|
|
345
355
|
}
|
|
346
356
|
|
|
347
357
|
rebuildVocabulary(): void {
|
|
358
|
+
const BATCH_SIZE = 100;
|
|
359
|
+
const termDocFreq = new Map<string, number>();
|
|
360
|
+
let docCount = 0;
|
|
361
|
+
|
|
362
|
+
// Helper to process a batch of entries into the term-doc-frequency map
|
|
363
|
+
const processBatch = (entries: IntelligenceEntry[]): void => {
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
const text = [
|
|
366
|
+
entry.title,
|
|
367
|
+
entry.description,
|
|
368
|
+
entry.context ?? '',
|
|
369
|
+
entry.tags.join(' '),
|
|
370
|
+
].join(' ');
|
|
371
|
+
const tokens = new Set(tokenize(text));
|
|
372
|
+
for (const token of tokens) {
|
|
373
|
+
termDocFreq.set(token, (termDocFreq.get(token) ?? 0) + 1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
docCount += entries.length;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Helper to iterate a single vault in batches
|
|
380
|
+
const iterateVault = (vault: Vault, seen: Set<string> | null): void => {
|
|
381
|
+
let offset = 0;
|
|
382
|
+
while (true) {
|
|
383
|
+
const batch = vault.list({ limit: BATCH_SIZE, offset });
|
|
384
|
+
if (batch.length === 0) break;
|
|
385
|
+
if (seen) {
|
|
386
|
+
const unique = batch.filter((e) => {
|
|
387
|
+
if (seen.has(e.id)) return false;
|
|
388
|
+
seen.add(e.id);
|
|
389
|
+
return true;
|
|
390
|
+
});
|
|
391
|
+
processBatch(unique);
|
|
392
|
+
} else {
|
|
393
|
+
processBatch(batch);
|
|
394
|
+
}
|
|
395
|
+
if (batch.length < BATCH_SIZE) break;
|
|
396
|
+
offset += BATCH_SIZE;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
348
400
|
// Collect entries from all connected sources when VaultManager is available
|
|
349
|
-
let entries: IntelligenceEntry[];
|
|
350
401
|
if (this.vaultManager) {
|
|
351
402
|
const seen = new Set<string>();
|
|
352
|
-
entries = [];
|
|
353
403
|
// Gather entries from all tier vaults and connected sources via manager
|
|
354
404
|
for (const tierInfo of this.vaultManager.listTiers()) {
|
|
355
405
|
if (!tierInfo.connected) continue;
|
|
356
406
|
try {
|
|
357
407
|
const tierVault = this.vaultManager.getTier(tierInfo.tier);
|
|
358
|
-
|
|
359
|
-
if (!seen.has(e.id)) {
|
|
360
|
-
seen.add(e.id);
|
|
361
|
-
entries.push(e);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
408
|
+
iterateVault(tierVault, seen);
|
|
364
409
|
} catch {
|
|
365
410
|
/* tier not connected */
|
|
366
411
|
}
|
|
@@ -369,44 +414,28 @@ export class Brain {
|
|
|
369
414
|
const cv = this.vaultManager.getConnected(name);
|
|
370
415
|
if (!cv) continue;
|
|
371
416
|
try {
|
|
372
|
-
|
|
373
|
-
if (!seen.has(e.id)) {
|
|
374
|
-
seen.add(e.id);
|
|
375
|
-
entries.push(e);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
417
|
+
iterateVault(cv.vault, seen);
|
|
378
418
|
} catch {
|
|
379
419
|
/* source not accessible */
|
|
380
420
|
}
|
|
381
421
|
}
|
|
382
422
|
} else {
|
|
383
|
-
|
|
423
|
+
iterateVault(this.vault, null);
|
|
384
424
|
}
|
|
385
|
-
|
|
425
|
+
|
|
386
426
|
if (docCount === 0) {
|
|
387
427
|
this.vocabulary.clear();
|
|
388
|
-
this.
|
|
428
|
+
this.persistVocabularyFull();
|
|
389
429
|
return;
|
|
390
430
|
}
|
|
391
431
|
|
|
392
|
-
const termDocFreq = new Map<string, number>();
|
|
393
|
-
for (const entry of entries) {
|
|
394
|
-
const text = [entry.title, entry.description, entry.context ?? '', entry.tags.join(' ')].join(
|
|
395
|
-
' ',
|
|
396
|
-
);
|
|
397
|
-
const tokens = new Set(tokenize(text));
|
|
398
|
-
for (const token of tokens) {
|
|
399
|
-
termDocFreq.set(token, (termDocFreq.get(token) ?? 0) + 1);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
432
|
this.vocabulary.clear();
|
|
404
433
|
for (const [term, df] of termDocFreq) {
|
|
405
434
|
const idf = Math.log((docCount + 1) / (df + 1)) + 1;
|
|
406
435
|
this.vocabulary.set(term, idf);
|
|
407
436
|
}
|
|
408
437
|
|
|
409
|
-
this.
|
|
438
|
+
this.persistVocabularyFull();
|
|
410
439
|
}
|
|
411
440
|
|
|
412
441
|
getStats(): BrainStats {
|
|
@@ -469,11 +498,12 @@ export class Brain {
|
|
|
469
498
|
queryTags: string[],
|
|
470
499
|
queryDomain: string | undefined,
|
|
471
500
|
now: number,
|
|
501
|
+
queryVec: Map<string, number> | null = null,
|
|
472
502
|
): ScoreBreakdown {
|
|
473
503
|
const w = this.weights;
|
|
474
504
|
|
|
475
505
|
let semantic = 0;
|
|
476
|
-
if (
|
|
506
|
+
if (queryVec && queryVec.size > 0) {
|
|
477
507
|
const entryText = [
|
|
478
508
|
entry.title,
|
|
479
509
|
entry.description,
|
|
@@ -481,7 +511,6 @@ export class Brain {
|
|
|
481
511
|
entry.tags.join(' '),
|
|
482
512
|
].join(' ');
|
|
483
513
|
const entryTokens = tokenize(entryText);
|
|
484
|
-
const queryVec = calculateTfIdf(queryTokens, this.vocabulary);
|
|
485
514
|
const entryVec = calculateTfIdf(entryTokens, this.vocabulary);
|
|
486
515
|
semantic = cosineSimilarity(queryVec, entryVec);
|
|
487
516
|
}
|
|
@@ -572,27 +601,72 @@ export class Brain {
|
|
|
572
601
|
const tokens = new Set(tokenize(text));
|
|
573
602
|
const totalDocs = this.vault.stats().totalEntries;
|
|
574
603
|
|
|
604
|
+
const changedTerms = new Map<string, number>();
|
|
575
605
|
for (const token of tokens) {
|
|
576
606
|
const currentDocCount = this.vocabulary.has(token)
|
|
577
607
|
? Math.round(totalDocs / Math.exp(this.vocabulary.get(token)! - 1)) + 1
|
|
578
608
|
: 1;
|
|
579
609
|
const newIdf = Math.log((totalDocs + 1) / (currentDocCount + 1)) + 1;
|
|
580
610
|
this.vocabulary.set(token, newIdf);
|
|
611
|
+
changedTerms.set(token, newIdf);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
this.persistVocabularyPartial(changedTerms);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private persistVocabularyPartial(terms: Map<string, number>): void {
|
|
618
|
+
if (terms.size === 0) return;
|
|
619
|
+
const db = this.vault.getDb();
|
|
620
|
+
const upsert = db.prepare(
|
|
621
|
+
'INSERT OR REPLACE INTO brain_vocabulary (term, idf, doc_count, updated_at) VALUES (?, ?, 1, unixepoch())',
|
|
622
|
+
);
|
|
623
|
+
const tx = db.transaction(() => {
|
|
624
|
+
for (const [term, idf] of terms) {
|
|
625
|
+
upsert.run(term, idf);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
tx();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Fast startup path: load vocabulary from the brain_vocabulary table.
|
|
633
|
+
* If the table is empty (first boot or after corruption), trigger a full rebuild.
|
|
634
|
+
*/
|
|
635
|
+
private loadVocabularyFromDb(): void {
|
|
636
|
+
const db = this.vault.getDb();
|
|
637
|
+
const rows = db.prepare('SELECT term, idf FROM brain_vocabulary').all() as Array<{
|
|
638
|
+
term: string;
|
|
639
|
+
idf: number;
|
|
640
|
+
}>;
|
|
641
|
+
|
|
642
|
+
if (rows.length === 0) {
|
|
643
|
+
this.rebuildVocabulary();
|
|
644
|
+
return;
|
|
581
645
|
}
|
|
582
646
|
|
|
583
|
-
this.
|
|
647
|
+
this.vocabulary.clear();
|
|
648
|
+
for (const row of rows) {
|
|
649
|
+
this.vocabulary.set(row.term, row.idf);
|
|
650
|
+
}
|
|
584
651
|
}
|
|
585
652
|
|
|
586
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Full persist: DELETE all rows then re-INSERT. Used by rebuildVocabulary() which
|
|
655
|
+
* replaces the entire vocabulary and needs to remove stale terms.
|
|
656
|
+
*/
|
|
657
|
+
private persistVocabularyFull(): void {
|
|
587
658
|
const db = this.vault.getDb();
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
659
|
+
if (this.vocabulary.size === 0) {
|
|
660
|
+
db.prepare('DELETE FROM brain_vocabulary').run();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const upsert = db.prepare(
|
|
664
|
+
'INSERT OR REPLACE INTO brain_vocabulary (term, idf, doc_count, updated_at) VALUES (?, ?, 1, unixepoch())',
|
|
592
665
|
);
|
|
593
666
|
const tx = db.transaction(() => {
|
|
667
|
+
db.prepare('DELETE FROM brain_vocabulary').run();
|
|
594
668
|
for (const [term, idf] of this.vocabulary) {
|
|
595
|
-
|
|
669
|
+
upsert.run(term, idf);
|
|
596
670
|
}
|
|
597
671
|
});
|
|
598
672
|
tx();
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD tests for brain extraction quality (issue #359).
|
|
3
|
+
*
|
|
4
|
+
* These tests define the DESIRED behavior of extractKnowledge().
|
|
5
|
+
* They are expected to FAIL against the current implementation.
|
|
6
|
+
* Implementation fixes come in issues #360-#366.
|
|
7
|
+
*
|
|
8
|
+
* What's wrong today:
|
|
9
|
+
* - plan_completed rule produces generic "Successful plan: {id}" titles
|
|
10
|
+
* - Extraction rules never read session.context (objective, scope, decisions)
|
|
11
|
+
* - No dedup: same rule + sessionId can produce duplicate proposals
|
|
12
|
+
* - long_session rule fires with low-value noise (to be removed in #360)
|
|
13
|
+
* - No drift_detected rule exists yet (to be added in #366)
|
|
14
|
+
* - Confidence is not adjusted based on context richness
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
18
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
22
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
23
|
+
|
|
24
|
+
describe('Extraction Quality', () => {
|
|
25
|
+
let runtime: AgentRuntime;
|
|
26
|
+
let plannerDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
plannerDir = join(tmpdir(), 'extraction-quality-test-' + Date.now());
|
|
30
|
+
mkdirSync(plannerDir, { recursive: true });
|
|
31
|
+
runtime = createAgentRuntime({
|
|
32
|
+
agentId: 'test-extraction-quality',
|
|
33
|
+
vaultPath: ':memory:',
|
|
34
|
+
plansPath: join(plannerDir, 'plans.json'),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
runtime.close();
|
|
40
|
+
rmSync(plannerDir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ─── Helper ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function createSessionWithContext(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
context: string,
|
|
48
|
+
overrides: {
|
|
49
|
+
planId?: string;
|
|
50
|
+
planOutcome?: string;
|
|
51
|
+
toolsUsed?: string[];
|
|
52
|
+
filesModified?: string[];
|
|
53
|
+
domain?: string;
|
|
54
|
+
} = {},
|
|
55
|
+
) {
|
|
56
|
+
runtime.brainIntelligence.lifecycle({
|
|
57
|
+
action: 'start',
|
|
58
|
+
sessionId,
|
|
59
|
+
domain: overrides.domain ?? 'testing',
|
|
60
|
+
context,
|
|
61
|
+
toolsUsed: overrides.toolsUsed ?? [],
|
|
62
|
+
filesModified: overrides.filesModified ?? [],
|
|
63
|
+
planId: overrides.planId,
|
|
64
|
+
});
|
|
65
|
+
runtime.brainIntelligence.lifecycle({
|
|
66
|
+
action: 'end',
|
|
67
|
+
sessionId,
|
|
68
|
+
planOutcome: overrides.planOutcome,
|
|
69
|
+
toolsUsed: overrides.toolsUsed,
|
|
70
|
+
filesModified: overrides.filesModified,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── 1. Actionable titles from rich context ───────────────────────
|
|
75
|
+
|
|
76
|
+
describe('actionable proposals from session context', () => {
|
|
77
|
+
it('should use session context objective in plan_completed proposal title', () => {
|
|
78
|
+
const richContext = JSON.stringify({
|
|
79
|
+
objective: 'Add OAuth2 authentication to the API gateway',
|
|
80
|
+
scope: { included: ['auth module', 'gateway routes'], excluded: ['frontend'] },
|
|
81
|
+
decisions: ['Use passport.js for OAuth2 strategy'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
createSessionWithContext('rich-ctx-1', richContext, {
|
|
85
|
+
planId: 'plan-oauth',
|
|
86
|
+
planOutcome: 'completed',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-1');
|
|
90
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
91
|
+
|
|
92
|
+
expect(planProposal).toBeDefined();
|
|
93
|
+
// The title should reference the objective, not just the plan ID
|
|
94
|
+
expect(planProposal!.title).not.toContain('Successful plan:');
|
|
95
|
+
expect(planProposal!.title.toLowerCase()).toContain('oauth');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use session context objective in plan_abandoned proposal title', () => {
|
|
99
|
+
const richContext = JSON.stringify({
|
|
100
|
+
objective: 'Migrate database from Postgres to CockroachDB',
|
|
101
|
+
scope: { included: ['migration scripts', 'connection pool'] },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
createSessionWithContext('rich-ctx-2', richContext, {
|
|
105
|
+
planId: 'plan-migrate',
|
|
106
|
+
planOutcome: 'abandoned',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-2');
|
|
110
|
+
const abandonedProposal = result.proposals.find((p) => p.rule === 'plan_abandoned');
|
|
111
|
+
|
|
112
|
+
expect(abandonedProposal).toBeDefined();
|
|
113
|
+
// The title should reference what was abandoned, not just the plan ID
|
|
114
|
+
expect(abandonedProposal!.title).not.toContain('Abandoned plan:');
|
|
115
|
+
expect(abandonedProposal!.title.toLowerCase()).toContain('migrate');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should include scope details in proposal description when context has scope', () => {
|
|
119
|
+
const richContext = JSON.stringify({
|
|
120
|
+
objective: 'Refactor the billing reconciliation module',
|
|
121
|
+
scope: {
|
|
122
|
+
included: ['stripe-adapter', 'webhook-handler', 'ledger-service'],
|
|
123
|
+
excluded: ['billing-ui', 'invoice-generator'],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
createSessionWithContext('rich-ctx-3', richContext, {
|
|
128
|
+
planId: 'plan-abc',
|
|
129
|
+
planOutcome: 'completed',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = runtime.brainIntelligence.extractKnowledge('rich-ctx-3');
|
|
133
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
134
|
+
|
|
135
|
+
expect(planProposal).toBeDefined();
|
|
136
|
+
// Description should mention scope components, not just "can be reused for similar tasks"
|
|
137
|
+
expect(planProposal!.description.toLowerCase()).toMatch(/stripe|webhook|ledger/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── 2. Dedup: same rule + sessionId = 1 proposal ────────────────
|
|
142
|
+
|
|
143
|
+
describe('proposal deduplication', () => {
|
|
144
|
+
it('should produce exactly 1 proposal per rule per session', () => {
|
|
145
|
+
createSessionWithContext('dedup-1', 'some context', {
|
|
146
|
+
planId: 'plan-dedup',
|
|
147
|
+
planOutcome: 'completed',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Extract twice on same session (reset extractedAt in between)
|
|
151
|
+
runtime.brainIntelligence.extractKnowledge('dedup-1');
|
|
152
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'dedup-1' });
|
|
153
|
+
runtime.brainIntelligence.extractKnowledge('dedup-1');
|
|
154
|
+
|
|
155
|
+
// Query all proposals for this session
|
|
156
|
+
const proposals = runtime.brainIntelligence.getProposals({
|
|
157
|
+
sessionId: 'dedup-1',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Count proposals per rule
|
|
161
|
+
const ruleCounts = new Map<string, number>();
|
|
162
|
+
for (const p of proposals) {
|
|
163
|
+
ruleCounts.set(p.rule, (ruleCounts.get(p.rule) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Each rule should appear at most once per session
|
|
167
|
+
for (const [rule, count] of ruleCounts) {
|
|
168
|
+
expect(count, `rule "${rule}" should appear exactly once`).toBe(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── 3. long_session rule should not fire ─────────────────────────
|
|
174
|
+
|
|
175
|
+
describe('long_session rule removal', () => {
|
|
176
|
+
it('should NOT produce a long_session proposal', () => {
|
|
177
|
+
// Create session manually with backdated start time to simulate >30 min duration.
|
|
178
|
+
// SQLite datetime('now') uses 'YYYY-MM-DD HH:MM:SS' format (no T/Z), so match that.
|
|
179
|
+
const d = new Date(Date.now() - 35 * 60 * 1000);
|
|
180
|
+
const thirtyFiveMinAgo = d
|
|
181
|
+
.toISOString()
|
|
182
|
+
.replace('T', ' ')
|
|
183
|
+
.replace(/\.\d{3}Z$/, '');
|
|
184
|
+
const provider = runtime.vault.getProvider();
|
|
185
|
+
|
|
186
|
+
// Insert session directly with backdated started_at so auto-extract sees the long duration
|
|
187
|
+
provider.run(
|
|
188
|
+
`INSERT INTO brain_sessions (id, started_at, domain, context, tools_used, files_modified)
|
|
189
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
190
|
+
['long-sess-1', thirtyFiveMinAgo, 'testing', null, '[]', '[]'],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// End the session — this sets ended_at to now(), creating a >30 min gap
|
|
194
|
+
runtime.brainIntelligence.lifecycle({
|
|
195
|
+
action: 'end',
|
|
196
|
+
sessionId: 'long-sess-1',
|
|
197
|
+
toolsUsed: ['search'], // need at least 1 tool for auto-extract gate
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Reset extracted_at so we can manually extract and inspect
|
|
201
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'long-sess-1' });
|
|
202
|
+
|
|
203
|
+
const result = runtime.brainIntelligence.extractKnowledge('long-sess-1');
|
|
204
|
+
|
|
205
|
+
// long_session rule should no longer exist (removal in #360)
|
|
206
|
+
expect(result.rulesApplied).not.toContain('long_session');
|
|
207
|
+
expect(result.proposals.find((p) => p.rule === 'long_session')).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── 4. drift_detected rule ───────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe('drift_detected rule', () => {
|
|
214
|
+
it('should fire when session context contains drift indicators', () => {
|
|
215
|
+
const contextWithDrift = JSON.stringify({
|
|
216
|
+
objective: 'Implement caching layer for API responses',
|
|
217
|
+
drift: {
|
|
218
|
+
items: [
|
|
219
|
+
{
|
|
220
|
+
type: 'added',
|
|
221
|
+
description: 'Added Redis fallback to in-memory cache',
|
|
222
|
+
impact: 'medium',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'skipped',
|
|
226
|
+
description: 'Skipped cache invalidation webhooks',
|
|
227
|
+
impact: 'high',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
accuracyScore: 65,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
createSessionWithContext('drift-1', contextWithDrift, {
|
|
235
|
+
planId: 'plan-cache',
|
|
236
|
+
planOutcome: 'completed',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = runtime.brainIntelligence.extractKnowledge('drift-1');
|
|
240
|
+
|
|
241
|
+
// A drift_detected rule should fire (to be added in #366)
|
|
242
|
+
expect(result.rulesApplied).toContain('drift_detected');
|
|
243
|
+
const driftProposal = result.proposals.find((p) => p.rule === 'drift_detected');
|
|
244
|
+
expect(driftProposal).toBeDefined();
|
|
245
|
+
expect(driftProposal!.type).toBe('anti-pattern');
|
|
246
|
+
expect(driftProposal!.description.toLowerCase()).toMatch(/drift|skipped|deviation/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should NOT fire drift_detected when context has no drift', () => {
|
|
250
|
+
const cleanContext = JSON.stringify({
|
|
251
|
+
objective: 'Add unit tests for auth module',
|
|
252
|
+
scope: { included: ['auth'] },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
createSessionWithContext('no-drift-1', cleanContext, {
|
|
256
|
+
planId: 'plan-tests',
|
|
257
|
+
planOutcome: 'completed',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = runtime.brainIntelligence.extractKnowledge('no-drift-1');
|
|
261
|
+
expect(result.rulesApplied).not.toContain('drift_detected');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── 5. Context richness affects confidence ───────────────────────
|
|
266
|
+
|
|
267
|
+
describe('confidence based on context richness', () => {
|
|
268
|
+
it('should assign higher confidence to proposals with rich session context', () => {
|
|
269
|
+
// Session with rich context
|
|
270
|
+
const richContext = JSON.stringify({
|
|
271
|
+
objective: 'Build notification service',
|
|
272
|
+
scope: { included: ['notifications', 'email-adapter', 'push-adapter'] },
|
|
273
|
+
decisions: ['Use event-driven architecture', 'SNS for push notifications'],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
createSessionWithContext('conf-rich', richContext, {
|
|
277
|
+
planId: 'plan-notify-rich',
|
|
278
|
+
planOutcome: 'completed',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Session with no context
|
|
282
|
+
createSessionWithContext('conf-empty', '', {
|
|
283
|
+
planId: 'plan-notify-empty',
|
|
284
|
+
planOutcome: 'completed',
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const richResult = runtime.brainIntelligence.extractKnowledge('conf-rich');
|
|
288
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'conf-empty' });
|
|
289
|
+
const emptyResult = runtime.brainIntelligence.extractKnowledge('conf-empty');
|
|
290
|
+
|
|
291
|
+
const richPlanProposal = richResult.proposals.find((p) => p.rule === 'plan_completed');
|
|
292
|
+
const emptyPlanProposal = emptyResult.proposals.find((p) => p.rule === 'plan_completed');
|
|
293
|
+
|
|
294
|
+
expect(richPlanProposal).toBeDefined();
|
|
295
|
+
expect(emptyPlanProposal).toBeDefined();
|
|
296
|
+
|
|
297
|
+
// Rich context should produce higher confidence than empty context
|
|
298
|
+
expect(richPlanProposal!.confidence).toBeGreaterThan(emptyPlanProposal!.confidence);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should assign lower confidence when session context is null', () => {
|
|
302
|
+
// Session with null context (no context field at all)
|
|
303
|
+
runtime.brainIntelligence.lifecycle({
|
|
304
|
+
action: 'start',
|
|
305
|
+
sessionId: 'conf-null',
|
|
306
|
+
planId: 'plan-null-ctx',
|
|
307
|
+
});
|
|
308
|
+
runtime.brainIntelligence.lifecycle({
|
|
309
|
+
action: 'end',
|
|
310
|
+
sessionId: 'conf-null',
|
|
311
|
+
planOutcome: 'completed',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
runtime.brainIntelligence.resetExtracted({ sessionId: 'conf-null' });
|
|
315
|
+
const result = runtime.brainIntelligence.extractKnowledge('conf-null');
|
|
316
|
+
const planProposal = result.proposals.find((p) => p.rule === 'plan_completed');
|
|
317
|
+
|
|
318
|
+
expect(planProposal).toBeDefined();
|
|
319
|
+
// Without context, confidence should be below the current hardcoded 0.65
|
|
320
|
+
expect(planProposal!.confidence).toBeLessThan(0.65);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|