@soleri/core 2.1.0 → 2.4.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.
Files changed (207) hide show
  1. package/dist/brain/brain.d.ts +3 -1
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +60 -4
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +36 -1
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +119 -14
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +32 -0
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/control/identity-manager.d.ts +22 -0
  12. package/dist/control/identity-manager.d.ts.map +1 -0
  13. package/dist/control/identity-manager.js +233 -0
  14. package/dist/control/identity-manager.js.map +1 -0
  15. package/dist/control/intent-router.d.ts +32 -0
  16. package/dist/control/intent-router.d.ts.map +1 -0
  17. package/dist/control/intent-router.js +242 -0
  18. package/dist/control/intent-router.js.map +1 -0
  19. package/dist/control/types.d.ts +68 -0
  20. package/dist/control/types.d.ts.map +1 -0
  21. package/dist/control/types.js +9 -0
  22. package/dist/control/types.js.map +1 -0
  23. package/dist/curator/curator.d.ts +29 -0
  24. package/dist/curator/curator.d.ts.map +1 -1
  25. package/dist/curator/curator.js +135 -0
  26. package/dist/curator/curator.js.map +1 -1
  27. package/dist/facades/types.d.ts +1 -1
  28. package/dist/governance/governance.d.ts +42 -0
  29. package/dist/governance/governance.d.ts.map +1 -0
  30. package/dist/governance/governance.js +488 -0
  31. package/dist/governance/governance.js.map +1 -0
  32. package/dist/governance/index.d.ts +3 -0
  33. package/dist/governance/index.d.ts.map +1 -0
  34. package/dist/governance/index.js +2 -0
  35. package/dist/governance/index.js.map +1 -0
  36. package/dist/governance/types.d.ts +102 -0
  37. package/dist/governance/types.d.ts.map +1 -0
  38. package/dist/governance/types.js +3 -0
  39. package/dist/governance/types.js.map +1 -0
  40. package/dist/index.d.ts +32 -3
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +29 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/logging/logger.d.ts +37 -0
  45. package/dist/logging/logger.d.ts.map +1 -0
  46. package/dist/logging/logger.js +145 -0
  47. package/dist/logging/logger.js.map +1 -0
  48. package/dist/logging/types.d.ts +19 -0
  49. package/dist/logging/types.d.ts.map +1 -0
  50. package/dist/logging/types.js +2 -0
  51. package/dist/logging/types.js.map +1 -0
  52. package/dist/loop/loop-manager.d.ts +49 -0
  53. package/dist/loop/loop-manager.d.ts.map +1 -0
  54. package/dist/loop/loop-manager.js +105 -0
  55. package/dist/loop/loop-manager.js.map +1 -0
  56. package/dist/loop/types.d.ts +35 -0
  57. package/dist/loop/types.d.ts.map +1 -0
  58. package/dist/loop/types.js +8 -0
  59. package/dist/loop/types.js.map +1 -0
  60. package/dist/planning/gap-analysis.d.ts +29 -0
  61. package/dist/planning/gap-analysis.d.ts.map +1 -0
  62. package/dist/planning/gap-analysis.js +265 -0
  63. package/dist/planning/gap-analysis.js.map +1 -0
  64. package/dist/planning/gap-types.d.ts +29 -0
  65. package/dist/planning/gap-types.d.ts.map +1 -0
  66. package/dist/planning/gap-types.js +28 -0
  67. package/dist/planning/gap-types.js.map +1 -0
  68. package/dist/planning/planner.d.ts +150 -1
  69. package/dist/planning/planner.d.ts.map +1 -1
  70. package/dist/planning/planner.js +365 -2
  71. package/dist/planning/planner.js.map +1 -1
  72. package/dist/project/project-registry.d.ts +79 -0
  73. package/dist/project/project-registry.d.ts.map +1 -0
  74. package/dist/project/project-registry.js +276 -0
  75. package/dist/project/project-registry.js.map +1 -0
  76. package/dist/project/types.d.ts +28 -0
  77. package/dist/project/types.d.ts.map +1 -0
  78. package/dist/project/types.js +5 -0
  79. package/dist/project/types.js.map +1 -0
  80. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  81. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  82. package/dist/runtime/admin-extra-ops.js +284 -0
  83. package/dist/runtime/admin-extra-ops.js.map +1 -0
  84. package/dist/runtime/admin-ops.d.ts +15 -0
  85. package/dist/runtime/admin-ops.d.ts.map +1 -0
  86. package/dist/runtime/admin-ops.js +322 -0
  87. package/dist/runtime/admin-ops.js.map +1 -0
  88. package/dist/runtime/capture-ops.d.ts +15 -0
  89. package/dist/runtime/capture-ops.d.ts.map +1 -0
  90. package/dist/runtime/capture-ops.js +345 -0
  91. package/dist/runtime/capture-ops.js.map +1 -0
  92. package/dist/runtime/core-ops.d.ts +7 -3
  93. package/dist/runtime/core-ops.d.ts.map +1 -1
  94. package/dist/runtime/core-ops.js +474 -8
  95. package/dist/runtime/core-ops.js.map +1 -1
  96. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  97. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  98. package/dist/runtime/curator-extra-ops.js +59 -0
  99. package/dist/runtime/curator-extra-ops.js.map +1 -0
  100. package/dist/runtime/domain-ops.d.ts.map +1 -1
  101. package/dist/runtime/domain-ops.js +59 -13
  102. package/dist/runtime/domain-ops.js.map +1 -1
  103. package/dist/runtime/grading-ops.d.ts +14 -0
  104. package/dist/runtime/grading-ops.d.ts.map +1 -0
  105. package/dist/runtime/grading-ops.js +105 -0
  106. package/dist/runtime/grading-ops.js.map +1 -0
  107. package/dist/runtime/loop-ops.d.ts +13 -0
  108. package/dist/runtime/loop-ops.d.ts.map +1 -0
  109. package/dist/runtime/loop-ops.js +179 -0
  110. package/dist/runtime/loop-ops.js.map +1 -0
  111. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  112. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  113. package/dist/runtime/memory-cross-project-ops.js +165 -0
  114. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  115. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  116. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  117. package/dist/runtime/memory-extra-ops.js +173 -0
  118. package/dist/runtime/memory-extra-ops.js.map +1 -0
  119. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  120. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  121. package/dist/runtime/orchestrate-ops.js +240 -0
  122. package/dist/runtime/orchestrate-ops.js.map +1 -0
  123. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  124. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  125. package/dist/runtime/planning-extra-ops.js +300 -0
  126. package/dist/runtime/planning-extra-ops.js.map +1 -0
  127. package/dist/runtime/project-ops.d.ts +15 -0
  128. package/dist/runtime/project-ops.d.ts.map +1 -0
  129. package/dist/runtime/project-ops.js +181 -0
  130. package/dist/runtime/project-ops.js.map +1 -0
  131. package/dist/runtime/runtime.d.ts.map +1 -1
  132. package/dist/runtime/runtime.js +44 -1
  133. package/dist/runtime/runtime.js.map +1 -1
  134. package/dist/runtime/types.d.ts +21 -0
  135. package/dist/runtime/types.d.ts.map +1 -1
  136. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  137. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/vault-extra-ops.js +195 -0
  139. package/dist/runtime/vault-extra-ops.js.map +1 -0
  140. package/dist/telemetry/telemetry.d.ts +48 -0
  141. package/dist/telemetry/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry/telemetry.js +87 -0
  143. package/dist/telemetry/telemetry.js.map +1 -0
  144. package/dist/vault/vault.d.ts +94 -0
  145. package/dist/vault/vault.d.ts.map +1 -1
  146. package/dist/vault/vault.js +340 -1
  147. package/dist/vault/vault.js.map +1 -1
  148. package/package.json +1 -1
  149. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  150. package/src/__tests__/admin-ops.test.ts +271 -0
  151. package/src/__tests__/brain-intelligence.test.ts +205 -0
  152. package/src/__tests__/brain.test.ts +131 -0
  153. package/src/__tests__/capture-ops.test.ts +509 -0
  154. package/src/__tests__/core-ops.test.ts +266 -2
  155. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  156. package/src/__tests__/domain-ops.test.ts +66 -0
  157. package/src/__tests__/governance.test.ts +522 -0
  158. package/src/__tests__/grading-ops.test.ts +340 -0
  159. package/src/__tests__/identity-manager.test.ts +243 -0
  160. package/src/__tests__/intent-router.test.ts +222 -0
  161. package/src/__tests__/logger.test.ts +200 -0
  162. package/src/__tests__/loop-ops.test.ts +398 -0
  163. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  164. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  165. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  166. package/src/__tests__/planner.test.ts +331 -0
  167. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  168. package/src/__tests__/project-ops.test.ts +367 -0
  169. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  170. package/src/brain/brain.ts +114 -7
  171. package/src/brain/intelligence.ts +179 -10
  172. package/src/brain/types.ts +38 -0
  173. package/src/control/identity-manager.ts +354 -0
  174. package/src/control/intent-router.ts +326 -0
  175. package/src/control/types.ts +102 -0
  176. package/src/curator/curator.ts +213 -0
  177. package/src/governance/governance.ts +698 -0
  178. package/src/governance/index.ts +18 -0
  179. package/src/governance/types.ts +111 -0
  180. package/src/index.ts +102 -2
  181. package/src/logging/logger.ts +154 -0
  182. package/src/logging/types.ts +21 -0
  183. package/src/loop/loop-manager.ts +130 -0
  184. package/src/loop/types.ts +44 -0
  185. package/src/planning/gap-analysis.ts +506 -0
  186. package/src/planning/gap-types.ts +58 -0
  187. package/src/planning/planner.ts +478 -2
  188. package/src/project/project-registry.ts +358 -0
  189. package/src/project/types.ts +31 -0
  190. package/src/runtime/admin-extra-ops.ts +307 -0
  191. package/src/runtime/admin-ops.ts +329 -0
  192. package/src/runtime/capture-ops.ts +385 -0
  193. package/src/runtime/core-ops.ts +535 -7
  194. package/src/runtime/curator-extra-ops.ts +71 -0
  195. package/src/runtime/domain-ops.ts +65 -13
  196. package/src/runtime/grading-ops.ts +121 -0
  197. package/src/runtime/loop-ops.ts +194 -0
  198. package/src/runtime/memory-cross-project-ops.ts +192 -0
  199. package/src/runtime/memory-extra-ops.ts +186 -0
  200. package/src/runtime/orchestrate-ops.ts +272 -0
  201. package/src/runtime/planning-extra-ops.ts +327 -0
  202. package/src/runtime/project-ops.ts +196 -0
  203. package/src/runtime/runtime.ts +49 -1
  204. package/src/runtime/types.ts +21 -0
  205. package/src/runtime/vault-extra-ops.ts +225 -0
  206. package/src/telemetry/telemetry.ts +118 -0
  207. package/src/vault/vault.ts +412 -1
@@ -431,6 +431,41 @@ describe('BrainIntelligence', () => {
431
431
  expect(result.failed).toContain('non-existent');
432
432
  });
433
433
 
434
+ it('should gate promote through governance when strict preset', () => {
435
+ // Create a brain session and extract proposals
436
+ runtime.brainIntelligence.lifecycle({
437
+ action: 'start',
438
+ sessionId: 'gov-promo-1',
439
+ toolsUsed: ['w', 'w', 'w'],
440
+ });
441
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'gov-promo-1' });
442
+ runtime.brainIntelligence.extractKnowledge('gov-promo-1');
443
+
444
+ const proposals = runtime.brainIntelligence.getProposals({ sessionId: 'gov-promo-1' });
445
+ expect(proposals.length).toBeGreaterThan(0);
446
+
447
+ // Apply strict preset — requireReview: true
448
+ runtime.governance.applyPreset('.', 'strict', 'test');
449
+
450
+ const result = runtime.brainIntelligence.promoteProposals(
451
+ [proposals[0].id],
452
+ runtime.governance,
453
+ '.',
454
+ );
455
+
456
+ // Should be gated, not promoted
457
+ expect(result.promoted).toBe(0);
458
+ expect(result.gated.length).toBe(1);
459
+ expect(result.gated[0].action).toBe('propose');
460
+
461
+ // Entry should NOT be in vault
462
+ expect(runtime.vault.get(`proposal-${proposals[0].id}`)).toBeNull();
463
+
464
+ // But should be in governance proposals
465
+ const pending = runtime.governance.listPendingProposals('.');
466
+ expect(pending.some((p) => p.source === 'brain-promote')).toBe(true);
467
+ });
468
+
434
469
  // ─── Build Intelligence ────────────────────────────────────────
435
470
 
436
471
  it('should build intelligence pipeline', () => {
@@ -557,6 +592,7 @@ describe('BrainIntelligence', () => {
557
592
  filesModified: [],
558
593
  planId: null,
559
594
  planOutcome: null,
595
+ extractedAt: null,
560
596
  },
561
597
  ],
562
598
  proposals: [],
@@ -609,6 +645,7 @@ describe('BrainIntelligence', () => {
609
645
  filesModified: [],
610
646
  planId: null,
611
647
  planOutcome: null,
648
+ extractedAt: null,
612
649
  },
613
650
  ],
614
651
  proposals: [],
@@ -620,4 +657,172 @@ describe('BrainIntelligence', () => {
620
657
  const result = runtime.brainIntelligence.importData(data);
621
658
  expect(result.imported.sessions).toBe(0); // Duplicate ignored
622
659
  });
660
+
661
+ // ─── Auto-extraction on session end ─────────────────────────
662
+
663
+ it('should auto-extract when session ends with tools used', () => {
664
+ runtime.brainIntelligence.lifecycle({
665
+ action: 'start',
666
+ sessionId: 'auto-1',
667
+ domain: 'testing',
668
+ toolsUsed: ['search', 'search', 'search'],
669
+ });
670
+ const session = runtime.brainIntelligence.lifecycle({
671
+ action: 'end',
672
+ sessionId: 'auto-1',
673
+ });
674
+ // extractedAt should be set automatically
675
+ expect(session.extractedAt).not.toBeNull();
676
+ });
677
+
678
+ it('should not auto-extract when session has no signal', () => {
679
+ runtime.brainIntelligence.lifecycle({
680
+ action: 'start',
681
+ sessionId: 'auto-2',
682
+ });
683
+ const session = runtime.brainIntelligence.lifecycle({
684
+ action: 'end',
685
+ sessionId: 'auto-2',
686
+ });
687
+ // No tools, no files, no plan — should not extract
688
+ expect(session.extractedAt).toBeNull();
689
+ });
690
+
691
+ it('should auto-extract when session has a plan', () => {
692
+ runtime.brainIntelligence.lifecycle({
693
+ action: 'start',
694
+ sessionId: 'auto-3',
695
+ planId: 'plan-123',
696
+ });
697
+ const session = runtime.brainIntelligence.lifecycle({
698
+ action: 'end',
699
+ sessionId: 'auto-3',
700
+ planOutcome: 'completed',
701
+ });
702
+ expect(session.extractedAt).not.toBeNull();
703
+ });
704
+
705
+ // ─── extractedAt Tracking ──────────────────────────────────
706
+
707
+ it('should set extractedAt when extractKnowledge is called manually', () => {
708
+ // Use a session with no tools/files/plan so auto-extraction doesn't fire
709
+ runtime.brainIntelligence.lifecycle({
710
+ action: 'start',
711
+ sessionId: 'ext-1',
712
+ domain: 'testing',
713
+ });
714
+ runtime.brainIntelligence.lifecycle({
715
+ action: 'end',
716
+ sessionId: 'ext-1',
717
+ });
718
+
719
+ // Before manual extraction, extractedAt should be null (no auto-extract — no signal)
720
+ const ctx = runtime.brainIntelligence.getSessionContext(10);
721
+ const before = ctx.recentSessions.find((s) => s.id === 'ext-1');
722
+ expect(before?.extractedAt).toBeNull();
723
+
724
+ // Manual extract
725
+ runtime.brainIntelligence.extractKnowledge('ext-1');
726
+
727
+ // After extraction, extractedAt should be set
728
+ const ctx2 = runtime.brainIntelligence.getSessionContext(10);
729
+ const after = ctx2.recentSessions.find((s) => s.id === 'ext-1');
730
+ expect(after?.extractedAt).not.toBeNull();
731
+ });
732
+
733
+ // ─── resetExtracted ────────────────────────────────────────
734
+
735
+ it('should reset extractedAt for a specific session', () => {
736
+ runtime.brainIntelligence.lifecycle({
737
+ action: 'start',
738
+ sessionId: 'reset-1',
739
+ toolsUsed: ['a', 'a', 'a'],
740
+ });
741
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-1' });
742
+ runtime.brainIntelligence.extractKnowledge('reset-1');
743
+
744
+ const result = runtime.brainIntelligence.resetExtracted({ sessionId: 'reset-1' });
745
+ expect(result.reset).toBe(1);
746
+
747
+ const ctx = runtime.brainIntelligence.getSessionContext(10);
748
+ const session = ctx.recentSessions.find((s) => s.id === 'reset-1');
749
+ expect(session?.extractedAt).toBeNull();
750
+ });
751
+
752
+ it('should reset all extracted sessions', () => {
753
+ runtime.brainIntelligence.lifecycle({
754
+ action: 'start',
755
+ sessionId: 'reset-a',
756
+ toolsUsed: ['a', 'a', 'a'],
757
+ });
758
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-a' });
759
+ runtime.brainIntelligence.extractKnowledge('reset-a');
760
+
761
+ runtime.brainIntelligence.lifecycle({
762
+ action: 'start',
763
+ sessionId: 'reset-b',
764
+ toolsUsed: ['b', 'b', 'b'],
765
+ });
766
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reset-b' });
767
+ runtime.brainIntelligence.extractKnowledge('reset-b');
768
+
769
+ const result = runtime.brainIntelligence.resetExtracted({ all: true });
770
+ expect(result.reset).toBe(2);
771
+ });
772
+
773
+ it('should return 0 when nothing to reset', () => {
774
+ const result = runtime.brainIntelligence.resetExtracted({ all: true });
775
+ expect(result.reset).toBe(0);
776
+ });
777
+
778
+ it('should return 0 when no filter is provided', () => {
779
+ const result = runtime.brainIntelligence.resetExtracted();
780
+ expect(result.reset).toBe(0);
781
+ });
782
+
783
+ it('should allow re-extraction after reset', () => {
784
+ runtime.brainIntelligence.lifecycle({
785
+ action: 'start',
786
+ sessionId: 'reext-1',
787
+ toolsUsed: ['search', 'search', 'search'],
788
+ });
789
+ runtime.brainIntelligence.lifecycle({ action: 'end', sessionId: 'reext-1' });
790
+
791
+ const first = runtime.brainIntelligence.extractKnowledge('reext-1');
792
+ expect(first.proposals.length).toBeGreaterThan(0);
793
+
794
+ runtime.brainIntelligence.resetExtracted({ sessionId: 'reext-1' });
795
+
796
+ const second = runtime.brainIntelligence.extractKnowledge('reext-1');
797
+ expect(second.proposals.length).toBeGreaterThan(0);
798
+ });
799
+
800
+ // ─── computeStrengths with modified feedback ───────────────
801
+
802
+ it('should weight modified feedback at 0.5 in computeStrengths', () => {
803
+ // Seed an entry for feedback to reference
804
+ runtime.vault.seed([
805
+ {
806
+ id: 'str-1',
807
+ type: 'pattern',
808
+ domain: 'testing',
809
+ title: 'Strength test pattern',
810
+ severity: 'warning',
811
+ description: 'Testing strength computation with modified feedback.',
812
+ tags: ['test'],
813
+ },
814
+ ]);
815
+
816
+ // Record feedback: 1 accepted, 1 modified, 1 failed
817
+ runtime.brain.recordFeedback({ query: 'q1', entryId: 'str-1', action: 'accepted' });
818
+ runtime.brain.recordFeedback({ query: 'q2', entryId: 'str-1', action: 'modified' });
819
+ runtime.brain.recordFeedback({ query: 'q3', entryId: 'str-1', action: 'failed' });
820
+
821
+ const strengths = runtime.brainIntelligence.computeStrengths();
822
+ const s = strengths.find((p) => p.pattern === 'Strength test pattern');
823
+ expect(s).toBeDefined();
824
+
825
+ // successRate = (1 + 0.5) / (3 - 1) = 1.5/2 = 0.75
826
+ expect(s!.successRate).toBeCloseTo(0.75, 2);
827
+ });
623
828
  });
@@ -741,6 +741,137 @@ describe('Brain', () => {
741
741
  });
742
742
  });
743
743
 
744
+ // ─── Enhanced Feedback ─────────────────────────────────────
745
+
746
+ describe('enhanced feedback', () => {
747
+ beforeEach(() => {
748
+ vault.seed([makeEntry({ id: 'fb-1', title: 'Feedback test pattern', tags: ['feedback'] })]);
749
+ brain = new Brain(vault);
750
+ });
751
+
752
+ it('should accept legacy 3-arg form (backward compat)', () => {
753
+ brain.recordFeedback('test query', 'fb-1', 'accepted');
754
+ const stats = brain.getFeedbackStats();
755
+ expect(stats.total).toBe(1);
756
+ expect(stats.byAction['accepted']).toBe(1);
757
+ });
758
+
759
+ it('should accept FeedbackInput and return FeedbackEntry', () => {
760
+ const entry = brain.recordFeedback({
761
+ query: 'test query',
762
+ entryId: 'fb-1',
763
+ action: 'modified',
764
+ source: 'recommendation',
765
+ confidence: 0.85,
766
+ duration: 1200,
767
+ reason: 'adjusted wording',
768
+ });
769
+ expect(entry).toBeDefined();
770
+ expect(entry.action).toBe('modified');
771
+ expect(entry.source).toBe('recommendation');
772
+ expect(entry.confidence).toBe(0.85);
773
+ expect(entry.duration).toBe(1200);
774
+ expect(entry.reason).toBe('adjusted wording');
775
+ expect(entry.id).toBeGreaterThan(0);
776
+ });
777
+
778
+ it('should accept modified and failed action types', () => {
779
+ brain.recordFeedback({ query: 'q1', entryId: 'fb-1', action: 'modified' });
780
+ brain.recordFeedback({ query: 'q2', entryId: 'fb-1', action: 'failed' });
781
+ const stats = brain.getFeedbackStats();
782
+ expect(stats.total).toBe(2);
783
+ expect(stats.byAction['modified']).toBe(1);
784
+ expect(stats.byAction['failed']).toBe(1);
785
+ });
786
+
787
+ it('should use default source and confidence when not provided', () => {
788
+ const entry = brain.recordFeedback({ query: 'q1', entryId: 'fb-1', action: 'accepted' });
789
+ expect(entry.source).toBe('search');
790
+ expect(entry.confidence).toBe(0.6);
791
+ });
792
+ });
793
+
794
+ // ─── Feedback Stats ───────────────────────────────────────
795
+
796
+ describe('getFeedbackStats', () => {
797
+ beforeEach(() => {
798
+ vault.seed([makeEntry({ id: 'fs-1', tags: ['stats'] })]);
799
+ brain = new Brain(vault);
800
+ });
801
+
802
+ it('should return zero stats on empty feedback', () => {
803
+ const stats = brain.getFeedbackStats();
804
+ expect(stats.total).toBe(0);
805
+ expect(stats.acceptanceRate).toBe(0);
806
+ expect(stats.averageConfidence).toBe(0);
807
+ });
808
+
809
+ it('should compute acceptance rate correctly', () => {
810
+ brain.recordFeedback('q1', 'fs-1', 'accepted');
811
+ brain.recordFeedback('q2', 'fs-1', 'dismissed');
812
+ brain.recordFeedback('q3', 'fs-1', 'accepted');
813
+ const stats = brain.getFeedbackStats();
814
+ expect(stats.total).toBe(3);
815
+ expect(stats.acceptanceRate).toBeCloseTo(2 / 3, 2);
816
+ });
817
+
818
+ it('should group by action and source', () => {
819
+ brain.recordFeedback({ query: 'q1', entryId: 'fs-1', action: 'accepted', source: 'search' });
820
+ brain.recordFeedback({
821
+ query: 'q2',
822
+ entryId: 'fs-1',
823
+ action: 'modified',
824
+ source: 'recommendation',
825
+ });
826
+ brain.recordFeedback({
827
+ query: 'q3',
828
+ entryId: 'fs-1',
829
+ action: 'failed',
830
+ source: 'tool-execution',
831
+ });
832
+ const stats = brain.getFeedbackStats();
833
+ expect(stats.byAction['accepted']).toBe(1);
834
+ expect(stats.byAction['modified']).toBe(1);
835
+ expect(stats.byAction['failed']).toBe(1);
836
+ expect(stats.bySource['search']).toBe(1);
837
+ expect(stats.bySource['recommendation']).toBe(1);
838
+ expect(stats.bySource['tool-execution']).toBe(1);
839
+ });
840
+
841
+ it('should compute average confidence', () => {
842
+ brain.recordFeedback({ query: 'q1', entryId: 'fs-1', action: 'accepted', confidence: 0.9 });
843
+ brain.recordFeedback({ query: 'q2', entryId: 'fs-1', action: 'dismissed', confidence: 0.3 });
844
+ const stats = brain.getFeedbackStats();
845
+ expect(stats.averageConfidence).toBeCloseTo(0.6, 2);
846
+ });
847
+ });
848
+
849
+ // ─── Recompute Weights with Modified/Failed ──────────────
850
+
851
+ describe('recomputeWeights with modified/failed', () => {
852
+ beforeEach(() => {
853
+ vault.seed([makeEntry({ id: 'rw-1', tags: ['weights'] })]);
854
+ brain = new Brain(vault);
855
+ });
856
+
857
+ it('should exclude failed from weight computation', () => {
858
+ // Add enough feedback to exceed threshold (30)
859
+ for (let i = 0; i < 20; i++) {
860
+ brain.recordFeedback('q', 'rw-1', 'accepted');
861
+ }
862
+ for (let i = 0; i < 10; i++) {
863
+ brain.recordFeedback({ query: 'q', entryId: 'rw-1', action: 'failed' });
864
+ }
865
+ // Failed entries should not count toward total for weight adaptation
866
+ // 20 accepted out of 20 relevant = 100% accept rate
867
+ const stats = brain.getStats();
868
+ // Weights should have adapted since we have 30+ total but only 20 non-failed
869
+ // (threshold is 30, total is 30, but only 20 are non-failed so threshold not met)
870
+ // The recomputeWeights() counts non-failed, which is 20 < 30, so weights stay default
871
+ expect(stats.weights.semantic).toBeCloseTo(0.4, 2);
872
+ });
873
+ });
874
+
744
875
  // ─── Graceful Degradation ───────────────────────────────────
745
876
 
746
877
  describe('graceful degradation', () => {