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
244 changes: 214 additions & 30 deletions brain-bar/Sources/BrainBar/BrainDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3552,6 +3552,29 @@ final class BrainDatabase: @unchecked Sendable {
let entityType: String
let description: String?
let importance: Double
let linkedChunkCount: Int

init(
id: String,
name: String,
entityType: String,
description: String?,
importance: Double,
linkedChunkCount: Int = 0
) {
self.id = id
self.name = name
self.entityType = entityType
self.description = description
self.importance = importance
self.linkedChunkCount = linkedChunkCount
}
}

private struct KGEntityAliasSeed {
let id: String
let name: String
let entityType: String
}

struct KGRelationRow: Equatable, Sendable {
Expand Down Expand Up @@ -3659,15 +3682,28 @@ final class BrainDatabase: @unchecked Sendable {
defer { sqlite3_finalize(stmt) }
sqlite3_bind_int(stmt, 1, Int32(limit))

var rows: [KGEntityRow] = []
var seeds: [KGEntityAliasSeed] = []
var descriptions: [String: String?] = [:]
var importances: [String: Double] = [:]
while sqlite3_step(stmt) == SQLITE_ROW {
rows.append(KGEntityRow(
id: columnText(stmt, 0) ?? "",
name: columnText(stmt, 1) ?? "",
entityType: columnText(stmt, 2) ?? "",
description: columnText(stmt, 3),
importance: sqlite3_column_double(stmt, 4)
))
let id = columnText(stmt, 0) ?? ""
let name = columnText(stmt, 1) ?? ""
let entityType = columnText(stmt, 2) ?? ""
seeds.append(KGEntityAliasSeed(id: id, name: name, entityType: entityType))
descriptions[id] = columnText(stmt, 3)
importances[id] = sqlite3_column_double(stmt, 4)
}

let linkedChunkCounts = try fetchLinkedChunkCounts(for: seeds)
let rows = seeds.map { seed in
KGEntityRow(
id: seed.id,
name: seed.name,
entityType: seed.entityType,
description: descriptions[seed.id] ?? nil,
importance: importances[seed.id] ?? 5.0,
linkedChunkCount: linkedChunkCounts[seed.id, default: 0]
)
}
return rows
}
Expand Down Expand Up @@ -3733,61 +3769,70 @@ final class BrainDatabase: @unchecked Sendable {
}

func fetchEntityChunkCount(entityId: String) throws -> Int {
let entityIds = try entityAliasGroupIDs(entityId: entityId)
let sql = """
SELECT COUNT(*)
SELECT COUNT(DISTINCT ec.chunk_id)
FROM kg_entity_chunks ec
JOIN chunks c ON c.id = ec.chunk_id
WHERE ec.entity_id = ?
WHERE ec.entity_id IN (\(placeholders(count: entityIds.count)))
"""
return try fetchCount(sql: sql, entityId: entityId)
return try fetchCount(sql: sql, entityIds: entityIds)
}

func fetchEntitySourceFileCount(entityId: String) throws -> Int {
guard let db else { throw DBError.notOpen }
let entityIds = try entityAliasGroupIDs(entityId: entityId)
let sql = """
SELECT COUNT(DISTINCT c.source_file)
FROM kg_entity_chunks ec
JOIN chunks c ON c.id = ec.chunk_id
WHERE ec.entity_id = ?
WHERE ec.entity_id IN (\(placeholders(count: entityIds.count)))
AND NULLIF(c.source_file, '') IS NOT NULL
"""
return try fetchCount(sql: sql, entityId: entityId, db: db)
return try fetchCount(sql: sql, entityIds: entityIds, db: db)
}

func fetchEntityChunksPage(entityId: String, after: ChunkCursor?, limit: Int) throws -> ChunkPage {
guard let db else { throw DBError.notOpen }
let pageLimit = max(0, limit)
guard pageLimit > 0 else { return ChunkPage(rows: [], nextCursor: nil) }
let entityIds = try entityAliasGroupIDs(entityId: entityId)

let cursorPredicate: String
if after == nil {
cursorPredicate = ""
} else {
cursorPredicate = """
AND (
ec.relevance < ?
OR (ec.relevance = ? AND COALESCE(c.created_at, '') < ?)
OR (ec.relevance = ? AND COALESCE(c.created_at, '') = ? AND c.id < ?)
WHERE (
relevance < ?
OR (relevance = ? AND created_at < ?)
OR (relevance = ? AND created_at = ? AND chunk_id < ?)
)
"""
}
let sql = """
SELECT c.id, COALESCE(NULLIF(c.summary, ''), substr(c.content, 1, 200)) AS snippet,
c.importance, ec.relevance, COALESCE(c.created_at, '') AS created_at
FROM kg_entity_chunks ec
JOIN chunks c ON c.id = ec.chunk_id
WHERE ec.entity_id = ?
SELECT chunk_id, snippet, importance, relevance, created_at
FROM (
SELECT c.id AS chunk_id,
COALESCE(NULLIF(c.summary, ''), substr(c.content, 1, 200)) AS snippet,
c.importance AS importance,
MAX(ec.relevance) AS relevance,
COALESCE(c.created_at, '') AS created_at
FROM kg_entity_chunks ec
JOIN chunks c ON c.id = ec.chunk_id
WHERE ec.entity_id IN (\(placeholders(count: entityIds.count)))
GROUP BY c.id
)
\(cursorPredicate)
ORDER BY ec.relevance DESC, COALESCE(c.created_at, '') DESC, c.id DESC
ORDER BY relevance DESC, created_at DESC, chunk_id DESC
LIMIT ?
"""
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(stmt) }
bindText(entityId, to: stmt, index: 1)
var bindIndex: Int32 = 2
var bindIndex = bindEntityIDs(entityIds, to: stmt, startingAt: 1)
if let after {
sqlite3_bind_double(stmt, bindIndex, after.relevance)
bindIndex += 1
Expand Down Expand Up @@ -3832,6 +3877,7 @@ final class BrainDatabase: @unchecked Sendable {
guard let db else { throw DBError.notOpen }
let pageLimit = max(0, limit)
guard pageLimit > 0 else { return SourceFilePage(rows: [], nextCursor: nil) }
let entityIds = try entityAliasGroupIDs(entityId: entityId)

let cursorPredicate: String
if after == nil {
Expand All @@ -3849,11 +3895,11 @@ final class BrainDatabase: @unchecked Sendable {
SELECT source_file, chunk_count, top_relevance
FROM (
SELECT c.source_file AS source_file,
COUNT(*) AS chunk_count,
COUNT(DISTINCT c.id) AS chunk_count,
MAX(ec.relevance) AS top_relevance
FROM kg_entity_chunks ec
JOIN chunks c ON c.id = ec.chunk_id
WHERE ec.entity_id = ?
WHERE ec.entity_id IN (\(placeholders(count: entityIds.count)))
AND NULLIF(c.source_file, '') IS NOT NULL
GROUP BY c.source_file
)
Expand All @@ -3866,8 +3912,7 @@ final class BrainDatabase: @unchecked Sendable {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(stmt) }
bindText(entityId, to: stmt, index: 1)
var bindIndex: Int32 = 2
var bindIndex = bindEntityIDs(entityIds, to: stmt, startingAt: 1)
if let after {
sqlite3_bind_double(stmt, bindIndex, after.topRelevance)
bindIndex += 1
Expand Down Expand Up @@ -3920,20 +3965,159 @@ final class BrainDatabase: @unchecked Sendable {
}

private func fetchCount(sql: String, entityId: String, db providedDB: OpaquePointer? = nil) throws -> Int {
try fetchCount(sql: sql, entityIds: [entityId], db: providedDB)
}

private func fetchCount(sql: String, entityIds: [String], db providedDB: OpaquePointer? = nil) throws -> Int {
guard let db = providedDB ?? self.db else { throw DBError.notOpen }
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(stmt) }
bindText(entityId, to: stmt, index: 1)
_ = bindEntityIDs(entityIds, to: stmt, startingAt: 1)
let stepRC = sqlite3_step(stmt)
guard stepRC == SQLITE_ROW else {
throw DBError.step(stepRC)
}
return Int(sqlite3_column_int(stmt, 0))
}

private func entityAliasGroupIDs(entityId: String) throws -> [String] {
guard let db else { throw DBError.notOpen }
let entitySQL = "SELECT name, entity_type FROM kg_entities WHERE id = ?"
var entityStmt: OpaquePointer?
guard sqlite3_prepare_v2(db, entitySQL, -1, &entityStmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(entityStmt) }
bindText(entityId, to: entityStmt, index: 1)
guard sqlite3_step(entityStmt) == SQLITE_ROW else {
return [entityId]
}
var orderedIds = [entityId]
var seen = Set(orderedIds)
func append(_ id: String) {
guard !id.isEmpty, !seen.contains(id) else { return }
seen.insert(id)
orderedIds.append(id)
}

let aliasEntitySQL = """
SELECT DISTINCT aliasEntity.id
FROM kg_entity_aliases a
JOIN kg_entities canonical ON canonical.id = a.entity_id
JOIN kg_entities aliasEntity ON lower(aliasEntity.name) = lower(a.alias)
WHERE a.entity_id IN (\(placeholders(count: orderedIds.count)))
AND aliasEntity.entity_type = canonical.entity_type
ORDER BY aliasEntity.id
Comment on lines +4011 to +4013
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expand alias membership transitively before counting

entityAliasGroupIDs only runs the alias lookup once against the initial entityId, so aliases discovered in that pass are appended but never queried for their own aliases. In a chain like A→B and B→C, sidebar totals/pagination for A will include B’s chunks but silently miss C’s, which undercounts evidence and breaks the intended alias-group aggregation behavior in fetchEntityChunkCount, fetchEntitySourceFileCount, and page queries.

Useful? React with 👍 / 👎.

"""
var aliasStmt: OpaquePointer?
guard sqlite3_prepare_v2(db, aliasEntitySQL, -1, &aliasStmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(aliasStmt) }
_ = bindEntityIDs(orderedIds, to: aliasStmt, startingAt: 1)
while sqlite3_step(aliasStmt) == SQLITE_ROW {
append(columnText(aliasStmt, 0) ?? "")
}
Comment on lines +4006 to +4023
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ambiguous alias surfaces can still leak into canonical groups

Line 4021 matches alias entities by surface for every canonical ID in scope, but there’s no uniqueness guard on that surface. If the same alias maps to multiple canonical entities of the same type, those ambiguous alias rows can still be absorbed into canonical groups.

💡 Proposed SQL guard
         let aliasEntitySQL = """
             SELECT DISTINCT aliasEntity.id
             FROM kg_entity_aliases a
             JOIN kg_entities canonical ON canonical.id = a.entity_id
             JOIN kg_entities aliasEntity ON lower(aliasEntity.name) = lower(a.alias)
             WHERE a.entity_id IN (\(placeholders(count: orderedIds.count)))
               AND aliasEntity.entity_type = canonical.entity_type
+              AND (
+                  SELECT COUNT(DISTINCT a2.entity_id)
+                  FROM kg_entity_aliases a2
+                  JOIN kg_entities canonical2 ON canonical2.id = a2.entity_id
+                  WHERE lower(a2.alias) = lower(a.alias)
+                    AND canonical2.entity_type = canonical.entity_type
+              ) = 1
             ORDER BY aliasEntity.id
         """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let aliasEntitySQL = """
SELECT DISTINCT aliasEntity.id
FROM kg_entity_aliases a
JOIN kg_entities canonical ON canonical.id = a.entity_id
JOIN kg_entities aliasEntity ON lower(aliasEntity.name) = lower(a.alias)
WHERE a.entity_id IN (\(placeholders(count: orderedIds.count)))
AND aliasEntity.entity_type = canonical.entity_type
ORDER BY aliasEntity.id
"""
var aliasStmt: OpaquePointer?
guard sqlite3_prepare_v2(db, aliasEntitySQL, -1, &aliasStmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(aliasStmt) }
_ = bindEntityIDs(orderedIds, to: aliasStmt, startingAt: 1)
while sqlite3_step(aliasStmt) == SQLITE_ROW {
append(columnText(aliasStmt, 0) ?? "")
}
let aliasEntitySQL = """
SELECT DISTINCT aliasEntity.id
FROM kg_entity_aliases a
JOIN kg_entities canonical ON canonical.id = a.entity_id
JOIN kg_entities aliasEntity ON lower(aliasEntity.name) = lower(a.alias)
WHERE a.entity_id IN (\(placeholders(count: orderedIds.count)))
AND aliasEntity.entity_type = canonical.entity_type
AND (
SELECT COUNT(DISTINCT a2.entity_id)
FROM kg_entity_aliases a2
JOIN kg_entities canonical2 ON canonical2.id = a2.entity_id
WHERE lower(a2.alias) = lower(a.alias)
AND canonical2.entity_type = canonical.entity_type
) = 1
ORDER BY aliasEntity.id
"""
var aliasStmt: OpaquePointer?
guard sqlite3_prepare_v2(db, aliasEntitySQL, -1, &aliasStmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(aliasStmt) }
_ = bindEntityIDs(orderedIds, to: aliasStmt, startingAt: 1)
while sqlite3_step(aliasStmt) == SQLITE_ROW {
append(columnText(aliasStmt, 0) ?? "")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@brain-bar/Sources/BrainBar/BrainDatabase.swift` around lines 4017 - 4034, The
current aliasEntitySQL can pull alias surfaces that map to multiple canonical
entities and thus leak ambiguous aliases into canonical groups; modify the query
(aliasEntitySQL) to restrict to alias surfaces that are unique within the
provided orderedIds set by grouping on lower(a.alias) and adding a HAVING
COUNT(DISTINCT a.entity_id) = 1 (or equivalently join to a subquery that selects
only alias values with a single distinct entity_id for the given IN (...) list),
then keep the existing join to kg_entities aliasEntity and selection/order
logic; update the prepared statement usage (aliasStmt / bindEntityIDs / append)
accordingly so only uniquely-mapped aliasEntity.id rows are returned.


return orderedIds
}

private func placeholders(count: Int) -> String {
Array(repeating: "?", count: max(1, count)).joined(separator: ", ")
}

private func fetchLinkedChunkCounts(for entities: [KGEntityAliasSeed]) throws -> [String: Int] {
guard let db else { throw DBError.notOpen }
guard !entities.isEmpty else { return [:] }

let visibleIds = entities.map(\.id)
var groupMembers = Dictionary(uniqueKeysWithValues: entities.map { ($0.id, [$0.id]) })
let canonicalIds = visibleIds
let aliasMembers = try aliasEntityIDsByCanonicalID(canonicalIds: canonicalIds, db: db)
for entity in entities {
if let aliases = aliasMembers[entity.id] {
groupMembers[entity.id, default: [entity.id]].append(contentsOf: aliases)
}
}

for (groupId, members) in groupMembers {
var seen: Set<String> = []
groupMembers[groupId] = members.filter { seen.insert($0).inserted }
}
return try fetchDistinctChunkCounts(groupMembers: groupMembers, db: db)
}

private func aliasEntityIDsByCanonicalID(canonicalIds: [String], db: OpaquePointer) throws -> [String: [String]] {
guard !canonicalIds.isEmpty else { return [:] }
let sql = """
SELECT a.entity_id, aliasEntity.id
FROM kg_entity_aliases a
JOIN kg_entities canonical ON canonical.id = a.entity_id
JOIN kg_entities aliasEntity ON lower(aliasEntity.name) = lower(a.alias)
WHERE a.entity_id IN (\(placeholders(count: canonicalIds.count)))
AND aliasEntity.entity_type = canonical.entity_type
ORDER BY a.entity_id, aliasEntity.id
"""
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(stmt) }
_ = bindEntityIDs(canonicalIds, to: stmt, startingAt: 1)
var aliases: [String: [String]] = [:]
while sqlite3_step(stmt) == SQLITE_ROW {
let canonicalId = columnText(stmt, 0) ?? ""
let aliasId = columnText(stmt, 1) ?? ""
guard !canonicalId.isEmpty, !aliasId.isEmpty else { continue }
aliases[canonicalId, default: []].append(aliasId)
}
return aliases
}

private func fetchDistinctChunkCounts(groupMembers: [String: [String]], db: OpaquePointer) throws -> [String: Int] {
let pairs = groupMembers.flatMap { groupId, entityIds in entityIds.map { (groupId, $0) } }
guard !pairs.isEmpty else { return [:] }
let values = Array(repeating: "(?, ?)", count: pairs.count).joined(separator: ", ")
let sql = """
WITH group_members(group_id, entity_id) AS (VALUES \(values))
SELECT gm.group_id, COUNT(DISTINCT ec.chunk_id)
FROM group_members gm
JOIN kg_entity_chunks ec ON ec.entity_id = gm.entity_id
JOIN chunks c ON c.id = ec.chunk_id
GROUP BY gm.group_id
"""
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DBError.prepare(sqlite3_errcode(db))
}
defer { sqlite3_finalize(stmt) }
var bindIndex: Int32 = 1
for (groupId, entityId) in pairs {
bindText(groupId, to: stmt, index: bindIndex)
bindIndex += 1
bindText(entityId, to: stmt, index: bindIndex)
bindIndex += 1
}
var counts: [String: Int] = [:]
while sqlite3_step(stmt) == SQLITE_ROW {
counts[columnText(stmt, 0) ?? ""] = Int(sqlite3_column_int(stmt, 1))
}
return counts
}

@discardableResult
private func bindEntityIDs(_ entityIds: [String], to stmt: OpaquePointer?, startingAt index: Int32) -> Int32 {
var bindIndex = index
for entityId in entityIds {
bindText(entityId, to: stmt, index: bindIndex)
bindIndex += 1
}
return bindIndex
}

func getChunk(id: String) throws -> [String: Any]? {
guard let db else { throw DBError.notOpen }
let sql = """
Expand Down
10 changes: 8 additions & 2 deletions brain-bar/Sources/BrainBar/KnowledgeGraph/KGNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ struct KGNode: Identifiable, Equatable, Sendable {
let name: String
let entityType: String
let importance: Double
let linkedChunkCount: Int

var position: CGPoint
var velocity: CGVector

/// Radius scales with importance (min 8, max 28)
/// Radius scales with importance and linked evidence density.
var radius: CGFloat {
CGFloat(8 + (importance / 10.0) * 20)
let clampedImportance = max(0, min(10, importance))
let importanceBoost = CGFloat((clampedImportance / 10.0) * 20)
let evidenceBoost = min(max(CGFloat(linkedChunkCount), 0) / 10.0, 8)
return 8 + importanceBoost + evidenceBoost
}

var color: Color {
Expand All @@ -33,13 +37,15 @@ struct KGNode: Identifiable, Equatable, Sendable {
name: String,
entityType: String,
importance: Double,
linkedChunkCount: Int = 0,
position: CGPoint? = nil,
velocity: CGVector = .zero
) {
self.id = id
self.name = name
self.entityType = entityType
self.importance = importance
self.linkedChunkCount = linkedChunkCount
self.position = position ?? CGPoint(
x: CGFloat.random(in: 100...500),
y: CGFloat.random(in: 100...400)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ final class KGViewModel: ObservableObject {
name: row.name,
entityType: row.entityType,
importance: row.importance,
linkedChunkCount: row.linkedChunkCount,
position: existingNode?.position,
velocity: existingNode?.velocity ?? .zero
)
Expand Down
Loading
Loading