Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- T11522 (E6-L2): Add brain_task_observations as a forward Drizzle migration.
--
-- brain_task_observations (T1615) is the join table linking brain_observations to
-- task IDs; it powers `cleo memory find` session-context lookups. The runtime
-- writer is sessions/session-memory-bridge.ts.
--
-- It was previously created ONLY by a post-hoc `CREATE TABLE IF NOT EXISTS` in
-- memory-sqlite.ts::runBrainMigrations — it had NO Drizzle migration anywhere
-- (neither drizzle-brain nor the T11363 consolidation; exodus maps it to `null`
-- as a runtime-only cache). E6-L2 removes that inline band-aid and replaces it
-- with this forward migration, matching the T9179 precedent (ensureColumns /
-- self-healing DDL → forward Drizzle migration).
--
-- All statements are IF NOT EXISTS so re-running on a DB that already has the
-- table is idempotent and safe.
CREATE TABLE IF NOT EXISTS `brain_task_observations` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`observation_id` text NOT NULL,
`task_id` text NOT NULL,
`link_type` text DEFAULT 'session-completed' NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `idx_brain_task_obs_unique` ON `brain_task_observations` (`observation_id`, `task_id`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_brain_task_obs_observation` ON `brain_task_observations` (`observation_id`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_brain_task_obs_task` ON `brain_task_observations` (`task_id`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-- T11522 (E6-L2): Forward Drizzle migrations for the three brain tables that had
-- NO drizzle-brain migration and were created ONLY by post-hoc
-- `CREATE TABLE IF NOT EXISTS` band-aids in memory-sqlite.ts::runBrainMigrations.
--
-- E6-L2 removes those inline band-aids and replaces them with this forward
-- migration (T11522 AC: post-hoc DDL → forward Drizzle migration), matching the
-- T9179 precedent. The schemas reproduce the removed inline DDL EXACTLY (legacy
-- runtime shape) so the brain runtime writers continue to work unchanged:
--
-- 1. brain_transcript_events (T1002) — full-fidelity Claude session block store.
-- 2. brain_promotion_log (T1001/T1903) — typed promotion audit trail (includes
-- the T1903 fulfilled_at / fulfillment_note columns folded into the CREATE).
-- 3. brain_backfill_runs (T1003) — staged backfill audit log.
--
-- All statements are IF NOT EXISTS so re-running on a DB that already has the
-- tables is idempotent and safe.
CREATE TABLE IF NOT EXISTS `brain_transcript_events` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`seq` integer NOT NULL,
`role` text NOT NULL,
`block_type` text NOT NULL,
`content` text NOT NULL,
`tokens` integer,
`redacted_at` text,
`created_at` text DEFAULT (datetime('now')) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS `idx_transcript_events_session_seq` ON `brain_transcript_events` (`session_id`, `seq`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_transcript_events_session` ON `brain_transcript_events` (`session_id`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_transcript_events_role` ON `brain_transcript_events` (`role`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_transcript_events_block_type` ON `brain_transcript_events` (`block_type`);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `brain_promotion_log` (
`id` text PRIMARY KEY,
`observation_id` text NOT NULL,
`from_tier` text NOT NULL,
`to_tier` text NOT NULL,
`score` real NOT NULL,
`decided_at` text DEFAULT (datetime('now')) NOT NULL,
`decided_by` text DEFAULT 'composite-scorer' NOT NULL,
`rationale_json` text,
`fulfilled_at` text,
`fulfillment_note` text
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_promotion_log_observation` ON `brain_promotion_log` (`observation_id`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_promotion_log_decided_at` ON `brain_promotion_log` (`decided_at`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_promotion_log_to_tier` ON `brain_promotion_log` (`to_tier`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_promotion_log_score` ON `brain_promotion_log` (`score`);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `brain_backfill_runs` (
`id` text PRIMARY KEY,
`kind` text NOT NULL,
`status` text DEFAULT 'staged' NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`approved_at` text,
`rows_affected` integer DEFAULT 0 NOT NULL,
`rollback_snapshot_json` text,
`source` text DEFAULT 'unknown' NOT NULL,
`target_table` text DEFAULT 'brain_observations' NOT NULL,
`approved_by` text
);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_backfill_runs_status` ON `brain_backfill_runs` (`status`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_backfill_runs_kind` ON `brain_backfill_runs` (`kind`);
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS `idx_backfill_runs_created_at` ON `brain_backfill_runs` (`created_at`);
12 changes: 7 additions & 5 deletions packages/core/src/__tests__/scaffold-characterization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,16 +409,18 @@ describe('characterization: ensureBrainDb', () => {
else delete process.env['CLEO_DIR'];
});

it('returns action=skipped when brain.db already exists', async () => {
writeFileSync(join(tmpDir, '.cleo', 'brain.db'), 'SQLite format 3');
// E6-L2 (T11522): ensureBrainDb now probes the consolidated `cleo.db`
// (getBrainDb → openDualScopeDb('project')), not a standalone `brain.db`.
it('returns action=skipped when cleo.db already exists', async () => {
writeFileSync(join(tmpDir, '.cleo', 'cleo.db'), 'SQLite format 3');
const result = await ensureBrainDb(tmpDir);
expect(result.action).toBe('skipped');
});

it('result path points to brain.db', async () => {
writeFileSync(join(tmpDir, '.cleo', 'brain.db'), 'SQLite format 3');
it('result path points to cleo.db', async () => {
writeFileSync(join(tmpDir, '.cleo', 'cleo.db'), 'SQLite format 3');
const result = await ensureBrainDb(tmpDir);
expect(result.path).toContain('brain.db');
expect(result.path).toContain('cleo.db');
});
});

Expand Down
29 changes: 20 additions & 9 deletions packages/core/src/scaffold/ensure-dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,27 @@ export async function ensureSqliteDb(projectRoot: string): Promise<ScaffoldResul
}

/**
* Create brain.db if missing.
* Idempotent: skips if brain.db already exists.
* Initialize the brain domain if missing.
*
* ## E6-L2 (T11522)
*
* The brain domain now lives inside the consolidated project `cleo.db`
* (`getBrainDb` → `openDualScopeDb('project')`), not a standalone `brain.db`.
* The idempotency probe therefore targets the consolidated `cleo.db` path so a
* second scaffold run correctly reports `skipped` rather than always `created`.
*
* @param projectRoot - Absolute path to the project root directory
* @returns Scaffold result indicating the action taken
*/
export async function ensureBrainDb(projectRoot: string): Promise<ScaffoldResult> {
const cleoDir = resolveScaffoldCleoDir(projectRoot);
const dbPath = join(cleoDir, 'brain.db');
const { getBrainDbPath, getBrainDb, getBrainNativeDb } = await import(
'../store/memory-sqlite.js'
);
// E6-L2: getBrainDbPath resolves to the consolidated `cleo.db`.
const dbPath = getBrainDbPath(projectRoot);

if (existsSync(dbPath)) {
try {
const { getBrainNativeDb } = await import('../store/memory-sqlite.js');
const nativeDb = getBrainNativeDb();
if (nativeDb) {
const { ensureFts5Tables } = await import('../memory/brain-search.js');
Expand All @@ -227,11 +235,10 @@ export async function ensureBrainDb(projectRoot: string): Promise<ScaffoldResult
} catch {
// Non-fatal
}
return { action: 'skipped', path: dbPath, details: 'brain.db already exists' };
return { action: 'skipped', path: dbPath, details: 'brain domain (cleo.db) already exists' };
}

try {
const { getBrainDb, getBrainNativeDb } = await import('../store/memory-sqlite.js');
await getBrainDb(projectRoot);

try {
Expand All @@ -244,12 +251,16 @@ export async function ensureBrainDb(projectRoot: string): Promise<ScaffoldResult
// FTS5 may not be available — non-fatal
}

return { action: 'created', path: dbPath, details: 'Brain database initialized with FTS5' };
return {
action: 'created',
path: dbPath,
details: 'Brain domain initialized in cleo.db with FTS5',
};
} catch (err) {
return {
action: 'skipped',
path: dbPath,
details: `Failed to initialize brain.db: ${err instanceof Error ? err.message : String(err)}`,
details: `Failed to initialize brain domain: ${err instanceof Error ? err.message : String(err)}`,
};
}
}
14 changes: 9 additions & 5 deletions packages/core/src/scaffold/project-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,21 +406,25 @@ export function checkSqliteDb(projectRoot: string): CheckResult {
}

/**
* Verify .cleo/brain.db exists and is non-empty.
* Verify the brain domain database exists and is non-empty.
*
* E6-L2 (T11522): the brain domain consolidated into the project `cleo.db`
* (`getBrainDb` → `openDualScopeDb('project')`), so this probe targets `cleo.db`
* rather than a standalone `brain.db`.
*
* @param projectRoot - Absolute path to the project root directory
* @returns Check result with database existence and size information
*/
export function checkBrainDb(projectRoot: string): CheckResult {
const cleoDir = join(projectRoot, '.cleo');
const dbPath = join(cleoDir, 'brain.db');
const dbPath = join(cleoDir, 'cleo.db');

if (!existsSync(dbPath)) {
return {
id: 'brain_db',
category: 'scaffold',
status: 'failed',
message: 'brain.db not found',
message: 'brain domain (cleo.db) not found',
details: { path: dbPath, exists: false },
fix: 'cleo init',
};
Expand All @@ -432,7 +436,7 @@ export function checkBrainDb(projectRoot: string): CheckResult {
id: 'brain_db',
category: 'scaffold',
status: 'warning',
message: 'brain.db exists but is empty (0 bytes)',
message: 'brain domain (cleo.db) exists but is empty (0 bytes)',
details: { path: dbPath, exists: true, size: 0 },
fix: 'cleo upgrade',
};
Expand All @@ -442,7 +446,7 @@ export function checkBrainDb(projectRoot: string): CheckResult {
id: 'brain_db',
category: 'scaffold',
status: 'passed',
message: `brain.db exists (${stat.size} bytes)`,
message: `brain domain (cleo.db) exists (${stat.size} bytes)`,
details: { path: dbPath, exists: true, size: stat.size },
fix: null,
};
Expand Down
Loading
Loading