feat(8.0): reserved-field contract — one canonical location, typed prevention, unified read/write
Brainy-owned field names (noun/verb, subtype, createdAt, updatedAt,
confidence, weight, service, data, createdBy, _rev) now have exactly one
home — top level — enforced by three layers driven from a single source of
truth, src/types/reservedFields.ts (RESERVED_ENTITY_FIELDS /
RESERVED_RELATION_FIELDS, exported):
1. Compile time — AddParams/UpdateParams/RelateParams/UpdateRelationParams
metadata (and the transact() ops that extend them) reject a literal
reserved key as a TypeScript error while keeping generic T ergonomics
(typed bags, untyped brains, index-signature shapes, and a documented
exemption for T-declared reserved keys). Pinned by @ts-expect-error
type tests run under vitest typecheck mode on every unit run.
2. Write time — the 7.x update() remap is ported to 8.0 and extended to
every write path: add/update/relate/updateRelation, their transact()
mirrors, and db.with() overlays. User-settable fields lift to their
dedicated param (top-level wins when both are supplied — closes the 7.x
trap where update({metadata:{confidence}}) silently no-oped), and
system-managed fields drop with a one-shot warning naming the right
path. A remapped subtype satisfies subtype-pairing enforcement exactly
like a top-level one.
3. Read time — every storage combine goes through one canonical hydration
helper (hydrateNounWithMetadata / hydrateVerbWithMetadata over
splitNoun/VerbMetadataRecord), so reserved fields surface ONLY top-level
and entity/relation.metadata carry ONLY custom fields on live reads,
batch reads, paginated listings, getRelations by source/target, streamed
verbs, and historical asOf() materialization alike.
Read-path echoes found and fixed (previously the full stored record —
including the verb type key — leaked inside metadata): noun pagination,
verb pagination, getVerbsBySource/ByTarget (adjacency + shard fallback),
getVerbsBySourceBatch (which also dropped subtype/data), and the
filesystem verb stream. getRelations() results now surface
confidence/updatedAt top-level via verbsToRelations, updateRelation() no
longer erases service/createdBy, relate() persists its top-level
confidence/service params, and the dead convertHNSWVerbToGraphVerb echo
path is deleted. Import paths (CLI extract, deduplicator, coordinators,
neural import) write confidence through the dedicated param instead of the
bag. UpdateRelationParams is now exported from the package root.
Documented for consumers in docs/concepts/consistency-model.md ("Reserved
fields") and RELEASES.md. Regression tests ported from the 7.x fix and
extended to the full 8.0 contract (17 runtime tests + 41 type-level
assertions); full unit suite 1427/1427, db-mvcc integration 24/24.
This commit is contained in:
parent
c44678390e
commit
970e08c466
18 changed files with 1688 additions and 452 deletions
|
|
@ -30,6 +30,32 @@ import { BlobStorage, type BlobStoreAdapter } from './blobStorage.js'
|
|||
import { unwrapBinaryData } from './binaryDataCodec.js'
|
||||
import { prodLog } from '../utils/logger.js'
|
||||
import { MetadataWriteBuffer } from '../utils/metadataWriteBuffer.js'
|
||||
import {
|
||||
splitNounMetadataRecord,
|
||||
splitVerbMetadataRecord
|
||||
} from '../types/reservedFields.js'
|
||||
|
||||
/**
|
||||
* Normalize a stored timestamp value to epoch milliseconds. Brainy 8.0 writes
|
||||
* plain numbers; records written by pre-8.0 cloud adapters may carry the
|
||||
* `{ seconds, nanoseconds }` object form. Anything else falls back to now —
|
||||
* matching the long-standing `|| Date.now()` combine behavior.
|
||||
* @param value - The raw `createdAt`/`updatedAt` value from a stored metadata record.
|
||||
* @returns Epoch milliseconds.
|
||||
*/
|
||||
function normalizeStoredTimestamp(value: unknown): number {
|
||||
if (typeof value === 'number' && value > 0) {
|
||||
return value
|
||||
}
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
typeof (value as { seconds?: unknown }).seconds === 'number'
|
||||
) {
|
||||
return (value as { seconds: number }).seconds * 1000
|
||||
}
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage key analysis result
|
||||
|
|
@ -969,6 +995,74 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
await this.saveNoun_internal(noun)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate a deserialized noun (pure HNSW vector data) with its stored flat
|
||||
* metadata record — THE canonical noun combine for every storage read path.
|
||||
* The record is split through `splitNounMetadataRecord` (single source of
|
||||
* truth: src/types/reservedFields.ts): reserved fields surface ONLY at
|
||||
* top level and `metadata` carries ONLY the consumer's custom fields.
|
||||
* Adding a combine site that bypasses this helper reintroduces the
|
||||
* reserved-field echo bug — don't.
|
||||
*
|
||||
* @param noun - The deserialized HNSW noun (id/vector/connections/level).
|
||||
* @param metadata - The stored flat metadata record (reserved + custom keys).
|
||||
* @returns The combined noun with reserved fields top-level, custom fields in `metadata`.
|
||||
*/
|
||||
protected hydrateNounWithMetadata(
|
||||
noun: HNSWNoun,
|
||||
metadata: Record<string, unknown> | null | undefined
|
||||
): HNSWNounWithMetadata {
|
||||
const { reserved, custom } = splitNounMetadataRecord(metadata)
|
||||
return {
|
||||
...noun,
|
||||
// Standard fields at top-level
|
||||
type: (reserved.noun as NounType) || NounType.Thing,
|
||||
subtype: reserved.subtype as string | undefined,
|
||||
createdAt: normalizeStoredTimestamp(reserved.createdAt),
|
||||
updatedAt: normalizeStoredTimestamp(reserved.updatedAt),
|
||||
confidence: reserved.confidence as number | undefined,
|
||||
weight: reserved.weight as number | undefined,
|
||||
service: reserved.service as string | undefined,
|
||||
data: reserved.data as Record<string, any> | undefined,
|
||||
createdBy: reserved.createdBy as HNSWNounWithMetadata['createdBy'],
|
||||
_rev: typeof reserved._rev === 'number' ? reserved._rev : 1,
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: custom
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate a deserialized verb (structural core) with its stored flat
|
||||
* metadata record — THE canonical verb combine, the relationship mirror of
|
||||
* {@link hydrateNounWithMetadata}. Splitting through
|
||||
* `splitVerbMetadataRecord` extracts `verb` too, so the type key never
|
||||
* echoes inside `metadata`.
|
||||
*
|
||||
* @param verb - The deserialized HNSW verb (id/vector/connections/verb/sourceId/targetId).
|
||||
* @param metadata - The stored flat metadata record (reserved + custom keys).
|
||||
* @returns The combined verb with reserved fields top-level, custom fields in `metadata`.
|
||||
*/
|
||||
protected hydrateVerbWithMetadata(
|
||||
verb: HNSWVerb,
|
||||
metadata: Record<string, unknown> | null | undefined
|
||||
): HNSWVerbWithMetadata {
|
||||
const { reserved, custom } = splitVerbMetadataRecord(metadata)
|
||||
return {
|
||||
...verb,
|
||||
// Standard fields at top-level
|
||||
subtype: reserved.subtype as string | undefined,
|
||||
createdAt: normalizeStoredTimestamp(reserved.createdAt),
|
||||
updatedAt: normalizeStoredTimestamp(reserved.updatedAt),
|
||||
confidence: reserved.confidence as number | undefined,
|
||||
weight: reserved.weight as number | undefined,
|
||||
service: reserved.service as string | undefined,
|
||||
data: reserved.data as Record<string, any> | undefined,
|
||||
createdBy: reserved.createdBy as HNSWVerbWithMetadata['createdBy'],
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: custom
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a noun from storage (returns combined HNSWNounWithMetadata)
|
||||
* @param id Entity ID
|
||||
|
|
@ -990,28 +1084,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
return null
|
||||
}
|
||||
|
||||
// Combine into HNSWNounWithMetadata - Extract standard fields to top-level
|
||||
const { noun, subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadata
|
||||
|
||||
return {
|
||||
id: vector.id,
|
||||
vector: vector.vector,
|
||||
connections: vector.connections,
|
||||
level: vector.level,
|
||||
// Standard fields at top-level
|
||||
type: (noun as NounType) || NounType.Thing,
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
_rev: typeof _rev === 'number' ? _rev : 1,
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: customMetadata
|
||||
}
|
||||
return this.hydrateNounWithMetadata(vector, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1025,29 +1098,12 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// Internal method returns HNSWNoun[], need to combine with metadata
|
||||
const nouns = await this.getNounsByNounType_internal(nounType)
|
||||
|
||||
// Combine each noun with its metadata - Extract standard fields to top-level
|
||||
// Combine each noun with its metadata via the canonical hydration helper
|
||||
const nounsWithMetadata: HNSWNounWithMetadata[] = []
|
||||
for (const noun of nouns) {
|
||||
const metadata = await this.getNounMetadata(noun.id)
|
||||
if (metadata) {
|
||||
const { noun: nounType, subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadata
|
||||
|
||||
nounsWithMetadata.push({
|
||||
...noun,
|
||||
// Standard fields at top-level
|
||||
type: (nounType as NounType) || NounType.Thing,
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
_rev: typeof _rev === 'number' ? _rev : 1,
|
||||
// Only custom user fields in metadata
|
||||
metadata: customMetadata
|
||||
})
|
||||
nounsWithMetadata.push(this.hydrateNounWithMetadata(noun, metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1109,28 +1165,7 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
return null
|
||||
}
|
||||
|
||||
// Combine into HNSWVerbWithMetadata - Extract standard fields to top-level
|
||||
const { subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadata
|
||||
|
||||
return {
|
||||
id: verb.id,
|
||||
vector: verb.vector,
|
||||
connections: verb.connections,
|
||||
verb: verb.verb,
|
||||
sourceId: verb.sourceId,
|
||||
targetId: verb.targetId,
|
||||
// Standard fields at top-level
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: customMetadata
|
||||
}
|
||||
return this.hydrateVerbWithMetadata(verb, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1181,98 +1216,15 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const metadataData = metadataResults.get(metadataPath)
|
||||
|
||||
if (vectorData && metadataData) {
|
||||
// Deserialize verb
|
||||
// Deserialize, then combine via the canonical hydration helper
|
||||
const verb = this.deserializeVerb(vectorData)
|
||||
|
||||
// Extract standard fields to top-level
|
||||
const { subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadataData
|
||||
|
||||
results.set(id, {
|
||||
id: verb.id,
|
||||
vector: verb.vector,
|
||||
connections: verb.connections,
|
||||
verb: verb.verb,
|
||||
sourceId: verb.sourceId,
|
||||
targetId: verb.targetId,
|
||||
// Standard fields at top-level
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: customMetadata
|
||||
})
|
||||
results.set(id, this.hydrateVerbWithMetadata(verb, metadataData))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an HNSW verb (raw vector store entry) into a `GraphVerb` shape by
|
||||
* combining with the verb's metadata. Used by internal code paths that need
|
||||
* the graph-layer shape rather than the storage-layer shape.
|
||||
*/
|
||||
protected async convertHNSWVerbToGraphVerb(hnswVerb: HNSWVerb): Promise<GraphVerb | null> {
|
||||
try {
|
||||
// Load metadata
|
||||
const metadata = await this.getVerbMetadata(hnswVerb.id)
|
||||
|
||||
// Create default timestamp in Firestore format
|
||||
const defaultTimestamp = {
|
||||
seconds: Math.floor(Date.now() / 1000),
|
||||
nanoseconds: (Date.now() % 1000) * 1000000
|
||||
}
|
||||
|
||||
// Create default createdBy if not present
|
||||
const defaultCreatedBy = {
|
||||
augmentation: 'unknown',
|
||||
version: '1.0'
|
||||
}
|
||||
|
||||
// Convert flexible timestamp to Firestore format for GraphVerb
|
||||
const normalizeTimestamp = (ts: any) => {
|
||||
if (!ts) return defaultTimestamp
|
||||
if (typeof ts === 'number') {
|
||||
return {
|
||||
seconds: Math.floor(ts / 1000),
|
||||
nanoseconds: (ts % 1000) * 1000000
|
||||
}
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
return {
|
||||
id: hnswVerb.id,
|
||||
vector: hnswVerb.vector,
|
||||
|
||||
// CORE FIELDS from HNSWVerb
|
||||
verb: hnswVerb.verb,
|
||||
sourceId: hnswVerb.sourceId,
|
||||
targetId: hnswVerb.targetId,
|
||||
|
||||
// Alias for ergonomic access
|
||||
type: hnswVerb.verb,
|
||||
|
||||
// Optional fields from metadata file
|
||||
weight: metadata?.weight || 1.0,
|
||||
metadata: metadata as any || {},
|
||||
createdAt: normalizeTimestamp(metadata?.createdAt),
|
||||
updatedAt: normalizeTimestamp(metadata?.updatedAt),
|
||||
createdBy: metadata?.createdBy || defaultCreatedBy,
|
||||
data: metadata?.data as Record<string, any> | undefined,
|
||||
embedding: hnswVerb.vector
|
||||
}
|
||||
} catch (error) {
|
||||
prodLog.error(`Failed to convert HNSWVerb to GraphVerb for ${hnswVerb.id}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method for loading all verbs - used by performance optimizations
|
||||
* @internal - Do not use directly, use getVerbs() with pagination instead
|
||||
|
|
@ -1571,27 +1523,10 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Combine noun + metadata. Subtype is surfaced to top-level here
|
||||
// (mirrors what `getNoun()` already does) so callers — including
|
||||
// the 7.30.1 `brain.audit()` diagnostic — see a consistent shape
|
||||
// regardless of which getter path they reach.
|
||||
collectedNouns.push({
|
||||
...deserialized,
|
||||
type: (metadata.noun || 'thing') as NounType,
|
||||
subtype: (metadata as any).subtype as string | undefined,
|
||||
confidence: metadata.confidence,
|
||||
weight: metadata.weight,
|
||||
createdAt: metadata.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata.service,
|
||||
data: metadata.data as Record<string, any> | undefined,
|
||||
createdBy: metadata.createdBy,
|
||||
metadata: metadata || ({} as NounMetadata)
|
||||
})
|
||||
// Combine noun + metadata via the canonical hydration helper —
|
||||
// reserved fields top-level, ONLY custom fields in `metadata`
|
||||
// (this site previously echoed the full flat record).
|
||||
collectedNouns.push(this.hydrateNounWithMetadata(deserialized, metadata))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -1723,22 +1658,10 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Combine verb + metadata
|
||||
collectedVerbs.push({
|
||||
...verb,
|
||||
subtype: (metadata as any)?.subtype as string | undefined,
|
||||
weight: metadata?.weight,
|
||||
confidence: metadata?.confidence,
|
||||
createdAt: metadata?.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata?.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata?.service,
|
||||
createdBy: metadata?.createdBy,
|
||||
metadata: metadata || ({} as VerbMetadata)
|
||||
})
|
||||
// Combine verb + metadata via the canonical hydration helper —
|
||||
// reserved fields top-level, ONLY custom fields in `metadata`
|
||||
// (this site previously echoed the full flat record).
|
||||
collectedVerbs.push(this.hydrateVerbWithMetadata(verb, metadata))
|
||||
} catch (error) {
|
||||
// Skip verbs that fail to load
|
||||
}
|
||||
|
|
@ -2571,31 +2494,9 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const metadataData = metadataResults.get(metadataPath)
|
||||
|
||||
if (vectorData && metadataData) {
|
||||
// Deserialize noun
|
||||
// Deserialize, then combine via the canonical hydration helper
|
||||
const noun = this.deserializeNoun(vectorData)
|
||||
|
||||
// Extract standard fields to top-level
|
||||
const { noun: nounType, subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadataData
|
||||
|
||||
results.set(id, {
|
||||
id: noun.id,
|
||||
vector: noun.vector,
|
||||
connections: noun.connections,
|
||||
level: noun.level,
|
||||
// Standard fields at top-level
|
||||
type: (nounType as NounType) || NounType.Thing,
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
_rev: typeof _rev === 'number' ? _rev : 1,
|
||||
// Only custom user fields remain in metadata
|
||||
metadata: customMetadata
|
||||
})
|
||||
results.set(id, this.hydrateNounWithMetadata(noun, metadataData))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3734,24 +3635,11 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const metadata = metadataMap.get(metadataPath)
|
||||
|
||||
if (rawVerb && metadata) {
|
||||
// CRITICAL - Deserialize connections Map from JSON storage format
|
||||
// CRITICAL - Deserialize connections Map from JSON storage format,
|
||||
// then combine via the canonical hydration helper (reserved fields
|
||||
// top-level, ONLY custom fields in `metadata`).
|
||||
const verb = this.deserializeVerb(rawVerb)
|
||||
|
||||
results.push({
|
||||
...verb,
|
||||
subtype: (metadata as any).subtype as string | undefined,
|
||||
weight: metadata.weight,
|
||||
confidence: metadata.confidence,
|
||||
createdAt: metadata.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata.service,
|
||||
createdBy: metadata.createdBy,
|
||||
metadata: metadata || {} as VerbMetadata
|
||||
})
|
||||
results.push(this.hydrateVerbWithMetadata(verb, metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3791,22 +3679,9 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
if (verb.sourceId === sourceId) {
|
||||
const metadataPath = getVerbMetadataPath(verb.id)
|
||||
const metadata = await this.readCanonicalObject(metadataPath)
|
||||
|
||||
results.push({
|
||||
...verb,
|
||||
subtype: (metadata as any)?.subtype as string | undefined,
|
||||
weight: metadata?.weight,
|
||||
confidence: metadata?.confidence,
|
||||
createdAt: metadata?.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata?.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata?.service,
|
||||
createdBy: metadata?.createdBy,
|
||||
metadata: metadata || {} as VerbMetadata
|
||||
})
|
||||
// Canonical hydration — reserved fields top-level, ONLY custom
|
||||
// fields in `metadata`.
|
||||
results.push(this.hydrateVerbWithMetadata(verb, metadata))
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip verbs that fail to load
|
||||
|
|
@ -3919,20 +3794,10 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const metadataPath = getVerbMetadataPath(verbId)
|
||||
const metadata = metadataMap.get(metadataPath) || {}
|
||||
|
||||
const hydratedVerb: HNSWVerbWithMetadata = {
|
||||
...verbData,
|
||||
weight: metadata?.weight,
|
||||
confidence: metadata?.confidence,
|
||||
createdAt: metadata?.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata?.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata?.service,
|
||||
createdBy: metadata?.createdBy,
|
||||
metadata: metadata as VerbMetadata
|
||||
}
|
||||
// Canonical hydration — reserved fields top-level (including
|
||||
// subtype/data, which this site previously dropped), ONLY custom
|
||||
// fields in `metadata`.
|
||||
const hydratedVerb = this.hydrateVerbWithMetadata(verbData, metadata)
|
||||
|
||||
// Add to results for this sourceId
|
||||
const sourceVerbs = results.get(verbData.sourceId)!
|
||||
|
|
@ -3976,21 +3841,9 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
const metadata = await this.getVerbMetadata(verbId)
|
||||
|
||||
if (verb && metadata) {
|
||||
results.push({
|
||||
...verb,
|
||||
subtype: (metadata as any).subtype as string | undefined,
|
||||
weight: metadata.weight,
|
||||
confidence: metadata.confidence,
|
||||
createdAt: metadata.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata.service,
|
||||
createdBy: metadata.createdBy,
|
||||
metadata: metadata || {} as VerbMetadata
|
||||
})
|
||||
// Canonical hydration — reserved fields top-level, ONLY custom
|
||||
// fields in `metadata`.
|
||||
results.push(this.hydrateVerbWithMetadata(verb, metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4023,22 +3876,9 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
if (verb.targetId === targetId) {
|
||||
const metadataPath = getVerbMetadataPath(verb.id)
|
||||
const metadata = await this.readCanonicalObject(metadataPath)
|
||||
|
||||
results.push({
|
||||
...verb,
|
||||
subtype: (metadata as any)?.subtype as string | undefined,
|
||||
weight: metadata?.weight,
|
||||
confidence: metadata?.confidence,
|
||||
createdAt: metadata?.createdAt
|
||||
? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
updatedAt: metadata?.updatedAt
|
||||
? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
|
||||
: Date.now(),
|
||||
service: metadata?.service,
|
||||
createdBy: metadata?.createdBy,
|
||||
metadata: metadata || {} as VerbMetadata
|
||||
})
|
||||
// Canonical hydration — reserved fields top-level, ONLY custom
|
||||
// fields in `metadata`.
|
||||
results.push(this.hydrateVerbWithMetadata(verb, metadata))
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip verbs that fail to load
|
||||
|
|
@ -4079,32 +3919,15 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// Filter by verb type
|
||||
if (hnswVerb.verb !== verbType) continue
|
||||
|
||||
// Load metadata separately (optional)
|
||||
// Load metadata separately (optional), then combine via the
|
||||
// canonical hydration helper (defensive vector copy preserved)
|
||||
const metadata = await this.getVerbMetadata(hnswVerb.id)
|
||||
|
||||
// Extract standard fields from metadata to top-level
|
||||
const metadataObj = (metadata || {}) as VerbMetadata
|
||||
const { subtype, createdAt, updatedAt, confidence, weight, service, data, createdBy, _rev, ...customMetadata } = metadataObj
|
||||
|
||||
const verbWithMetadata: HNSWVerbWithMetadata = {
|
||||
id: hnswVerb.id,
|
||||
vector: [...hnswVerb.vector],
|
||||
connections: hnswVerb.connections, // Already deserialized
|
||||
verb: hnswVerb.verb,
|
||||
sourceId: hnswVerb.sourceId,
|
||||
targetId: hnswVerb.targetId,
|
||||
subtype: subtype as string | undefined,
|
||||
createdAt: (createdAt as number) || Date.now(),
|
||||
updatedAt: (updatedAt as number) || Date.now(),
|
||||
confidence: confidence as number | undefined,
|
||||
weight: weight as number | undefined,
|
||||
service: service as string | undefined,
|
||||
data: data as Record<string, any> | undefined,
|
||||
createdBy,
|
||||
metadata: customMetadata
|
||||
}
|
||||
|
||||
verbs.push(verbWithMetadata)
|
||||
verbs.push(
|
||||
this.hydrateVerbWithMetadata(
|
||||
{ ...hnswVerb, vector: [...hnswVerb.vector] },
|
||||
metadata
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
// Skip verbs that fail to load
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue