fix(8.0): neural.clusters()/similarity must request vectors from get()

brain.get() returns metadata only by default (includeVectors: false) for speed;
the vector is fetched via get(id, { includeVectors: true }). The neural API's
clustering, similarity, and vector-conversion helpers called get() without it,
so every entity came back with vector: [] — _getItemsWithVectors filtered them
all out, leaving k-means++ to dereference an empty array ("Cannot read
properties of undefined (reading 'vector')"). neural.clusters() was effectively
non-functional, and similarity-by-id silently scored 0.

- Pass { includeVectors: true } at the 8 vector-consuming get() call sites
  (similarity, _convertToVector, _getItemsWith{Vectors,Metadata}, cluster
  membership + quality scoring).
- Guard k-means against an empty item set (return an empty result, never crash).

"cluster entities semantically" passes. (includeVFS clustering + vfs.move have a
separate VFS-vector-dimension issue, tracked in .strategy.)
This commit is contained in:
David Snelling 2026-06-16 13:13:07 -07:00
parent 73a7d8291b
commit cc1a4317b1

View file

@ -1076,6 +1076,12 @@ export class ImprovedNeuralAPI {
// Get vectors for all items using existing infrastructure // Get vectors for all items using existing infrastructure
const itemsWithVectors = await this._getItemsWithVectors(itemIds) const itemsWithVectors = await this._getItemsWithVectors(itemIds)
// No items carried a usable vector (e.g. all vectorless) — return an empty
// result rather than letting centroid init dereference an empty array.
if (itemsWithVectors.length === 0) {
return this._createEmptyResult(startTime, 'kmeans')
}
// Determine optimal k // Determine optimal k
const k = options.maxClusters || Math.min( const k = options.maxClusters || Math.min(
Math.floor(Math.sqrt(itemsWithVectors.length / 2)), Math.floor(Math.sqrt(itemsWithVectors.length / 2)),
@ -1448,8 +1454,8 @@ export class ImprovedNeuralAPI {
// Similarity implementation methods // Similarity implementation methods
private async _similarityById(id1: string, id2: string, options: SimilarityOptions): Promise<number | SimilarityResult> { private async _similarityById(id1: string, id2: string, options: SimilarityOptions): Promise<number | SimilarityResult> {
// Get vectors for both items // Get vectors for both items
const item1 = await this.brain.get(id1) const item1 = await this.brain.get(id1, { includeVectors: true })
const item2 = await this.brain.get(id2) const item2 = await this.brain.get(id2, { includeVectors: true })
if (!item1 || !item2) { if (!item1 || !item2) {
return 0 return 0
@ -1520,7 +1526,7 @@ export class ImprovedNeuralAPI {
if (this._isVector(input)) { if (this._isVector(input)) {
return input return input
} else if (this._isId(input)) { } else if (this._isId(input)) {
const item = await this.brain.get(input) const item = await this.brain.get(input, { includeVectors: true })
return item?.vector || [] return item?.vector || []
} else if (typeof input === 'string') { } else if (typeof input === 'string') {
return await this.brain.embed(input) return await this.brain.embed(input)
@ -1795,7 +1801,7 @@ export class ImprovedNeuralAPI {
private async _getItemsWithMetadata(itemIds: string[]): Promise<ItemWithMetadata[]> { private async _getItemsWithMetadata(itemIds: string[]): Promise<ItemWithMetadata[]> {
const items = await Promise.all( const items = await Promise.all(
itemIds.map(async id => { itemIds.map(async id => {
const noun = await this.brain.get(id) const noun = await this.brain.get(id, { includeVectors: true })
if (!noun) { if (!noun) {
return null return null
} }
@ -2087,7 +2093,7 @@ export class ImprovedNeuralAPI {
private async _getItemsWithVectors(itemIds: string[]): Promise<Array<{id: string, vector: number[]}>> { private async _getItemsWithVectors(itemIds: string[]): Promise<Array<{id: string, vector: number[]}>> {
const items = await Promise.all( const items = await Promise.all(
itemIds.map(async id => { itemIds.map(async id => {
const noun = await this.brain.get(id) const noun = await this.brain.get(id, { includeVectors: true })
return { return {
id, id,
vector: noun?.vector || [] vector: noun?.vector || []
@ -3208,7 +3214,7 @@ export class ImprovedNeuralAPI {
private async _calculateItemToClusterSimilarity(itemId: string, cluster: SemanticCluster): Promise<number> { private async _calculateItemToClusterSimilarity(itemId: string, cluster: SemanticCluster): Promise<number> {
// Calculate similarity between an item and a cluster centroid // Calculate similarity between an item and a cluster centroid
const item = await this.brain.get(itemId) const item = await this.brain.get(itemId, { includeVectors: true })
if (!item || !item.vector || !cluster.centroid) { if (!item || !item.vector || !cluster.centroid) {
return 0 // No similarity if vectors missing return 0 // No similarity if vectors missing
} }
@ -3234,7 +3240,7 @@ export class ImprovedNeuralAPI {
// Get all member vectors // Get all member vectors
const memberVectors: Vector[] = [] const memberVectors: Vector[] = []
for (const memberId of cluster.members) { for (const memberId of cluster.members) {
const member = await this.brain.get(memberId) const member = await this.brain.get(memberId, { includeVectors: true })
if (member && member.vector) { if (member && member.vector) {
memberVectors.push(member.vector) memberVectors.push(member.vector)
} }
@ -3630,7 +3636,7 @@ export class ImprovedNeuralAPI {
if (clusters.length === 0) break if (clusters.length === 0) break
try { try {
const noun = await this.brain.get(itemId) const noun = await this.brain.get(itemId, { includeVectors: true })
const itemVector = noun?.vector || [] const itemVector = noun?.vector || []
if (itemVector.length === 0) continue if (itemVector.length === 0) continue