2025-08-26 12:32:21 -07:00
/ * *
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.
2026-06-20 14:40:57 -07:00
* @module universal / uuid
* @description Framework - friendly UUID utilities used across Brainy — works in
* Node , Bun , Deno , and browsers ( relies only on the global Web Crypto API , with
* pure - JS fallbacks ) .
*
* Three generators + helpers :
* - { @link v4 } — random UUID ( legacy default ) .
* - { @link v7 } — time - ordered UUID ( RFC 9562 ) . The 8.0 default for new ids :
* lexicographically sortable by creation time , which keeps the id ↔ int mapper
* and any range scan locality - friendly .
* - { @link v5 } — deterministic namespaced UUID ( RFC 4122 , SHA - 1 ) . Used by the
* 8.0 id - normalization layer to map a caller ' s non - UUID string key to a STABLE
* UUID , so a native engine ( which requires UUID ids for its int mapper ) always
* sees a valid UUID while the caller can keep using their natural key .
2025-08-26 12:32:21 -07:00
* /
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.
2026-06-20 14:40:57 -07:00
/** Render 16 bytes as a canonical hyphenated UUID string. */
function bytesToUuid ( b : Uint8Array ) : string {
const h : string [ ] = [ ]
for ( let i = 0 ; i < 16 ; i ++ ) h . push ( b [ i ] . toString ( 16 ) . padStart ( 2 , '0' ) )
return (
` ${ h [ 0 ] } ${ h [ 1 ] } ${ h [ 2 ] } ${ h [ 3 ] } - ${ h [ 4 ] } ${ h [ 5 ] } - ${ h [ 6 ] } ${ h [ 7 ] } - ` +
` ${ h [ 8 ] } ${ h [ 9 ] } - ${ h [ 10 ] } ${ h [ 11 ] } ${ h [ 12 ] } ${ h [ 13 ] } ${ h [ 14 ] } ${ h [ 15 ] } `
)
}
/** Parse a hyphenated UUID string into its 16 bytes. */
function uuidToBytes ( uuid : string ) : Uint8Array {
const hex = uuid . replace ( /-/g , '' )
const b = new Uint8Array ( 16 )
for ( let i = 0 ; i < 16 ; i ++ ) b [ i ] = parseInt ( hex . slice ( i * 2 , i * 2 + 2 ) , 16 )
return b
}
/** Fill a byte array with cryptographically-strong (or Math.random fallback) randomness. */
function randomBytes ( n : number ) : Uint8Array {
const out = new Uint8Array ( n )
if ( typeof crypto !== 'undefined' && crypto . getRandomValues ) {
crypto . getRandomValues ( out )
} else {
for ( let i = 0 ; i < n ; i ++ ) out [ i ] = Math . floor ( Math . random ( ) * 256 )
}
return out
}
/ * *
* @description Canonical UUID - format check ( any version / variant ) . NOT a version
* assertion — just "is this string shaped like a UUID" .
* @param value - The string to test .
* @returns ` true ` if ` value ` matches the 8 - 4 - 4 - 4 - 12 hex UUID format .
* /
export function isUUID ( value : string ) : boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i . test ( value )
}
/ * *
* @description Random ( version 4 ) UUID .
* @returns A random UUID string .
* /
2025-08-26 12:32:21 -07:00
export function v4 ( ) : string {
if ( typeof crypto !== 'undefined' && crypto . randomUUID ) {
return crypto . randomUUID ( )
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( /[xy]/g , ( c ) = > {
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.
2026-06-20 14:40:57 -07:00
const r = ( Math . random ( ) * 16 ) | 0
const v = c === 'x' ? r : ( r & 0x3 ) | 0x8
2025-08-26 12:32:21 -07:00
return v . toString ( 16 )
} )
}
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.
2026-06-20 14:40:57 -07:00
/ * *
* @description Time - ordered ( version 7 ) UUID — 48 - bit Unix - ms timestamp prefix
* + random tail . Two ids minted in order sort in order as strings .
* @returns A version - 7 UUID string .
* /
export function v7 ( ) : string {
const ms = Date . now ( )
const b = new Uint8Array ( 16 )
// 48-bit big-endian millisecond timestamp.
b [ 0 ] = Math . floor ( ms / 0x10000000000 ) & 0xff
b [ 1 ] = Math . floor ( ms / 0x100000000 ) & 0xff
b [ 2 ] = Math . floor ( ms / 0x1000000 ) & 0xff
b [ 3 ] = Math . floor ( ms / 0x10000 ) & 0xff
b [ 4 ] = Math . floor ( ms / 0x100 ) & 0xff
b [ 5 ] = ms & 0xff
const rand = randomBytes ( 10 )
b [ 6 ] = ( rand [ 0 ] & 0x0f ) | 0x70 // version 7
b [ 7 ] = rand [ 1 ]
b [ 8 ] = ( rand [ 2 ] & 0x3f ) | 0x80 // variant 10xx
for ( let i = 9 ; i < 16 ; i ++ ) b [ i ] = rand [ i - 6 ] // rand[3..9]
return bytesToUuid ( b )
}
/** Left-rotate a 32-bit word. */
function rotl ( n : number , s : number ) : number {
return ( ( n << s ) | ( n >>> ( 32 - s ) ) ) >>> 0
}
/ * *
* @description SHA - 1 of a byte array → 20 bytes . Pure JS so it runs synchronously
* everywhere ( Web Crypto ' s digest is async - only ) . Used solely by { @link v5 } .
* @param msg - Input bytes .
* @returns The 20 - byte SHA - 1 digest .
* /
function sha1 ( msg : Uint8Array ) : Uint8Array {
const ml = msg . length * 8
const withByte = msg . length + 1
const padLen = withByte % 64 <= 56 ? 56 - ( withByte % 64 ) : 120 - ( withByte % 64 )
const total = msg . length + 1 + padLen + 8
const buf = new Uint8Array ( total )
buf . set ( msg , 0 )
buf [ msg . length ] = 0x80
const hi = Math . floor ( ml / 0x100000000 )
const lo = ml >>> 0
buf [ total - 8 ] = ( hi >>> 24 ) & 0xff
buf [ total - 7 ] = ( hi >>> 16 ) & 0xff
buf [ total - 6 ] = ( hi >>> 8 ) & 0xff
buf [ total - 5 ] = hi & 0xff
buf [ total - 4 ] = ( lo >>> 24 ) & 0xff
buf [ total - 3 ] = ( lo >>> 16 ) & 0xff
buf [ total - 2 ] = ( lo >>> 8 ) & 0xff
buf [ total - 1 ] = lo & 0xff
let h0 = 0x67452301
let h1 = 0xefcdab89
let h2 = 0x98badcfe
let h3 = 0x10325476
let h4 = 0xc3d2e1f0
const w = new Array < number > ( 80 )
for ( let i = 0 ; i < total ; i += 64 ) {
for ( let t = 0 ; t < 16 ; t ++ ) {
w [ t ] =
( ( buf [ i + t * 4 ] << 24 ) |
( buf [ i + t * 4 + 1 ] << 16 ) |
( buf [ i + t * 4 + 2 ] << 8 ) |
buf [ i + t * 4 + 3 ] ) >>>
0
}
for ( let t = 16 ; t < 80 ; t ++ ) {
w [ t ] = rotl ( w [ t - 3 ] ^ w [ t - 8 ] ^ w [ t - 14 ] ^ w [ t - 16 ] , 1 )
}
let a = h0
let b = h1
let c = h2
let d = h3
let e = h4
for ( let t = 0 ; t < 80 ; t ++ ) {
let f : number
let k : number
if ( t < 20 ) {
f = ( b & c ) | ( ~ b & d )
k = 0x5a827999
} else if ( t < 40 ) {
f = b ^ c ^ d
k = 0x6ed9eba1
} else if ( t < 60 ) {
f = ( b & c ) | ( b & d ) | ( c & d )
k = 0x8f1bbcdc
} else {
f = b ^ c ^ d
k = 0xca62c1d6
}
const tmp = ( rotl ( a , 5 ) + ( f >>> 0 ) + e + k + w [ t ] ) >>> 0
e = d
d = c
c = rotl ( b , 30 )
b = a
a = tmp
}
h0 = ( h0 + a ) >>> 0
h1 = ( h1 + b ) >>> 0
h2 = ( h2 + c ) >>> 0
h3 = ( h3 + d ) >>> 0
h4 = ( h4 + e ) >>> 0
}
const out = new Uint8Array ( 20 )
const hs = [ h0 , h1 , h2 , h3 , h4 ]
for ( let i = 0 ; i < 5 ; i ++ ) {
out [ i * 4 ] = ( hs [ i ] >>> 24 ) & 0xff
out [ i * 4 + 1 ] = ( hs [ i ] >>> 16 ) & 0xff
out [ i * 4 + 2 ] = ( hs [ i ] >>> 8 ) & 0xff
out [ i * 4 + 3 ] = hs [ i ] & 0xff
}
return out
}
/ * *
* @description Deterministic version - 5 ( SHA - 1 , namespaced ) UUID . The same
* ` (name, namespace) ` always yields the same UUID — the property the
* id - normalization layer relies on to turn a stable string key into a stable
* UUID id .
* @param name - The name to hash ( e . g . a caller - supplied string id ) .
* @param namespace - A UUID - format namespace ( defaults to { @link BRAINY_ID_NAMESPACE } ) .
* @returns A version - 5 UUID string .
* /
export function v5 ( name : string , namespace : string = BRAINY_ID_NAMESPACE ) : string {
const ns = uuidToBytes ( namespace )
const nameBytes = new TextEncoder ( ) . encode ( name )
const data = new Uint8Array ( 16 + nameBytes . length )
data . set ( ns , 0 )
data . set ( nameBytes , 16 )
const hash = sha1 ( data )
const b = hash . slice ( 0 , 16 )
b [ 6 ] = ( b [ 6 ] & 0x0f ) | 0x50 // version 5
b [ 8 ] = ( b [ 8 ] & 0x3f ) | 0x80 // variant 10xx
return bytesToUuid ( b )
}
/ * *
* @description The fixed namespace for Brainy ' s string → UUID normalization . A
* stable constant ( spells "brainy" in the first bytes ) so the same caller string
* maps to the same UUID across processes and machines .
* /
export const BRAINY_ID_NAMESPACE = '62726169-6e79-5000-8000-000000000000'
// Named export to match the `uuid` package API.
export { v4 as uuidv4 , v7 as uuidv7 , v5 as uuidv5 }
2025-08-26 12:32:21 -07:00
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.
2026-06-20 14:40:57 -07:00
export default { v4 , v7 , v5 , isUUID , BRAINY_ID_NAMESPACE }