feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release

- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
  transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
  relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
  db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
  ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
  BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
  deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
  entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
  already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
  `--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
  verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.

Unit 1461/0 · integration 599/0 · tsc clean.
This commit is contained in:
David Snelling 2026-06-20 14:40:57 -07:00
parent 606445cd61
commit d02e522a3e
14 changed files with 943 additions and 100 deletions

View file

@ -252,7 +252,10 @@ describe('reserved-field metadata remap (8.0 contract)', () => {
const entity = await speculative.get('spec-entity')
expect(entity?.confidence).toBe(0.65)
expect(entity?.metadata).toEqual({ custom: 'spec' })
// 8.0 id normalization: a natural-key id is mapped to a stable UUID and
// the caller's original string is preserved under _originalId — surfaced
// here exactly as the durable transact()/add() paths do.
expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'spec-entity' })
await speculative.release()
await base.release()
})

View file

@ -0,0 +1,47 @@
/**
* @module tests/unit/universal/uuid
* @description Pins the in-house UUID utilities (v4/v5/v7 + isUUID). v5 is checked
* against a known RFC/python `uuid5(NAMESPACE_DNS, 'python.org')` vector if the
* bundled SHA-1 or the v5 byte-twiddling were wrong, this fails.
*/
import { describe, it, expect } from 'vitest'
import { v4, v5, v7, isUUID, BRAINY_ID_NAMESPACE } from '../../../src/universal/uuid.js'
describe('universal/uuid', () => {
it('v5 matches the known DNS-namespace vector (SHA-1 correctness)', () => {
// python: uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
expect(v5('python.org', '6ba7b810-9dad-11d1-80b4-00c04fd430c8')).toBe(
'886313e1-3b8a-5372-9b90-0c9aee199e5d'
)
})
it('v5 is deterministic, valid, version-5, and input-sensitive', () => {
const a = v5('user-123')
expect(v5('user-123')).toBe(a) // deterministic
expect(isUUID(a)).toBe(true)
expect(a[14]).toBe('5') // version nibble
expect(v5('user-124')).not.toBe(a) // different name → different id
expect(BRAINY_ID_NAMESPACE).toBe('62726169-6e79-5000-8000-000000000000')
})
it('v7 is valid, version-7, unique, and time-sortable', () => {
const a = v7()
expect(isUUID(a)).toBe(true)
expect(a[14]).toBe('7')
const ids = Array.from({ length: 8 }, () => v7())
expect(new Set(ids).size).toBe(8) // unique
// the timestamp prefix makes lexical order track creation order
const sorted = [...ids].sort()
expect(sorted[sorted.length - 1] >= sorted[0]).toBe(true)
})
it('v4 is valid', () => {
expect(isUUID(v4())).toBe(true)
})
it('isUUID rejects non-UUID strings', () => {
expect(isUUID('user-123')).toBe(false)
expect(isUUID('hello')).toBe(false)
expect(isUUID('')).toBe(false)
})
})

View file

@ -47,7 +47,7 @@ describe('EntityIdMapper U32 IdSpace ceiling (Brainy 8.0)', () => {
expect(err.ceiling).toBe(0xffff_ffff)
expect(err.attempted).toBe(0x1_0000_0000)
expect(err.message).toMatch(/u32::MAX/)
expect(err.message).toMatch(/cortex/)
expect(err.message).toMatch(/@soulcraft\/cor/)
expect(err.message).toMatch(/idSpace: 'u64'/)
})