recallnest 2.3.0 → 2.6.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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Tests for HP-predictive Phase 5: Prediction Engine
3
+ *
4
+ * Validates:
5
+ * 1. collectSignals() — each signal type is correctly extracted
6
+ * 2. scorePredictions() — scoring, filtering by threshold, ranking
7
+ * 3. Edge cases — empty inputs, boundary conditions
8
+ */
9
+ import { describe, expect, it } from "bun:test";
10
+ import {
11
+ collectSignals,
12
+ scorePredictions,
13
+ _testConstants,
14
+ type PredictionContext,
15
+ type PredictionSignal,
16
+ } from "../prediction-engine.js";
17
+ import type { SessionCheckpointRecord } from "../session-schema.js";
18
+ import type { WorkflowObservationRecord } from "../workflow-observation-schema.js";
19
+
20
+ const { CONFIDENCE_THRESHOLD, STALE_OPEN_LOOP_HOURS, STALE_MEMORY_DAYS, MAX_PREDICTIONS, SIGNAL_WEIGHTS } = _testConstants;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function makeCheckpoint(overrides: Partial<SessionCheckpointRecord> & { openLoops?: string[]; updatedAt: string }): SessionCheckpointRecord {
27
+ return {
28
+ checkpointId: `cp-${Math.random().toString(36).slice(2, 8)}`,
29
+ sessionId: "test-session",
30
+ resolvedScope: "test",
31
+ scope: "test",
32
+ summary: "test checkpoint",
33
+ task: "test task",
34
+ decisions: [],
35
+ openLoops: overrides.openLoops ?? [],
36
+ nextActions: [],
37
+ entities: [],
38
+ files: [],
39
+ updatedAt: overrides.updatedAt,
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ function makeWorkflowObservation(overrides: Partial<WorkflowObservationRecord>): WorkflowObservationRecord {
45
+ return {
46
+ observationId: `obs-${Math.random().toString(36).slice(2, 8)}`,
47
+ workflowId: "test-workflow",
48
+ outcome: "corrected",
49
+ summary: "Fixed a workflow issue",
50
+ scope: "test",
51
+ resolvedScope: "test",
52
+ source: "manual",
53
+ tags: [],
54
+ tools: [],
55
+ recordedAt: new Date().toISOString(),
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ function emptyContext(overrides?: Partial<PredictionContext>): PredictionContext {
61
+ return {
62
+ checkpoints: [],
63
+ workflowObservations: [],
64
+ frequentMemories: [],
65
+ uncoveredTopics: [],
66
+ now: new Date("2026-04-11T12:00:00Z"),
67
+ ...overrides,
68
+ };
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // collectSignals
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe("collectSignals", () => {
76
+ describe("stale open loop signals", () => {
77
+ it("detects stale open loops from checkpoints older than 24h", () => {
78
+ const now = new Date("2026-04-11T12:00:00Z");
79
+ const staleCheckpoint = makeCheckpoint({
80
+ openLoops: ["Fix CI pipeline", "Review PR #42"],
81
+ updatedAt: new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(), // 48h ago
82
+ });
83
+
84
+ const signals = collectSignals(emptyContext({
85
+ checkpoints: [staleCheckpoint],
86
+ now,
87
+ }));
88
+
89
+ const openLoopSignals = signals.filter(s => s.type === "stale_open_loop");
90
+ expect(openLoopSignals.length).toBe(2);
91
+ expect(openLoopSignals[0].trigger).toBe("Fix CI pipeline");
92
+ expect(openLoopSignals[0].weight).toBeGreaterThan(0.6);
93
+ expect(openLoopSignals[0].evidence.length).toBeGreaterThan(0);
94
+ });
95
+
96
+ it("ignores fresh open loops (< 24h)", () => {
97
+ const now = new Date("2026-04-11T12:00:00Z");
98
+ const freshCheckpoint = makeCheckpoint({
99
+ openLoops: ["In-progress task"],
100
+ updatedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(), // 12h ago
101
+ });
102
+
103
+ const signals = collectSignals(emptyContext({
104
+ checkpoints: [freshCheckpoint],
105
+ now,
106
+ }));
107
+
108
+ expect(signals.filter(s => s.type === "stale_open_loop").length).toBe(0);
109
+ });
110
+
111
+ it("deduplicates same open loop across checkpoints", () => {
112
+ const now = new Date("2026-04-11T12:00:00Z");
113
+ const cp1 = makeCheckpoint({
114
+ openLoops: ["Fix CI pipeline"],
115
+ updatedAt: new Date(now.getTime() - 48 * 60 * 60 * 1000).toISOString(),
116
+ });
117
+ const cp2 = makeCheckpoint({
118
+ openLoops: ["fix ci pipeline"], // same but different case
119
+ updatedAt: new Date(now.getTime() - 72 * 60 * 60 * 1000).toISOString(),
120
+ });
121
+
122
+ const signals = collectSignals(emptyContext({
123
+ checkpoints: [cp1, cp2],
124
+ now,
125
+ }));
126
+
127
+ expect(signals.filter(s => s.type === "stale_open_loop").length).toBe(1);
128
+ });
129
+
130
+ it("weights increase with staleness", () => {
131
+ const now = new Date("2026-04-11T12:00:00Z");
132
+ const cp24h = makeCheckpoint({
133
+ openLoops: ["Task A"],
134
+ updatedAt: new Date(now.getTime() - 25 * 60 * 60 * 1000).toISOString(),
135
+ });
136
+ const cp72h = makeCheckpoint({
137
+ openLoops: ["Task B"],
138
+ updatedAt: new Date(now.getTime() - 72 * 60 * 60 * 1000).toISOString(),
139
+ });
140
+
141
+ const signals = collectSignals(emptyContext({
142
+ checkpoints: [cp24h, cp72h],
143
+ now,
144
+ }));
145
+
146
+ const signalA = signals.find(s => s.trigger === "Task A")!;
147
+ const signalB = signals.find(s => s.trigger === "Task B")!;
148
+ expect(signalB.weight).toBeGreaterThan(signalA.weight);
149
+ });
150
+ });
151
+
152
+ describe("stale high-frequency memory signals", () => {
153
+ it("detects memories accessed frequently but not recently", () => {
154
+ const now = new Date("2026-04-11T12:00:00Z");
155
+ const signals = collectSignals(emptyContext({
156
+ frequentMemories: [{
157
+ text: "Auth migration plan",
158
+ topicTag: "auth",
159
+ lastAccessedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago
160
+ accessCount: 5,
161
+ }],
162
+ now,
163
+ }));
164
+
165
+ const staleSignals = signals.filter(s => s.type === "stale_high_frequency");
166
+ expect(staleSignals.length).toBe(1);
167
+ expect(staleSignals[0].trigger).toBe("auth");
168
+ expect(staleSignals[0].evidence).toContain("Accessed 5 times but not in 30 days");
169
+ });
170
+
171
+ it("ignores recently accessed memories", () => {
172
+ const now = new Date("2026-04-11T12:00:00Z");
173
+ const signals = collectSignals(emptyContext({
174
+ frequentMemories: [{
175
+ text: "Recent memory",
176
+ lastAccessedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
177
+ accessCount: 10,
178
+ }],
179
+ now,
180
+ }));
181
+
182
+ expect(signals.filter(s => s.type === "stale_high_frequency").length).toBe(0);
183
+ });
184
+
185
+ it("ignores infrequently accessed memories (< 3 accesses)", () => {
186
+ const now = new Date("2026-04-11T12:00:00Z");
187
+ const signals = collectSignals(emptyContext({
188
+ frequentMemories: [{
189
+ text: "Rarely accessed",
190
+ lastAccessedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
191
+ accessCount: 2,
192
+ }],
193
+ now,
194
+ }));
195
+
196
+ expect(signals.filter(s => s.type === "stale_high_frequency").length).toBe(0);
197
+ });
198
+ });
199
+
200
+ describe("workflow issue signals", () => {
201
+ it("detects corrected/missed workflow observations", () => {
202
+ const observations = [
203
+ makeWorkflowObservation({ workflowId: "deploy", outcome: "corrected", summary: "Forgot to run tests" }),
204
+ makeWorkflowObservation({ workflowId: "deploy", outcome: "missed", summary: "Missed the changelog update" }),
205
+ makeWorkflowObservation({ workflowId: "deploy", outcome: "success", summary: "All good" }),
206
+ ];
207
+
208
+ const signals = collectSignals(emptyContext({ workflowObservations: observations }));
209
+ const issueSignals = signals.filter(s => s.type === "workflow_issue");
210
+ expect(issueSignals.length).toBe(1);
211
+ expect(issueSignals[0].trigger).toBe("deploy");
212
+ expect(issueSignals[0].evidence[0]).toContain("2 corrected/missed");
213
+ });
214
+
215
+ it("ignores workflows with only successes", () => {
216
+ const observations = [
217
+ makeWorkflowObservation({ workflowId: "deploy", outcome: "success" }),
218
+ ];
219
+
220
+ const signals = collectSignals(emptyContext({ workflowObservations: observations }));
221
+ expect(signals.filter(s => s.type === "workflow_issue").length).toBe(0);
222
+ });
223
+ });
224
+
225
+ describe("uncovered topic signals", () => {
226
+ it("creates signals for uncovered topics", () => {
227
+ const signals = collectSignals(emptyContext({
228
+ uncoveredTopics: ["kubernetes", "migration"],
229
+ }));
230
+
231
+ const topicSignals = signals.filter(s => s.type === "uncovered_topic");
232
+ expect(topicSignals.length).toBe(2);
233
+ expect(topicSignals[0].trigger).toBe("kubernetes");
234
+ expect(topicSignals[0].weight).toBe(0.65);
235
+ });
236
+ });
237
+
238
+ describe("empty inputs", () => {
239
+ it("returns empty array for empty context", () => {
240
+ const signals = collectSignals(emptyContext());
241
+ expect(signals).toEqual([]);
242
+ });
243
+ });
244
+ });
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // scorePredictions
248
+ // ---------------------------------------------------------------------------
249
+
250
+ describe("scorePredictions", () => {
251
+ it("scores signals and filters by confidence threshold", () => {
252
+ const signals: PredictionSignal[] = [
253
+ {
254
+ type: "stale_open_loop",
255
+ trigger: "Fix CI",
256
+ action: "Unresolved: Fix CI",
257
+ weight: 0.9,
258
+ evidence: ["48h old"],
259
+ },
260
+ {
261
+ type: "uncovered_topic",
262
+ trigger: "obscure",
263
+ action: "No memories for obscure",
264
+ weight: 0.5, // Below threshold after scoring
265
+ evidence: ["No coverage"],
266
+ },
267
+ ];
268
+
269
+ const predictions = scorePredictions(signals);
270
+
271
+ // stale_open_loop: 0.9 * 0.9 = 0.81 (above 0.6)
272
+ // uncovered_topic: 0.5 * 0.65 = 0.325 (below 0.6)
273
+ expect(predictions.length).toBe(1);
274
+ expect(predictions[0].trigger).toBe("Fix CI");
275
+ expect(predictions[0].confidence).toBe(0.81);
276
+ });
277
+
278
+ it("sorts by confidence descending", () => {
279
+ const signals: PredictionSignal[] = [
280
+ { type: "uncovered_topic", trigger: "low", action: "low", weight: 1.0, evidence: [] },
281
+ { type: "stale_open_loop", trigger: "high", action: "high", weight: 1.0, evidence: [] },
282
+ { type: "workflow_issue", trigger: "mid", action: "mid", weight: 1.0, evidence: [] },
283
+ ];
284
+
285
+ const predictions = scorePredictions(signals);
286
+ // stale_open_loop: 1.0 * 0.9 = 0.9
287
+ // workflow_issue: 1.0 * 0.85 = 0.85
288
+ // uncovered_topic: 1.0 * 0.65 = 0.65
289
+ expect(predictions.length).toBe(3);
290
+ expect(predictions[0].trigger).toBe("high");
291
+ expect(predictions[1].trigger).toBe("mid");
292
+ expect(predictions[2].trigger).toBe("low");
293
+ });
294
+
295
+ it("limits to MAX_PREDICTIONS", () => {
296
+ const signals: PredictionSignal[] = Array.from({ length: 10 }, (_, i) => ({
297
+ type: "stale_open_loop" as const,
298
+ trigger: `task-${i}`,
299
+ action: `action-${i}`,
300
+ weight: 1.0,
301
+ evidence: [],
302
+ }));
303
+
304
+ const predictions = scorePredictions(signals);
305
+ expect(predictions.length).toBe(MAX_PREDICTIONS);
306
+ });
307
+
308
+ it("returns empty for all below threshold", () => {
309
+ const signals: PredictionSignal[] = [
310
+ { type: "uncovered_topic", trigger: "x", action: "x", weight: 0.3, evidence: [] },
311
+ ];
312
+
313
+ const predictions = scorePredictions(signals);
314
+ expect(predictions.length).toBe(0);
315
+ });
316
+
317
+ it("returns empty for empty signals", () => {
318
+ expect(scorePredictions([])).toEqual([]);
319
+ });
320
+ });
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Constants sanity
324
+ // ---------------------------------------------------------------------------
325
+
326
+ describe("prediction engine constants", () => {
327
+ it("confidence threshold is 0.6", () => {
328
+ expect(CONFIDENCE_THRESHOLD).toBe(0.6);
329
+ });
330
+
331
+ it("stale open loop hours is 24", () => {
332
+ expect(STALE_OPEN_LOOP_HOURS).toBe(24);
333
+ });
334
+
335
+ it("stale memory days is 14", () => {
336
+ expect(STALE_MEMORY_DAYS).toBe(14);
337
+ });
338
+
339
+ it("max predictions is 5", () => {
340
+ expect(MAX_PREDICTIONS).toBe(5);
341
+ });
342
+
343
+ it("signal weights are all between 0 and 1", () => {
344
+ for (const [, weight] of Object.entries(SIGNAL_WEIGHTS)) {
345
+ expect(weight).toBeGreaterThanOrEqual(0);
346
+ expect(weight).toBeLessThanOrEqual(1);
347
+ }
348
+ });
349
+ });