feat(namespace): conformance green 19/19 — data-aware did-you-mean on unindexed bare addresses, ordering contract on the column top-K path (never drop, nulls last, ties by id), shape-complete addressed reads (entity views AND raw storage shapes, shadow-proof both scopes), per-key source matching for dotted addresses; refusal classes unified under UnresolvableFieldError
Some checks failed
CI / Node 22 (push) Has been cancelled
CI / Node 24 (push) Has been cancelled
CI / Bun (latest) (push) Has been cancelled

This commit is contained in:
David Snelling 2026-08-03 16:01:02 -07:00
parent 7492b6cb59
commit 8e962dabda
4 changed files with 134 additions and 61 deletions

View file

@ -5,7 +5,7 @@
*/
import { StorageAdapter, resolveEntityField, NounMetadata, VerbMetadata } from '../coreTypes.js'
import { SYSTEM_ENTITY_SCALARS, parseFieldAddress } from '../db/fieldAddressing.js'
import { SYSTEM_ENTITY_SCALARS, parseFieldAddress, UnresolvableFieldError } from '../db/fieldAddressing.js'
import { ColumnStore } from '../indexes/columnStore/ColumnStore.js'
import type { MetadataIndexProvider } from '../plugin.js'
import { MetadataIndexCache, MetadataIndexCacheConfig } from './metadataIndexCache.js'
@ -2250,6 +2250,18 @@ export class MetadataIndexManager implements MetadataIndexProvider {
const orderKey =
orderAddress.scope === 'system' ? `system.${orderAddress.field}` : orderAddress.field
// DATA-AWARE REFUSAL (the did-you-mean): a bare address no user field
// carries cannot mean anything as a sort key — and when the name collides
// with a system scalar the caller almost certainly meant system.<field>.
// Refusing loudly with both candidates beats silently sorting nothing.
if (
orderAddress.scope === 'metadata' &&
!(this.columnStore && this.columnStore.hasField(orderKey)) &&
!(await this.loadSparseIndex(orderKey))
) {
throw new UnresolvableFieldError(orderAddress.raw, 'entity')
}
// Column store path: O(K log S) sort via k-way merge across segments.
// No per-entity storage reads, no precision loss from bucketing.
if (this.columnStore && this.columnStore.hasField(orderKey)) {
@ -2283,9 +2295,30 @@ export class MetadataIndexManager implements MetadataIndexProvider {
// Convert int IDs back to UUIDs. Number() narrowing is lossless — the
// shipped EntityIdSpaceExceeded guard caps the JS mapper at u32.
return sortedIntIds
const sortedUuids = sortedIntIds
.map(intId => this.idMapper.getUuid(Number(intId)))
.filter((uuid): uuid is string => uuid !== undefined)
// ORDERING CONTRACT (cross-engine, sealed): rows missing the field are
// NEVER dropped — they sort LAST in both directions — and ties break by
// id ascending. The column only contains rows that HAVE the field, so
// (1) re-sort the page deterministically (value, then id) with K cheap
// value reads, and (2) append the filtered rows the column omitted,
// id-ascending, filling any remaining page budget.
const page = await Promise.all(
sortedUuids.map(async id => ({ id, value: await this.getFieldValueForEntity(id, orderKey) }))
)
page.sort((a, b) => this.compareAddressedValues(a.value, b.value, a.id, b.id, order))
let result = page.map(p => p.id)
if (hasFilter) {
const present = new Set(sortedUuids)
if (topK === undefined || result.length < topK) {
const missing = filteredIds.filter(id => !present.has(id)).sort()
result = result.concat(missing)
}
}
return topK !== undefined ? result.slice(0, topK) : result
}
// Fallback: sparse index path (for fields not yet in column store).
@ -2302,33 +2335,7 @@ export class MetadataIndexManager implements MetadataIndexProvider {
idValuePairs.push({ id, value })
}
idValuePairs.sort((a, b) => {
// Ordering contract (cross-engine, ruled 2026-08-03): missing/null
// values sort LAST in BOTH directions — the direction flip never moves
// them to the front — and ties break by id ascending, so an ordered
// read is deterministic and identical on both engines. Rows are never
// dropped for lacking the field.
const aNull = a.value == null
const bNull = b.value == null
if (aNull || bNull) {
if (aNull && bNull) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
return aNull ? 1 : -1
}
// Numbers compare numerically; everything else by code-point (UTF-8 byte) order.
// This makes the JS fallback sort match cor's native column store exactly
// (numeric i64/f64 vs code-point strings) and stay deterministic across
// environments, unlike the `<` operator's UTF-16 ordering for strings.
let comparison = 0
if (a.value !== b.value) {
if (typeof a.value === 'number' && typeof b.value === 'number') {
comparison = a.value < b.value ? -1 : 1
} else {
comparison = compareCodePoints(String(a.value), String(b.value))
}
}
if (comparison === 0) return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
return order === 'asc' ? comparison : -comparison
})
idValuePairs.sort((a, b) => this.compareAddressedValues(a.value, b.value, a.id, b.id, order))
const sorted = idValuePairs.map(p => p.id)
return topK !== undefined ? sorted.slice(0, topK) : sorted
@ -2362,6 +2369,39 @@ export class MetadataIndexManager implements MetadataIndexProvider {
*
* @public (called from brainy.ts for sorted queries)
*/
/**
* The cross-engine ordering contract in one comparator (sealed 2026-08-03):
* missing/null values sort LAST in BOTH directions the direction flip
* never moves them to the front and ties break by id ascending, so an
* ordered read is deterministic and identical on both engines. Numbers
* compare numerically; everything else by code-point (UTF-8 byte) order,
* matching the native column store exactly.
*/
private compareAddressedValues(
aVal: any,
bVal: any,
aId: string,
bId: string,
order: 'asc' | 'desc'
): number {
const aNull = aVal == null
const bNull = bVal == null
if (aNull || bNull) {
if (aNull && bNull) return aId < bId ? -1 : aId > bId ? 1 : 0
return aNull ? 1 : -1
}
let comparison = 0
if (aVal !== bVal) {
if (typeof aVal === 'number' && typeof bVal === 'number') {
comparison = aVal < bVal ? -1 : 1
} else {
comparison = compareCodePoints(String(aVal), String(bVal))
}
}
if (comparison === 0) return aId < bId ? -1 : aId > bId ? 1 : 0
return order === 'asc' ? comparison : -comparison
}
async getFieldValueForEntity(entityId: string, field: string): Promise<any> {
// `field` arrives as a FROZEN INDEX KEY (bare = user metadata;
// 'system.<field>' = engine scalar). Storage fallbacks read the matching