fix: find()/stats() correctness + Cortex compat (BR-FIND-WHERE-ZERO, BR-DEFENSIVE-INTERFACE)

Two production correctness defects fixed together so consumers can land
one upgrade.

BR-FIND-WHERE-ZERO — find() returned [] and stats() reported 0 entities
for any workspace whose data was written after the 7.20.0 column-store
refactor. Root cause: getStats() and the getIds() fallback still read
from the deleted sparse-index path. Separately, BaseStorage.getNounType()
was hardcoded to return 'thing', poisoning type-statistics.json with
every noun attributed to that bucket.

  - MetadataIndex.getStats() reads from ColumnStore + idMapper.
  - MetadataIndex.getIdsFromChunks() throws BrainyError(FIELD_NOT_INDEXED)
    when neither store has the field. getIdsForFilter() catches per
    clause, logs once, returns [].
  - BaseStorage.nounTypeByIdCache populated in saveNounMetadata_internal
    and consumed in saveNoun_internal. flushCounts() now persists the
    Uint32Array counters too, so readers see fresh per-type counts.
  - Self-heal at init: loadTypeStatistics() auto-rebuilds when the
    poisoned signature is detected. rebuildTypeCounts() is now public for
    use from `brainy inspect repair`.
  - Dead state removed: dirtyChunks, dirtySparseIndices,
    flushDirtyMetadata().

BR-DEFENSIVE-INTERFACE — 7.21.0 called supportsMultiProcessLocking()
unconditionally, crashing on older Cortex storage adapters that predate
the method. New hasStorageMethod(name) helper gates every new-method
call site. Older adapter triggers a one-line warning at init pointing
at the recommended plugin version.

Tests:
  - new tests/integration/find-where-zero.test.ts (7 cases)
  - new tests/integration/cortex-compat.test.ts (5 cases)
  - multi-process-safety.test.ts updated to enforce correct counts
  - 1329/1329 unit + 24/24 new integration tests passing
This commit is contained in:
David Snelling 2026-05-15 12:31:28 -07:00
parent 79f58cb87b
commit 70263113ad
9 changed files with 895 additions and 144 deletions

View file

@ -309,6 +309,44 @@ export class ColumnStore implements ColumnStoreProvider {
return (manifest !== undefined && !manifest.isEmpty()) || (buffer !== undefined && buffer.size > 0)
}
/**
* List every field that has at least one persisted segment or buffered
* write. Used by `MetadataIndexManager.getStats()` and by
* `brainy inspect fields` so callers see the same field set the column
* store will actually serve queries from.
*/
getIndexedFields(): string[] {
const fields = new Set<string>()
for (const [field, manifest] of this.manifests) {
if (!manifest.isEmpty()) fields.add(field)
}
for (const [field, buffer] of this.tailBuffers) {
if (buffer.size > 0) fields.add(field)
}
return Array.from(fields).sort()
}
/**
* Approximate segment + tail-buffer sizes per indexed field. Returns
* `segmentCount` (number of persisted L0+ segments) and `tailSize` (entries
* buffered but not yet flushed) per field. Suitable for stats / health
* surfaces. For exact distinct-value cardinality use
* `getFilterValues(field).length` per field on demand.
*/
getFieldSizeSummary(): Array<{ field: string; segmentCount: number; tailSize: number }> {
const summary: Array<{ field: string; segmentCount: number; tailSize: number }> = []
for (const field of this.getIndexedFields()) {
const manifest = this.manifests.get(field)
const buffer = this.tailBuffers.get(field)
const segmentCount = manifest && !manifest.isEmpty()
? manifest.getAllSegments().length
: 0
const tailSize = buffer ? buffer.size : 0
summary.push({ field, segmentCount, tailSize })
}
return summary
}
/**
* Flush all tail buffers to L0 segments and save manifests.
* No-op if init() hasn't been called (no storage to write to).