open-brainy/src/storage/cow/binaryDataCodec.ts
David Snelling 364360d447 fix: exclude __words__ keyword index from corruption detection and getStats()
The __words__ keyword index stores 50-5000 entries per entity (one per
word), which inflated avg entries/entity well above the corruption
threshold of 100. This caused:

1. validateConsistency() to falsely detect corruption on every startup,
   triggering unnecessary clearAllIndexData() + rebuild() cycles
2. getStats() to log false "Metadata index may be corrupted" warnings
   and report inflated totalEntries/totalIds stats

Both methods now skip __words__ when counting, so stats and health
checks reflect metadata fields only (noun, type, createdAt, etc.).
Keyword search is unaffected since the __words__ field index itself
is not modified.
2026-01-27 15:38:21 -08:00

135 lines
3.9 KiB
TypeScript

/**
* Binary Data Codec: Single Source of Truth for Wrap/Unwrap Operations
*
* This module provides the ONLY implementation of binary data encoding/decoding
* used across all storage adapters and blob storage.
*
* Design Principles:
* - DRY: One implementation, used everywhere
* - Single Responsibility: Only handles binary ↔ JSON conversion
* - Type-Safe: Proper TypeScript types
* - Defensive: Handles all edge cases
*
* Used by:
* - BaseStorage COW adapter (write/read operations)
* - BlobStorage (defense-in-depth verification)
* - All storage adapters (via BaseStorage)
*
* @module storage/cow/binaryDataCodec
*/
/**
* Wrapped binary data format
* Used when storing binary data in JSON-based storage
*/
export interface WrappedBinaryData {
_binary: true
data: string // base64-encoded
}
/**
* Check if data is wrapped binary format
*/
export function isWrappedBinary(data: any): data is WrappedBinaryData {
return (
typeof data === 'object' &&
data !== null &&
data._binary === true &&
typeof data.data === 'string'
)
}
/**
* Unwrap binary data from JSON wrapper
*
* This is the SINGLE SOURCE OF TRUTH for unwrapping binary data.
* All storage operations MUST use this function.
*
* Handles:
* - Buffer → Buffer (pass-through)
* - {_binary: true, data: "base64..."} → Buffer (unwrap)
* - Plain object → Buffer (JSON stringify)
* - Other types → Error
*
* @param data - Data to unwrap (may be Buffer, wrapped object, or plain object)
* @returns Unwrapped Buffer
* @throws Error if data type is invalid
*/
export function unwrapBinaryData(data: any): Buffer {
// Case 1: Already a Buffer (no unwrapping needed)
if (Buffer.isBuffer(data)) {
return data
}
// Case 2: Wrapped binary data {_binary: true, data: "base64..."}
if (isWrappedBinary(data)) {
return Buffer.from(data.data, 'base64')
}
// Case 3: Plain object (shouldn't happen for binary blobs, but handle gracefully)
if (typeof data === 'object' && data !== null) {
return Buffer.from(JSON.stringify(data))
}
// Case 4: String (convert to Buffer)
if (typeof data === 'string') {
return Buffer.from(data)
}
// Case 5: Invalid type
throw new Error(
`Invalid data type for unwrap: ${typeof data}. ` +
`Expected Buffer or {_binary: true, data: "base64..."}`
)
}
/**
* Wrap binary data for JSON storage
*
* ⚠️ WARNING: DO NOT USE THIS ON WRITE PATH!
* ⚠️ Use key-based dispatch in baseStorage.ts COW adapter instead.
* ⚠️ This function exists for legacy/compatibility only.
*
* DEPRECATED APPROACH: Tries to guess if data is JSON by parsing.
* This is FRAGILE because compressed binary can accidentally parse as valid JSON,
* causing blob integrity failures.
*
* SOLUTION: baseStorage.ts COW adapter now uses key naming convention:
* - Keys with '-meta:' or 'ref:' prefix → Always JSON
* - Keys with 'blob:', 'commit:', 'tree:' prefix → Always binary
* No guessing needed!
*
* @param data - Buffer to wrap
* @returns Wrapped object or parsed JSON object
* @deprecated Use key-based dispatch in baseStorage.ts instead
*/
export function wrapBinaryData(data: Buffer): any {
// Try to parse as JSON first (for metadata, trees, commits)
// NOTE: This is the OLD approach - fragile because compressed data
// can accidentally parse as valid JSON!
try {
return JSON.parse(data.toString())
} catch {
// Not JSON - wrap as binary data
return {
_binary: true,
data: data.toString('base64')
} as WrappedBinaryData
}
}
/**
* Ensure data is a Buffer
*
* Convenience function that combines type checking and unwrapping.
* Use this when you need to ensure you have a Buffer.
*
* @param data - Data that should be or can be converted to Buffer
* @returns Buffer
*/
export function ensureBuffer(data: any): Buffer {
if (Buffer.isBuffer(data)) {
return data
}
return unwrapBinaryData(data)
}