feat(namespace): aggregation reads under the law + epoch 3 (the key-split rebuild) + THE ARMING COMMIT — the capability constant, the law module, and the typed refusals export from the package root; both engines' conformance suites light on this signal
This commit is contained in:
parent
c2fb28a2f7
commit
7492b6cb59
7 changed files with 73 additions and 22 deletions
|
|
@ -14,7 +14,22 @@
|
|||
*/
|
||||
|
||||
import type { StorageAdapter, HNSWNounWithMetadata } from '../coreTypes.js'
|
||||
import { resolveEntityField } from '../coreTypes.js'
|
||||
import { parseFieldAddress, readEntityFieldAddress } from '../db/fieldAddressing.js'
|
||||
import type { HNSWNounWithMetadata as AddressedEntity } from '../coreTypes.js'
|
||||
|
||||
/**
|
||||
* Read a user-supplied field name under the one addressing law (sealed
|
||||
* 2026-08-03): bare / `metadata.` = the user's metadata field, `system.<x>` =
|
||||
* the ruled engine scalar, malformed = typed refusal. The aggregation engine
|
||||
* NEVER resolves names any other way — the pre-law resolver made bare
|
||||
* `subtype`/`confidence` read engine scalars, silently shadowing user fields.
|
||||
*/
|
||||
function readAddressed(e: unknown, name: string): unknown {
|
||||
return readEntityFieldAddress(
|
||||
e as AddressedEntity,
|
||||
parseFieldAddress(name, 'entity')
|
||||
)
|
||||
}
|
||||
import type {
|
||||
AggregateDefinition,
|
||||
AggregateGroupState,
|
||||
|
|
@ -97,7 +112,7 @@ function matchesSource(entity: Record<string, unknown>, source: AggregateDefinit
|
|||
const e = entity as unknown as HNSWNounWithMetadata
|
||||
const resolved: Record<string, unknown> = {}
|
||||
for (const key of Object.keys(source.where)) {
|
||||
resolved[key] = resolveEntityField(e, key)
|
||||
resolved[key] = readAddressed(e, key)
|
||||
}
|
||||
if (!matchesMetadataFilter(resolved, source.where)) return false
|
||||
}
|
||||
|
|
@ -129,11 +144,11 @@ function computeGroupKeys(
|
|||
|
||||
for (const dim of groupBy) {
|
||||
if (typeof dim === 'string') {
|
||||
const val = resolveEntityField(e, dim)
|
||||
const val = readAddressed(e, dim)
|
||||
const v = val !== undefined && val !== null ? String(val) : '__null__'
|
||||
for (const k of keys) k[dim] = v
|
||||
} else if ('unnest' in dim) {
|
||||
const val = resolveEntityField(e, dim.field)
|
||||
const val = readAddressed(e, dim.field)
|
||||
const raw = Array.isArray(val) ? val : val !== undefined && val !== null ? [val] : []
|
||||
// Distinct elements: an entity with duplicate tags counts once per distinct tag.
|
||||
const elems = Array.from(new Set(raw.map(x => String(x))))
|
||||
|
|
@ -145,7 +160,7 @@ function computeGroupKeys(
|
|||
keys = next
|
||||
} else {
|
||||
// Time-windowed field
|
||||
const val = resolveEntityField(e, dim.field)
|
||||
const val = readAddressed(e, dim.field)
|
||||
const v = typeof val === 'number' ? bucketTimestamp(val, dim.window) : '__null__'
|
||||
for (const k of keys) k[dim.field] = v
|
||||
}
|
||||
|
|
@ -174,7 +189,7 @@ function computeGroupKey(
|
|||
* in metadata are both handled in one place.
|
||||
*/
|
||||
function getNumericField(entity: Record<string, unknown>, field: string): number | undefined {
|
||||
const val = resolveEntityField(entity as unknown as HNSWNounWithMetadata, field)
|
||||
const val = readAddressed(entity as unknown as HNSWNounWithMetadata, field)
|
||||
if (typeof val === 'number' && !isNaN(val)) return val
|
||||
if (typeof val === 'string') {
|
||||
const num = parseFloat(val)
|
||||
|
|
@ -990,7 +1005,7 @@ export class AggregationIndex {
|
|||
// distinctCount tracks distinct values of ANY type (strings, numbers, booleans),
|
||||
// keyed by their string form — NOT numeric-coerced, since its primary use is
|
||||
// categorical (distinct categories / users / tags), not numeric columns.
|
||||
const raw = resolveEntityField(entity as unknown as HNSWNounWithMetadata, metricDef.field!)
|
||||
const raw = readAddressed(entity as unknown as HNSWNounWithMetadata, metricDef.field!)
|
||||
if (raw !== undefined && raw !== null) {
|
||||
if (!state.valueCounts) state.valueCounts = {}
|
||||
const key = String(raw)
|
||||
|
|
@ -1034,7 +1049,7 @@ export class AggregationIndex {
|
|||
state.count = Math.max(0, state.count - 1)
|
||||
state.sum = Math.max(0, state.sum - 1)
|
||||
} else if (metricDef.op === 'distinctCount') {
|
||||
const raw = resolveEntityField(entity as unknown as HNSWNounWithMetadata, metricDef.field!)
|
||||
const raw = readAddressed(entity as unknown as HNSWNounWithMetadata, metricDef.field!)
|
||||
if (raw !== undefined && raw !== null && state.valueCounts) {
|
||||
const key = String(raw)
|
||||
const c = state.valueCounts[key]
|
||||
|
|
|
|||
Reference in a new issue