@cleocode/core 2026.4.46 → 2026.4.48

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.
@@ -83,14 +83,21 @@ export interface PromotionResult {
83
83
  /**
84
84
  * Run tier promotion for all memory tables.
85
85
  *
86
- * Promotion rules (per spec §1.1–§1.3):
87
- * - short → medium: citationCount >= 3 AND age > 24h AND verified = true
88
- * OR qualityScore >= 0.7 AND verified = true (fast-track)
89
- * - medium → long: citationCount >= 5 AND age > 7 days AND verified = true
86
+ * Promotion rules (per spec §1.1–§1.3, relaxed in T614):
87
+ * - short → medium:
88
+ * A. (citationCount >= 3 AND age > 24h) — citation-based track
89
+ * B. (qualityScore >= 0.7 AND age > 24h) quality fast-track
90
+ * C. (verified = true AND age > 24h) — owner-verified track
91
+ * Note: `verified` is no longer a hard gate for routes A and B.
92
+ * Requiring verified=true on all paths caused all 235 short-tier observations
93
+ * to be permanently stuck (T614 bug).
94
+ * - medium → long:
95
+ * (citationCount >= 5 AND age > 7 days) OR (verified = true AND age > 7 days)
96
+ * Verified entries accelerate to long-tier without citation threshold.
90
97
  *
91
98
  * Eviction rules:
92
- * - short-term entries older than shortTermEvictionDays (default 7) with no
93
- * promotion eligibility are soft-evicted (invalidAt = now).
99
+ * - short-term entries older than 7 days with no promotion eligibility are
100
+ * soft-evicted (invalidAt = now).
94
101
  * - long-term entries are NEVER auto-evicted.
95
102
  *
96
103
  * @param projectRoot - Project root directory for brain.db resolution
@@ -1 +1 @@
1
- {"version":3,"file":"brain-lifecycle.d.ts","sourceRoot":"","sources":["../../src/memory/brain-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,WAAW,CAAC,CAqCtB;AAMD,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAmID;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,mBAAmB,CAAC,CA+G9B;AAMD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAmKpF;AAMD,oDAAoD;AACpD,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,cAAc,EAAE,eAAe,CAAC;IAChC,iCAAiC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkF3F"}
1
+ {"version":3,"file":"brain-lifecycle.d.ts","sourceRoot":"","sources":["../../src/memory/brain-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,WAAW,CAAC,CAqCtB;AAMD,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAmID;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5D,OAAO,CAAC,mBAAmB,CAAC,CA+G9B;AAMD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAuKpF;AAMD,oDAAoD;AACpD,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,cAAc,EAAE,eAAe,CAAC;IAChC,iCAAiC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+BAA+B;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkF3F"}
@@ -48,6 +48,13 @@ export interface BrainMaintenanceEmbeddingsResult {
48
48
  /** Observations that failed embedding. */
49
49
  errors: number;
50
50
  }
51
+ /** Tier promotion step result. */
52
+ export interface BrainMaintenanceTierPromotionResult {
53
+ /** Number of entries promoted (short→medium or medium→long). */
54
+ promoted: number;
55
+ /** Number of stale short-tier entries soft-evicted. */
56
+ evicted: number;
57
+ }
51
58
  /**
52
59
  * Aggregated result from a full brain maintenance run.
53
60
  *
@@ -61,6 +68,8 @@ export interface BrainMaintenanceResult {
61
68
  consolidation: BrainMaintenanceConsolidationResult;
62
69
  /** Results from the cross-DB orphaned reference reconciliation step. */
63
70
  reconciliation: BrainMaintenanceReconciliationResult;
71
+ /** Results from the tier promotion step. */
72
+ tierPromotion: BrainMaintenanceTierPromotionResult;
64
73
  /** Results from the embedding backfill step. */
65
74
  embeddings: BrainMaintenanceEmbeddingsResult;
66
75
  /** Total wall-clock duration of the maintenance run in milliseconds. */
@@ -79,6 +88,8 @@ export interface BrainMaintenanceOptions {
79
88
  skipConsolidation?: boolean;
80
89
  /** Skip the cross-DB orphaned reference reconciliation step. Default: false. */
81
90
  skipReconciliation?: boolean;
91
+ /** Skip the tier promotion step (short→medium, medium→long). Default: false. */
92
+ skipTierPromotion?: boolean;
82
93
  /** Skip the embedding backfill step. Default: false. */
83
94
  skipEmbeddings?: boolean;
84
95
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"brain-maintenance.d.ts","sourceRoot":"","sources":["../../src/memory/brain-maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,oEAAoE;AACpE,MAAM,WAAW,2BAA2B;IAC1C,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mCAAmC;IAClD,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,oCAAoC;IACnD,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,sCAAsC;AACtC,MAAM,WAAW,gCAAgC;IAC/C,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,KAAK,EAAE,2BAA2B,CAAC;IACnC,kDAAkD;IAClD,aAAa,EAAE,mCAAmC,CAAC;IACnD,wEAAwE;IACxE,cAAc,EAAE,oCAAoC,CAAC;IACrD,gDAAgD;IAChD,UAAU,EAAE,gCAAgC,CAAC;IAC7C,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wDAAwD;IACxD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,CAuEjC"}
1
+ {"version":3,"file":"brain-maintenance.d.ts","sourceRoot":"","sources":["../../src/memory/brain-maintenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,oEAAoE;AACpE,MAAM,WAAW,2BAA2B;IAC1C,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mCAAmC;IAClD,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,oCAAoC;IACnD,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,sCAAsC;AACtC,MAAM,WAAW,gCAAgC;IAC/C,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,kCAAkC;AAClC,MAAM,WAAW,mCAAmC;IAClD,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,4CAA4C;IAC5C,KAAK,EAAE,2BAA2B,CAAC;IACnC,kDAAkD;IAClD,aAAa,EAAE,mCAAmC,CAAC;IACnD,wEAAwE;IACxE,cAAc,EAAE,oCAAoC,CAAC;IACrD,4CAA4C;IAC5C,aAAa,EAAE,mCAAmC,CAAC;IACnD,gDAAgD;IAChD,UAAU,EAAE,gCAAgC,CAAC;IAC7C,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,wDAAwD;IACxD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrE;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,CAoFjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.4.46",
3
+ "version": "2026.4.48",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,16 @@
21
21
  "import": "./dist/conduit/index.js",
22
22
  "require": "./dist/conduit/index.js"
23
23
  },
24
+ "./store/*": {
25
+ "types": "./dist/store/*.d.ts",
26
+ "import": "./dist/store/*.js",
27
+ "require": "./dist/store/*.js"
28
+ },
29
+ "./conduit/*": {
30
+ "types": "./dist/conduit/*.d.ts",
31
+ "import": "./dist/conduit/*.js",
32
+ "require": "./dist/conduit/*.js"
33
+ },
24
34
  "./*": {
25
35
  "types": "./dist/*.d.ts",
26
36
  "import": "./dist/*.js",
@@ -53,13 +63,13 @@
53
63
  "write-file-atomic": "^7.0.1",
54
64
  "yaml": "^2.8.3",
55
65
  "zod": "^4.3.6",
56
- "@cleocode/adapters": "2026.4.46",
57
- "@cleocode/caamp": "2026.4.46",
58
- "@cleocode/agents": "2026.4.46",
59
- "@cleocode/lafs": "2026.4.46",
60
- "@cleocode/contracts": "2026.4.46",
61
- "@cleocode/nexus": "2026.4.46",
62
- "@cleocode/skills": "2026.4.46"
66
+ "@cleocode/adapters": "2026.4.48",
67
+ "@cleocode/agents": "2026.4.48",
68
+ "@cleocode/caamp": "2026.4.48",
69
+ "@cleocode/contracts": "2026.4.48",
70
+ "@cleocode/lafs": "2026.4.48",
71
+ "@cleocode/nexus": "2026.4.48",
72
+ "@cleocode/skills": "2026.4.48"
63
73
  },
64
74
  "engines": {
65
75
  "node": ">=24.0.0"
package/src/internal.ts CHANGED
@@ -170,9 +170,21 @@ export { STAGE_DEFINITIONS } from './lifecycle/stages.js';
170
170
  export { instantiateTessera, showTessera } from './lifecycle/tessera-engine.js';
171
171
  export type { BrainBackfillResult } from './memory/brain-backfill.js';
172
172
  export { backfillBrainGraph } from './memory/brain-backfill.js';
173
- // Memory — brain lifecycle (temporal decay + consolidation)
174
- export type { ConsolidationResult, DecayResult } from './memory/brain-lifecycle.js';
175
- export { applyTemporalDecay, consolidateMemories } from './memory/brain-lifecycle.js';
173
+ // Memory — brain lifecycle (temporal decay + consolidation + tier promotion)
174
+ export type {
175
+ ConsolidationResult,
176
+ DecayResult,
177
+ EvictionRecord,
178
+ PromotionRecord,
179
+ PromotionResult,
180
+ RunConsolidationResult,
181
+ } from './memory/brain-lifecycle.js';
182
+ export {
183
+ applyTemporalDecay,
184
+ consolidateMemories,
185
+ runConsolidation,
186
+ runTierPromotion,
187
+ } from './memory/brain-lifecycle.js';
176
188
  // Memory — brain maintenance
177
189
  export type {
178
190
  BrainMaintenanceConsolidationResult,
@@ -181,6 +193,7 @@ export type {
181
193
  BrainMaintenanceOptions,
182
194
  BrainMaintenanceReconciliationResult,
183
195
  BrainMaintenanceResult,
196
+ BrainMaintenanceTierPromotionResult,
184
197
  } from './memory/brain-maintenance.js';
185
198
  export { runBrainMaintenance } from './memory/brain-maintenance.js';
186
199
  export type { PurgeResult } from './memory/brain-purge.js';
@@ -46,6 +46,7 @@ vi.mock('../brain-retrieval.js', () => ({
46
46
  vi.mock('../brain-lifecycle.js', () => ({
47
47
  applyTemporalDecay: vi.fn().mockResolvedValue({ updated: 3 }),
48
48
  consolidateMemories: vi.fn().mockResolvedValue({ merged: 2, archived: 4 }),
49
+ runTierPromotion: vi.fn().mockResolvedValue({ promoted: [], evicted: [] }),
49
50
  }));
50
51
 
51
52
  // Mock auto-extract dependencies
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Tests for runTierPromotion in brain-lifecycle.ts (T614 fix).
3
+ *
4
+ * Verifies that tier promotion correctly promotes entries from short → medium
5
+ * and medium → long based on quality score, citation count, verification status,
6
+ * and age thresholds — WITHOUT requiring `verified = true` as a hard gate for
7
+ * the quality/citation tracks (T614 bug fix).
8
+ *
9
+ * @task T614
10
+ * @epic T569
11
+ */
12
+
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
14
+
15
+ // ============================================================================
16
+ // Hoisted mock factories
17
+ // ============================================================================
18
+
19
+ const { mockGetBrainDb, mockGetBrainNativeDb } = vi.hoisted(() => ({
20
+ mockGetBrainDb: vi.fn().mockResolvedValue({}),
21
+ mockGetBrainNativeDb: vi.fn(),
22
+ }));
23
+
24
+ vi.mock('../../store/brain-sqlite.js', () => ({
25
+ getBrainDb: mockGetBrainDb,
26
+ getBrainNativeDb: mockGetBrainNativeDb,
27
+ }));
28
+
29
+ // ============================================================================
30
+ // Import module under test (after all mocks)
31
+ // ============================================================================
32
+
33
+ import { runTierPromotion } from '../brain-lifecycle.js';
34
+
35
+ // ============================================================================
36
+ // Helpers
37
+ // ============================================================================
38
+
39
+ const PROJECT_ROOT = '/fake/project';
40
+
41
+ /** ISO datetime string for `daysAgo` days before now. */
42
+ function daysAgo(days: number): string {
43
+ return new Date(Date.now() - days * 24 * 60 * 60 * 1000)
44
+ .toISOString()
45
+ .replace('T', ' ')
46
+ .slice(0, 19);
47
+ }
48
+
49
+ type PreparedStmt = {
50
+ run: ReturnType<typeof vi.fn>;
51
+ get: ReturnType<typeof vi.fn>;
52
+ };
53
+
54
+ /** Build a minimal SQLite-like prepared statement stub. */
55
+ function makeStmt(rows: unknown[] = []): PreparedStmt {
56
+ return {
57
+ run: vi.fn().mockReturnValue({ changes: rows.length }),
58
+ get: vi.fn().mockReturnValue(rows[0] ?? undefined),
59
+ };
60
+ }
61
+
62
+ // ============================================================================
63
+ // Tests
64
+ // ============================================================================
65
+
66
+ describe('runTierPromotion', () => {
67
+ let capturedUpdates: Array<{ table: string; id: string; tier: string }> = [];
68
+
69
+ beforeEach(() => {
70
+ capturedUpdates = [];
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.clearAllMocks();
75
+ });
76
+
77
+ it('returns empty result when nativeDb is unavailable', async () => {
78
+ mockGetBrainNativeDb.mockReturnValue(null);
79
+
80
+ const result = await runTierPromotion(PROJECT_ROOT);
81
+
82
+ expect(result.promoted).toHaveLength(0);
83
+ expect(result.evicted).toHaveLength(0);
84
+ });
85
+
86
+ it('promotes unverified observation with quality_score >= 0.7 (T614 fix: no verified gate)', async () => {
87
+ // This is the core T614 regression test: an unverified (verified=0) observation
88
+ // with quality_score=0.8 older than 24h MUST promote to medium.
89
+ const shortObs = [
90
+ {
91
+ id: 'O-test-001',
92
+ citation_count: 0,
93
+ quality_score: 0.8,
94
+ verified: 0, // NOT verified — was blocked before T614 fix
95
+ },
96
+ ];
97
+
98
+ const nativeDb = buildMockDb({
99
+ brain_observations: {
100
+ shortToMedium: shortObs,
101
+ mediumToLong: [],
102
+ toEvict: [],
103
+ },
104
+ });
105
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
106
+
107
+ const result = await runTierPromotion(PROJECT_ROOT);
108
+
109
+ expect(result.promoted).toHaveLength(1);
110
+ expect(result.promoted[0]).toMatchObject({
111
+ id: 'O-test-001',
112
+ table: 'brain_observations',
113
+ fromTier: 'short',
114
+ toTier: 'medium',
115
+ });
116
+ expect(result.promoted[0]!.reason).toContain('qualityScore=0.80');
117
+ });
118
+
119
+ it('promotes unverified observation with citation_count >= 3 (T614 fix)', async () => {
120
+ const shortObs = [
121
+ {
122
+ id: 'O-test-002',
123
+ citation_count: 5,
124
+ quality_score: 0.4, // low quality but high citations
125
+ verified: 0,
126
+ },
127
+ ];
128
+
129
+ const nativeDb = buildMockDb({
130
+ brain_observations: {
131
+ shortToMedium: shortObs,
132
+ mediumToLong: [],
133
+ toEvict: [],
134
+ },
135
+ });
136
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
137
+
138
+ const result = await runTierPromotion(PROJECT_ROOT);
139
+
140
+ expect(result.promoted).toHaveLength(1);
141
+ expect(result.promoted[0]).toMatchObject({
142
+ id: 'O-test-002',
143
+ fromTier: 'short',
144
+ toTier: 'medium',
145
+ });
146
+ expect(result.promoted[0]!.reason).toContain('citationCount=5');
147
+ });
148
+
149
+ it('promotes verified observation with any quality (verified track)', async () => {
150
+ const shortObs = [
151
+ {
152
+ id: 'O-test-003',
153
+ citation_count: 0,
154
+ quality_score: 0.3, // low quality, low citations, but verified
155
+ verified: 1,
156
+ },
157
+ ];
158
+
159
+ const nativeDb = buildMockDb({
160
+ brain_observations: {
161
+ shortToMedium: shortObs,
162
+ mediumToLong: [],
163
+ toEvict: [],
164
+ },
165
+ });
166
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
167
+
168
+ const result = await runTierPromotion(PROJECT_ROOT);
169
+
170
+ expect(result.promoted).toHaveLength(1);
171
+ expect(result.promoted[0]).toMatchObject({
172
+ id: 'O-test-003',
173
+ fromTier: 'short',
174
+ toTier: 'medium',
175
+ });
176
+ expect(result.promoted[0]!.reason).toContain('verified=true');
177
+ });
178
+
179
+ it('promotes medium entry to long with citation_count >= 5 (no verified requirement)', async () => {
180
+ const mediumObs = [
181
+ {
182
+ id: 'O-test-004',
183
+ citation_count: 7,
184
+ quality_score: 0.9,
185
+ verified: 0, // not verified but high citations
186
+ },
187
+ ];
188
+
189
+ const nativeDb = buildMockDb({
190
+ brain_observations: {
191
+ shortToMedium: [],
192
+ mediumToLong: mediumObs,
193
+ toEvict: [],
194
+ },
195
+ });
196
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
197
+
198
+ const result = await runTierPromotion(PROJECT_ROOT);
199
+
200
+ expect(result.promoted).toHaveLength(1);
201
+ expect(result.promoted[0]).toMatchObject({
202
+ id: 'O-test-004',
203
+ fromTier: 'medium',
204
+ toTier: 'long',
205
+ });
206
+ expect(result.promoted[0]!.reason).toContain('citationCount=7');
207
+ });
208
+
209
+ it('promotes verified medium entry to long (accelerated track)', async () => {
210
+ const mediumObs = [
211
+ {
212
+ id: 'O-test-005',
213
+ citation_count: 1, // only 1 citation but verified
214
+ quality_score: 0.6,
215
+ verified: 1,
216
+ },
217
+ ];
218
+
219
+ const nativeDb = buildMockDb({
220
+ brain_observations: {
221
+ shortToMedium: [],
222
+ mediumToLong: mediumObs,
223
+ toEvict: [],
224
+ },
225
+ });
226
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
227
+
228
+ const result = await runTierPromotion(PROJECT_ROOT);
229
+
230
+ expect(result.promoted).toHaveLength(1);
231
+ expect(result.promoted[0]).toMatchObject({
232
+ id: 'O-test-005',
233
+ fromTier: 'medium',
234
+ toTier: 'long',
235
+ });
236
+ expect(result.promoted[0]!.reason).toContain('verified=true');
237
+ });
238
+
239
+ it('soft-evicts stale short entries with low quality and no promotion', async () => {
240
+ const staleObs = [
241
+ {
242
+ id: 'O-test-006',
243
+ quality_score: 0.1,
244
+ },
245
+ ];
246
+
247
+ const nativeDb = buildMockDb({
248
+ brain_observations: {
249
+ shortToMedium: [],
250
+ mediumToLong: [],
251
+ toEvict: staleObs,
252
+ },
253
+ });
254
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
255
+
256
+ const result = await runTierPromotion(PROJECT_ROOT);
257
+
258
+ expect(result.evicted).toHaveLength(1);
259
+ expect(result.evicted[0]).toMatchObject({
260
+ id: 'O-test-006',
261
+ table: 'brain_observations',
262
+ tier: 'short',
263
+ });
264
+ expect(result.promoted).toHaveLength(0);
265
+ });
266
+
267
+ it('processes all four memory tables', async () => {
268
+ // Each table should get one promotion
269
+ const obsRow = [{ id: 'O-obs', citation_count: 5, quality_score: 0.8, verified: 0 }];
270
+ const learningRow = [{ id: 'L-learn', citation_count: 0, quality_score: 0.75, verified: 0 }];
271
+ const patternRow = [{ id: 'P-pat', citation_count: 3, quality_score: 0.5, verified: 0 }];
272
+ const decisionRow = [{ id: 'D-dec', citation_count: 0, quality_score: 0.0, verified: 1 }];
273
+
274
+ const nativeDb = buildMockDb({
275
+ brain_observations: { shortToMedium: obsRow, mediumToLong: [], toEvict: [] },
276
+ brain_learnings: { shortToMedium: learningRow, mediumToLong: [], toEvict: [] },
277
+ brain_patterns: { shortToMedium: patternRow, mediumToLong: [], toEvict: [] },
278
+ brain_decisions: { shortToMedium: decisionRow, mediumToLong: [], toEvict: [] },
279
+ });
280
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
281
+
282
+ const result = await runTierPromotion(PROJECT_ROOT);
283
+
284
+ expect(result.promoted).toHaveLength(4);
285
+ const promotedTables = result.promoted.map((p) => p.table);
286
+ expect(promotedTables).toContain('brain_observations');
287
+ expect(promotedTables).toContain('brain_learnings');
288
+ expect(promotedTables).toContain('brain_patterns');
289
+ expect(promotedTables).toContain('brain_decisions');
290
+ });
291
+
292
+ it('returns empty result when no entries qualify', async () => {
293
+ const nativeDb = buildMockDb({
294
+ brain_observations: { shortToMedium: [], mediumToLong: [], toEvict: [] },
295
+ brain_learnings: { shortToMedium: [], mediumToLong: [], toEvict: [] },
296
+ brain_patterns: { shortToMedium: [], mediumToLong: [], toEvict: [] },
297
+ brain_decisions: { shortToMedium: [], mediumToLong: [], toEvict: [] },
298
+ });
299
+ mockGetBrainNativeDb.mockReturnValue(nativeDb);
300
+
301
+ const result = await runTierPromotion(PROJECT_ROOT);
302
+
303
+ expect(result.promoted).toHaveLength(0);
304
+ expect(result.evicted).toHaveLength(0);
305
+ });
306
+ });
307
+
308
+ // ============================================================================
309
+ // Helper: build a mock nativeDb that routes prepare() calls based on SQL content
310
+ // ============================================================================
311
+
312
+ type TableFixtures = {
313
+ shortToMedium: unknown[];
314
+ mediumToLong: unknown[];
315
+ toEvict: unknown[];
316
+ };
317
+
318
+ function buildMockDb(tables: Partial<Record<string, TableFixtures>>): {
319
+ prepare: ReturnType<typeof vi.fn>;
320
+ } {
321
+ const allTables = ['brain_observations', 'brain_learnings', 'brain_patterns', 'brain_decisions'];
322
+
323
+ // Normalise: every table gets a default empty fixture
324
+ const fixtures: Record<string, TableFixtures> = {};
325
+ for (const t of allTables) {
326
+ fixtures[t] = tables[t] ?? { shortToMedium: [], mediumToLong: [], toEvict: [] };
327
+ }
328
+
329
+ return {
330
+ prepare: vi.fn().mockImplementation((sql: string) => {
331
+ // Determine which table this statement targets
332
+ const targetTable = allTables.find((t) => sql.includes(t)) ?? 'brain_observations';
333
+ const fix = fixtures[targetTable]!;
334
+
335
+ // SELECT for eviction — identified by "(verified = 0 OR verified IS NULL)"
336
+ // Must be checked BEFORE the short-promotion check since both use memory_tier='short'
337
+ if (sql.includes('verified = 0') || sql.includes('verified IS NULL')) {
338
+ return {
339
+ all: vi.fn().mockReturnValue(fix.toEvict),
340
+ };
341
+ }
342
+ // SELECT for short→medium promotion — uses "(citation_count >= 3 OR quality_score >= 0.7 OR verified = 1)"
343
+ if (sql.includes("memory_tier = 'short'") && sql.includes('SELECT')) {
344
+ return {
345
+ all: vi.fn().mockReturnValue(fix.shortToMedium),
346
+ };
347
+ }
348
+ // SELECT for medium→long promotion
349
+ if (sql.includes("memory_tier = 'medium'") && sql.includes('SELECT')) {
350
+ return {
351
+ all: vi.fn().mockReturnValue(fix.mediumToLong),
352
+ };
353
+ }
354
+ // UPDATE statements (tier change, eviction)
355
+ if (sql.includes('UPDATE')) {
356
+ return {
357
+ run: vi.fn().mockReturnValue({ changes: 1 }),
358
+ };
359
+ }
360
+ // Fallback: SELECT returning empty
361
+ return {
362
+ all: vi.fn().mockReturnValue([]),
363
+ run: vi.fn().mockReturnValue({ changes: 0 }),
364
+ };
365
+ }),
366
+ };
367
+ }
@@ -381,14 +381,21 @@ export interface PromotionResult {
381
381
  /**
382
382
  * Run tier promotion for all memory tables.
383
383
  *
384
- * Promotion rules (per spec §1.1–§1.3):
385
- * - short → medium: citationCount >= 3 AND age > 24h AND verified = true
386
- * OR qualityScore >= 0.7 AND verified = true (fast-track)
387
- * - medium → long: citationCount >= 5 AND age > 7 days AND verified = true
384
+ * Promotion rules (per spec §1.1–§1.3, relaxed in T614):
385
+ * - short → medium:
386
+ * A. (citationCount >= 3 AND age > 24h) — citation-based track
387
+ * B. (qualityScore >= 0.7 AND age > 24h) quality fast-track
388
+ * C. (verified = true AND age > 24h) — owner-verified track
389
+ * Note: `verified` is no longer a hard gate for routes A and B.
390
+ * Requiring verified=true on all paths caused all 235 short-tier observations
391
+ * to be permanently stuck (T614 bug).
392
+ * - medium → long:
393
+ * (citationCount >= 5 AND age > 7 days) OR (verified = true AND age > 7 days)
394
+ * Verified entries accelerate to long-tier without citation threshold.
388
395
  *
389
396
  * Eviction rules:
390
- * - short-term entries older than shortTermEvictionDays (default 7) with no
391
- * promotion eligibility are soft-evicted (invalidAt = now).
397
+ * - short-term entries older than 7 days with no promotion eligibility are
398
+ * soft-evicted (invalidAt = now).
392
399
  * - long-term entries are NEVER auto-evicted.
393
400
  *
394
401
  * @param projectRoot - Project root directory for brain.db resolution
@@ -431,25 +438,30 @@ export async function runTierPromotion(projectRoot: string): Promise<PromotionRe
431
438
 
432
439
  for (const { table, dateCol } of tables) {
433
440
  // --- short → medium promotion ---
434
- // Two criteria (union):
435
- // A. citationCount >= 3 AND age > 24h AND verified = 1
436
- // B. quality_score >= 0.7 AND verified = 1 AND age > 24h (fast-track)
441
+ // Three criteria (union — verified is no longer a hard gate for A/B paths):
442
+ // A. citationCount >= 3 AND age > 24h (citation track — no verified requirement)
443
+ // B. quality_score >= 0.7 AND age > 24h (quality fast-track — no verified requirement)
444
+ // C. verified = 1 AND age > 24h (owner-verified track)
437
445
  interface TierRow {
438
446
  id: string;
439
447
  citation_count: number;
440
448
  quality_score: number | null;
449
+ verified: number;
441
450
  }
442
451
  let shortToMedium: TierRow[] = [];
443
452
  try {
444
453
  shortToMedium = typedAll<TierRow>(
445
454
  nativeDb.prepare(`
446
- SELECT id, citation_count, quality_score
455
+ SELECT id, citation_count, quality_score, verified
447
456
  FROM ${table}
448
457
  WHERE memory_tier = 'short'
449
458
  AND invalid_at IS NULL
450
459
  AND ${dateCol} < ?
451
- AND verified = 1
452
- AND (citation_count >= 3 OR quality_score >= 0.7)
460
+ AND (
461
+ citation_count >= 3
462
+ OR quality_score >= 0.7
463
+ OR verified = 1
464
+ )
453
465
  `),
454
466
  age24h,
455
467
  );
@@ -463,33 +475,34 @@ export async function runTierPromotion(projectRoot: string): Promise<PromotionRe
463
475
  nativeDb
464
476
  .prepare(`UPDATE ${table} SET memory_tier = 'medium', updated_at = ? WHERE id = ?`)
465
477
  .run(now, row.id);
466
- promoted.push({
467
- id: row.id,
468
- table,
469
- fromTier: 'short',
470
- toTier: 'medium',
471
- reason:
472
- row.citation_count >= 3
473
- ? `citationCount=${row.citation_count} >= 3, verified, age > 24h`
474
- : `qualityScore=${row.quality_score?.toFixed(2)} >= 0.70, verified, age > 24h`,
475
- });
478
+ let reason: string;
479
+ if (row.citation_count >= 3) {
480
+ reason = `citationCount=${row.citation_count} >= 3, age > 24h`;
481
+ } else if ((row.quality_score ?? 0) >= 0.7) {
482
+ reason = `qualityScore=${row.quality_score?.toFixed(2)} >= 0.70, age > 24h`;
483
+ } else {
484
+ reason = `verified=true, age > 24h`;
485
+ }
486
+ promoted.push({ id: row.id, table, fromTier: 'short', toTier: 'medium', reason });
476
487
  } catch {
477
488
  /* best-effort */
478
489
  }
479
490
  }
480
491
 
481
492
  // --- medium → long promotion ---
493
+ // Two criteria (union — verified accelerates to long without citation threshold):
494
+ // A. citationCount >= 5 AND age > 7d
495
+ // B. verified = 1 AND age > 7d (owner-verified accelerated track)
482
496
  let mediumToLong: TierRow[] = [];
483
497
  try {
484
498
  mediumToLong = typedAll<TierRow>(
485
499
  nativeDb.prepare(`
486
- SELECT id, citation_count, quality_score
500
+ SELECT id, citation_count, quality_score, verified
487
501
  FROM ${table}
488
502
  WHERE memory_tier = 'medium'
489
503
  AND invalid_at IS NULL
490
504
  AND ${dateCol} < ?
491
- AND verified = 1
492
- AND citation_count >= 5
505
+ AND (citation_count >= 5 OR verified = 1)
493
506
  `),
494
507
  age7d,
495
508
  );
@@ -502,13 +515,11 @@ export async function runTierPromotion(projectRoot: string): Promise<PromotionRe
502
515
  nativeDb
503
516
  .prepare(`UPDATE ${table} SET memory_tier = 'long', updated_at = ? WHERE id = ?`)
504
517
  .run(now, row.id);
505
- promoted.push({
506
- id: row.id,
507
- table,
508
- fromTier: 'medium',
509
- toTier: 'long',
510
- reason: `citationCount=${row.citation_count} >= 5, verified, age > 7d`,
511
- });
518
+ const reason =
519
+ row.citation_count >= 5
520
+ ? `citationCount=${row.citation_count} >= 5, age > 7d`
521
+ : `verified=true, age > 7d`;
522
+ promoted.push({ id: row.id, table, fromTier: 'medium', toTier: 'long', reason });
512
523
  } catch {
513
524
  /* best-effort */
514
525
  }