Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@four-bytes/four-opencode-brain",
"version": "1.5.1",
"version": "1.5.0",
"description": "Unified brain plugin — single SQLite DB for RAG search, memory, and knowledge base with content-hash dedup and per-session LRU cache",
"license": "Apache-2.0",
"type": "module",
Expand All @@ -18,7 +18,7 @@
"four-bytes"
],
"dependencies": {
"@four-bytes/opencode-plugin-lib": "github:four-bytes/opencode-plugin-lib#v0.2.0",
"@four-bytes/opencode-plugin-lib": "github:four-bytes/opencode-plugin-lib",
"@opencode-ai/plugin": "1.16.2",
"@opentui/core": "0.3.2",
"@opentui/solid": "0.3.2",
Expand Down
6 changes: 6 additions & 0 deletions src/knowledge/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ export function kbRecord(db: Database, input: KbRecordInput): KbOccurrence {
throw new Error(`Entry not found: ${input.entry_key} (kind: ${input.kind})`);
}

// Validate outcome
const VALID_OUTCOMES = ["fixed", "failed", "workaround", "observed"];
if (!VALID_OUTCOMES.includes(input.outcome)) {
throw new Error(`Invalid outcome: "${input.outcome}". Must be one of: ${VALID_OUTCOMES.join(", ")}`);
}

const id = generateId();
db.run(
`INSERT INTO knowledge_occurrences (id, entry_key, kind, project_ref, repo_ref, issue_ref, commit_ref, observed_symptoms, outcome)
Expand Down
202 changes: 199 additions & 3 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ export function createSchema(db: Database): void {
root_cause TEXT,
canonical_solution TEXT,
tags TEXT,
confidence REAL NOT NULL DEFAULT 0.0,
review_state TEXT NOT NULL DEFAULT 'draft',
confidence REAL NOT NULL DEFAULT 0.0 CHECK(confidence >= 0.0 AND confidence <= 1.0),
review_state TEXT NOT NULL DEFAULT 'draft' CHECK(review_state IN ('draft','reviewed','accepted','rejected','superseded')),
superseded_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT,
Expand All @@ -357,6 +357,10 @@ export function createSchema(db: Database): void {
`);

// ---- Migration: add entity_type CHECK for existing databases -----------
migrateConfidenceCheck(db);
migrateReviewStateCheck(db);
migrateOccurrenceOutcomeCheck(db);
migrateKnowledgeFts(db);
migrateEntityTypeCheck(db);

db.exec(`
Expand All @@ -369,7 +373,7 @@ export function createSchema(db: Database): void {
issue_ref TEXT,
commit_ref TEXT,
observed_symptoms TEXT,
outcome TEXT NOT NULL,
outcome TEXT NOT NULL CHECK(outcome IN ('fixed','failed','workaround','observed')),
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (entry_key, kind) REFERENCES knowledge_entries(entry_key, kind) ON DELETE CASCADE
)
Expand Down Expand Up @@ -700,3 +704,195 @@ function migrateEntityTypeCheck(db: Database): void {
}
}
}

// ---- KB quality migration: confidence CHECK constraint ------------------

function migrateConfidenceCheck(db: Database): void {
for (let attempt = 0; attempt < 3; attempt++) {
try {
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_entries_check_confidence_bi
BEFORE INSERT ON knowledge_entries
BEGIN
SELECT CASE
WHEN NEW.confidence < 0.0 OR NEW.confidence > 1.0
THEN RAISE(ABORT, 'Invalid confidence: ' || NEW.confidence || ' (must be 0.0-1.0)')
END;
END
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_entries_check_confidence_bu
BEFORE UPDATE ON knowledge_entries
BEGIN
SELECT CASE
WHEN NEW.confidence < 0.0 OR NEW.confidence > 1.0
THEN RAISE(ABORT, 'Invalid confidence: ' || NEW.confidence || ' (must be 0.0-1.0)')
END;
END
`);
log("info", "schema", "Applied confidence CHECK migration via triggers");
return;
} catch (err) {
if (attempt < 2) {
log("warn", "schema", `confidence migration attempt ${attempt + 1} failed (locked), retrying...`);
Bun.sleepSync(1000);
} else {
log("warn", "schema", `confidence CHECK migration deferred: ${String(err)}`);
}
}
}
}

// ---- KB quality migration: review_state CHECK constraint ------------------

function migrateReviewStateCheck(db: Database): void {
for (let attempt = 0; attempt < 3; attempt++) {
try {
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_entries_check_review_state_bi
BEFORE INSERT ON knowledge_entries
BEGIN
SELECT CASE
WHEN NEW.review_state NOT IN ('draft','reviewed','accepted','rejected','superseded')
THEN RAISE(ABORT, 'Invalid review_state: ' || NEW.review_state)
END;
END
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_entries_check_review_state_bu
BEFORE UPDATE ON knowledge_entries
BEGIN
SELECT CASE
WHEN NEW.review_state NOT IN ('draft','reviewed','accepted','rejected','superseded')
THEN RAISE(ABORT, 'Invalid review_state: ' || NEW.review_state)
END;
END
`);
log("info", "schema", "Applied review_state CHECK migration via triggers");
return;
} catch (err) {
if (attempt < 2) {
log("warn", "schema", `review_state migration attempt ${attempt + 1} failed (locked), retrying...`);
Bun.sleepSync(1000);
} else {
log("warn", "schema", `review_state CHECK migration deferred: ${String(err)}`);
}
}
}
}

// ---- KB quality migration: occurrence outcome CHECK constraint -----------

function migrateOccurrenceOutcomeCheck(db: Database): void {
for (let attempt = 0; attempt < 3; attempt++) {
try {
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_occurrences_check_outcome_bi
BEFORE INSERT ON knowledge_occurrences
BEGIN
SELECT CASE
WHEN NEW.outcome NOT IN ('fixed','failed','workaround','observed')
THEN RAISE(ABORT, 'Invalid outcome: ' || NEW.outcome)
END;
END
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS knowledge_occurrences_check_outcome_bu
BEFORE UPDATE ON knowledge_occurrences
BEGIN
SELECT CASE
WHEN NEW.outcome NOT IN ('fixed','failed','workaround','observed')
THEN RAISE(ABORT, 'Invalid outcome: ' || NEW.outcome)
END;
END
`);
log("info", "schema", "Applied occurrence outcome CHECK migration via triggers");
return;
} catch (err) {
if (attempt < 2) {
log("warn", "schema", `outcome migration attempt ${attempt + 1} failed (locked), retrying...`);
Bun.sleepSync(1000);
} else {
log("warn", "schema", `outcome CHECK migration deferred: ${String(err)}`);
}
}
}
}

// ---- KB quality migration: rebuild FTS with entry_key/kind/entity_type -----

function migrateKnowledgeFts(db: Database): void {
for (let attempt = 0; attempt < 3; attempt++) {
try {
// Check if FTS already has the new columns
const ftsInfo = db.query<{ name: string }, []>(
"SELECT name FROM pragma_table_info('entries_fts') WHERE name = 'entry_key'"
).get();
if (ftsInfo) {
log("debug", "schema", "FTS already has entry_key column — skipping rebuild");
return;
}

// Drop old FTS (non-destructive — content table untouched)
db.run("DROP TABLE IF EXISTS entries_fts");

// Recreate with full column coverage
db.exec(`
CREATE VIRTUAL TABLE entries_fts USING fts5(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
entry_key,
kind,
entity_type,
title,
description,
root_cause,
canonical_solution,
tags,
content='knowledge_entries',
content_rowid='rowid'
)
`);

// Rebuild FTS index from existing data
db.run("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
log("info", "schema", "Rebuilt knowledge FTS with entry_key, kind, entity_type columns");

// Recreate FTS sync triggers with full column coverage
db.exec("DROP TRIGGER IF EXISTS entries_ai");
db.exec("DROP TRIGGER IF EXISTS entries_ad");
db.exec("DROP TRIGGER IF EXISTS entries_au");

db.exec(`
CREATE TRIGGER entries_ai AFTER INSERT ON knowledge_entries BEGIN
INSERT INTO entries_fts(rowid, entry_key, kind, entity_type, title, description, root_cause, canonical_solution, tags)
VALUES (new.rowid, new.entry_key, new.kind, new.entity_type, new.title, new.description, new.root_cause, new.canonical_solution, new.tags);
END
`);

db.exec(`
CREATE TRIGGER entries_ad AFTER DELETE ON knowledge_entries BEGIN
INSERT INTO entries_fts(entries_fts, rowid, entry_key, kind, entity_type, title, description, root_cause, canonical_solution, tags)
VALUES ('delete', old.rowid, old.entry_key, old.kind, old.entity_type, old.title, old.description, old.root_cause, old.canonical_solution, old.tags);
END
`);

db.exec(`
CREATE TRIGGER entries_au AFTER UPDATE ON knowledge_entries BEGIN
INSERT INTO entries_fts(entries_fts, rowid, entry_key, kind, entity_type, title, description, root_cause, canonical_solution, tags)
VALUES ('delete', old.rowid, old.entry_key, old.kind, old.entity_type, old.title, old.description, old.root_cause, old.canonical_solution, old.tags);
INSERT INTO entries_fts(rowid, entry_key, kind, entity_type, title, description, root_cause, canonical_solution, tags)
VALUES (new.rowid, new.entry_key, new.kind, new.entity_type, new.title, new.description, new.root_cause, new.canonical_solution, new.tags);
END
`);

log("info", "schema", "Recreated FTS sync triggers with full 8-column coverage");
return;
} catch (err) {
if (attempt < 2) {
log("warn", "schema", `FTS migration attempt ${attempt + 1} failed (locked), retrying...`);
Bun.sleepSync(1000);
} else {
log("warn", "schema", `FTS migration deferred: ${String(err)}`);
}
}
}
}
Loading