๐ src/models/enriched-index.ts
D-OPEN SOVEREIGN
Documentation & Insights
Sidecar binary wire format โ parser and writer (SPEC ยง4).
Every sidecar file begins with a 10-byte header (all multi-byte integers
are big-endian):
Offset Size Field
0 4 Magic number: 0x44504D31 ("DPM1")
4 1 Format version (0x02)
5 3 recordCount (uint24, big-endian, max 16,777,215)
8 1 columnCount (uint8)
9 1 sidecarType (0x00 = Slim, 0x01 = Extended, 0x02 = Provenance)
Format version 0x01 used a uint16 recordCount (2 bytes, max 65,535) and
uint16 relation target UIDs. Version 0x02 expands both to uint24.
Columns follow sequentially: columnType (u8), nameLen (u8), name (UTF-8),
then the column data block. Extended sidecars carry two structured tail
sections after the columns: relations (self-describing) and instances.Column type codes (SPEC ยง4.1).Picks the narrowest column type for a prop's bit width (versions.json).One entry of the instance field descriptor embedded in Extended sidecars.
The instances tail is SELF-DESCRIBING: the writer embeds
`fieldCount + (typeCode, name)*` once per file, and instances are encoded
in descriptor order. The parser never consults any model schema, so:
- old files keep parsing correctly after instanceFields change,
- any model with `multipleInstances: true` works with zero new code,
- files written under an older schema simply yield InstanceData objects
missing the newer fields.Derives the writer's field spec from schema instanceFields
(generated-src JSON shape) plus optional per-index bit layouts from
versions.json (`layout.instanceFields[objectTypeId]`).
The writer appends `isDefault` automatically โ do not include it here.Per-record target-UID arrays for every `has`-direction relation, keyed by
the schema relation name (Extended sidecars only). Self-describing: the
writer embeds the relation names in the file, so the parser needs no
schema and new relations parse with zero code change.Back-compat alias for `relations.get('authors')`.Field descriptor the instances were encoded with (Extended only).Per-record instance objects, keyed by field name (Extended only).Flattens a parsed sidecar into the generated `<Model>SlimCols` /
`<Model>ExtendedCols` / `<Model>ProvCols` object shape.
Provenance sidecars nest their uint8 columns under `fields`
(`{ ids, fields: Record<name, Uint8Array> }`), matching `mergeProv`.Extracts a single record as a cols object whose arrays have exactly one
entry (index 0). Used by the worker's GetRecord command so the main thread
can re-hydrate an evicted record with `merge*(cols, 0, lang)` without the
worker knowing any model schema.One `has`-direction relation serialized into the Extended tail.Schema relation name (e.g. 'authors', 'tags', 'books').Per-record arrays of target UIDs.`has`-direction relation tails (Extended only).Back-compat sugar โ serialized as the `authors` relation.Encode order + types of the instance fields, derived from the schema
via `instanceSpecFromFields`. `isDefault` is appended automatically.
Required when `instances` is provided.Merges the `relations` array with the `authorIds` sugar into one list.1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library โ The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15/**
16 * Sidecar binary wire format โ parser and writer (SPEC ยง4).
17 *
18 * Every sidecar file begins with a 10-byte header (all multi-byte integers
19 * are big-endian):
20 *
21 * Offset Size Field
22 * 0 4 Magic number: 0x44504D31 ("DPM1")
23 * 4 1 Format version (0x02)
24 * 5 3 recordCount (uint24, big-endian, max 16,777,215)
25 * 8 1 columnCount (uint8)
26 * 9 1 sidecarType (0x00 = Slim, 0x01 = Extended, 0x02 = Provenance)
27 *
28 * Format version 0x01 used a uint16 recordCount (2 bytes, max 65,535) and
29 * uint16 relation target UIDs. Version 0x02 expands both to uint24.
30 *
31 * Columns follow sequentially: columnType (u8), nameLen (u8), name (UTF-8),
32 * then the column data block. Extended sidecars carry two structured tail
33 * sections after the columns: relations (self-describing) and instances.
34 */
35import { SidecarType } from "./enums.ts";
36import type { InstanceData } from "./interfaces.ts";
37
38export const SIDECAR_MAGIC = 0x44504D31; // "DPM1"
39export const SIDECAR_FORMAT_VERSION = 0x02;
40
41/** Column type codes (SPEC ยง4.1). */
42export enum ColumnType {
43 Uint8 = 0x01,
44 Uint16 = 0x02,
45 NullableString = 0x03,
46 Relation = 0x04, // structured tail โ flat uint32 stream
47 InstanceArray = 0x05, // structured tail โ self-describing instance section
48 Uint32 = 0x06,
49 Bool = 0x07, // stored as u8 0/1, decoded as boolean
50}
51
52/** Picks the narrowest column type for a prop's bit width (versions.json). */
53export const columnTypeForBits = (bits: number): ColumnType => {
54 if (bits <= 0) return ColumnType.NullableString;
55 if (bits <= 8) return ColumnType.Uint8;
56 if (bits <= 16) return ColumnType.Uint16;
57 return ColumnType.Uint32;
58}
59
60/**
61 * One entry of the instance field descriptor embedded in Extended sidecars.
62 *
63 * The instances tail is SELF-DESCRIBING: the writer embeds
64 * `fieldCount + (typeCode, name)*` once per file, and instances are encoded
65 * in descriptor order. The parser never consults any model schema, so:
66 * - old files keep parsing correctly after instanceFields change,
67 * - any model with `multipleInstances: true` works with zero new code,
68 * - files written under an older schema simply yield InstanceData objects
69 * missing the newer fields.
70 */
71export interface InstanceFieldSpec {
72 name: string;
73 type: ColumnType;
74}
75
76/**
77 * Derives the writer's field spec from schema instanceFields
78 * (generated-src JSON shape) plus optional per-index bit layouts from
79 * versions.json (`layout.instanceFields[objectTypeId]`).
80 * The writer appends `isDefault` automatically โ do not include it here.
81 */
82export const instanceSpecFromFields = (
83 fields: Array<{ name: string; type: string; bits?: number }>,
84 fieldLayouts?: Record<number, { bits: number }>,
85): InstanceFieldSpec[] => {
86 return fields.map((f, idx) => {
87 if (f.type === 'string') return { name: f.name, type: ColumnType.NullableString };
88 if (f.type === 'boolean') return { name: f.name, type: ColumnType.Bool };
89 const bits = fieldLayouts?.[idx]?.bits ?? f.bits ?? 32;
90 return { name: f.name, type: columnTypeForBits(bits) };
91 });
92}
93
94const IS_DEFAULT_SPEC: InstanceFieldSpec = { name: 'isDefault', type: ColumnType.Bool };
95
96export type ColumnData = Uint8Array | Uint16Array | Uint32Array | Array<string | null>;
97
98export interface SidecarHeader {
99 formatVersion: number;
100 recordCount: number;
101 columnCount: number;
102 sidecarType: SidecarType;
103}
104
105export interface ParsedSidecar {
106 header: SidecarHeader;
107 columns: Map<string, ColumnData>;
108 /**
109 * Per-record target-UID arrays for every `has`-direction relation, keyed by
110 * the schema relation name (Extended sidecars only). Self-describing: the
111 * writer embeds the relation names in the file, so the parser needs no
112 * schema and new relations parse with zero code change.
113 */
114 relations?: Map<string, number[][]>;
115 /** Back-compat alias for `relations.get('authors')`. */
116 authorIds?: number[][];
117 /** Field descriptor the instances were encoded with (Extended only). */
118 instanceFields?: InstanceFieldSpec[];
119 /** Per-record instance objects, keyed by field name (Extended only). */
120 instances?: InstanceData[][];
121}
122
123const textDecoder = new TextDecoder();
124const textEncoder = new TextEncoder();
125
126// โโโ Reader โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
127
128class Reader {
129 private view: DataView;
130 offset = 0;
131 constructor(buf: ArrayBuffer) {
132 this.view = new DataView(buf);
133 }
134 get remaining(): number { return this.view.byteLength - this.offset; }
135 u8(): number { const v = this.view.getUint8(this.offset); this.offset += 1; return v; }
136 u16(): number { const v = this.view.getUint16(this.offset); this.offset += 2; return v; }
137 u24(): number { const v = (this.view.getUint8(this.offset) << 16) | this.view.getUint16(this.offset + 1); this.offset += 3; return v; }
138 u32(): number { const v = this.view.getUint32(this.offset); this.offset += 4; return v; }
139 bytes(len: number): Uint8Array {
140 const v = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, len);
141 this.offset += len;
142 return v;
143 }
144 utf8(len: number): string {
145 return textDecoder.decode(this.bytes(len));
146 }
147 nullableString(): string | null {
148 const flag = this.u8();
149 if (flag === 0) return null;
150 const len = this.u16();
151 return this.utf8(len);
152 }
153}
154
155const readColumnData = (r: Reader, type: ColumnType, count: number): ColumnData => {
156 switch (type) {
157 case ColumnType.Uint8: {
158 const out = new Uint8Array(count);
159 for (let i = 0; i < count; i++) out[i] = r.u8();
160 return out;
161 }
162 case ColumnType.Uint16: {
163 const out = new Uint16Array(count);
164 for (let i = 0; i < count; i++) out[i] = r.u16();
165 return out;
166 }
167 case ColumnType.Uint32: {
168 const out = new Uint32Array(count);
169 for (let i = 0; i < count; i++) out[i] = r.u32();
170 return out;
171 }
172 case ColumnType.NullableString: {
173 const out: Array<string | null> = new Array(count);
174 for (let i = 0; i < count; i++) out[i] = r.nullableString();
175 return out;
176 }
177 default:
178 throw new Error(`Unsupported column type 0x${type.toString(16)}`);
179 }
180}
181
182const readFieldValue = (r: Reader, type: ColumnType): string | number | boolean | null => {
183 switch (type) {
184 case ColumnType.Uint8: return r.u8();
185 case ColumnType.Uint16: return r.u16();
186 case ColumnType.Uint32: return r.u32();
187 case ColumnType.Bool: return r.u8() === 1;
188 case ColumnType.NullableString: return r.nullableString();
189 default:
190 throw new Error(`Unsupported instance field type 0x${type.toString(16)}`);
191 }
192}
193
194const readInstanceDescriptor = (r: Reader): InstanceFieldSpec[] => {
195 const fieldCount = r.u8();
196 const spec: InstanceFieldSpec[] = new Array(fieldCount);
197 for (let i = 0; i < fieldCount; i++) {
198 const type = r.u8() as ColumnType;
199 const nameLen = r.u8();
200 spec[i] = { name: r.utf8(nameLen), type };
201 }
202 return spec;
203}
204
205const readInstance = (r: Reader, spec: InstanceFieldSpec[]): InstanceData => {
206 const data: InstanceData = {};
207 for (const field of spec) {
208 data[field.name] = readFieldValue(r, field.type);
209 }
210 return data;
211}
212
213export const parseSidecar = (buf: ArrayBuffer): ParsedSidecar => {
214 const r = new Reader(buf);
215
216 const magic = r.u32();
217 if (magic !== SIDECAR_MAGIC) {
218 throw new Error(`Invalid sidecar magic 0x${magic.toString(16)} (expected DPM1)`);
219 }
220 const formatVersion = r.u8();
221 if (formatVersion !== 0x01 && formatVersion !== SIDECAR_FORMAT_VERSION) {
222 throw new Error(`Unsupported sidecar format version ${formatVersion}`);
223 }
224 const recordCount = formatVersion === 0x01 ? r.u16() : r.u24();
225 const columnCount = r.u8();
226 const sidecarType = r.u8() as SidecarType;
227
228 const columns = new Map<string, ColumnData>();
229 for (let c = 0; c < columnCount; c++) {
230 const colType = r.u8() as ColumnType;
231 const nameLen = r.u8();
232 const name = r.utf8(nameLen);
233 columns.set(name, readColumnData(r, colType, recordCount));
234 }
235
236 const parsed: ParsedSidecar = {
237 header: { formatVersion, recordCount, columnCount, sidecarType },
238 columns,
239 };
240
241 if (sidecarType === SidecarType.Extended) {
242 // Relations section: self-describing descriptor (relCount + names),
243 // then per record, per relation -> count (u16) + count uids (u16 each).
244 const relCount = r.u8();
245 const relNames: string[] = new Array(relCount);
246 for (let k = 0; k < relCount; k++) {
247 const nameLen = r.u8();
248 relNames[k] = r.utf8(nameLen);
249 }
250 if (relCount > 0) {
251 const relations = new Map<string, number[][]>();
252 for (const name of relNames) relations.set(name, new Array(recordCount));
253 const readUid = formatVersion === 0x01 ? () => r.u16() : () => r.u24();
254 for (let i = 0; i < recordCount; i++) {
255 for (const name of relNames) {
256 const count = r.u16();
257 const uids: number[] = new Array(count);
258 for (let j = 0; j < count; j++) uids[j] = readUid();
259 relations.get(name)![i] = uids;
260 }
261 }
262 parsed.relations = relations;
263 const authors = relations.get('authors');
264 if (authors) parsed.authorIds = authors;
265 }
266
267 // instances section: self-describing field descriptor, then per
268 // record -> instCount (u16) + instances encoded in descriptor order
269 const spec = readInstanceDescriptor(r);
270 const instances: InstanceData[][] = new Array(recordCount);
271 for (let i = 0; i < recordCount; i++) {
272 const instCount = r.u16();
273 const list: InstanceData[] = new Array(instCount);
274 for (let j = 0; j < instCount; j++) list[j] = readInstance(r, spec);
275 instances[i] = list;
276 }
277 parsed.instanceFields = spec;
278 parsed.instances = instances;
279 }
280
281 return parsed;
282}
283
284// โโโ Cols helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
285
286/**
287 * Flattens a parsed sidecar into the generated `<Model>SlimCols` /
288 * `<Model>ExtendedCols` / `<Model>ProvCols` object shape.
289 * Provenance sidecars nest their uint8 columns under `fields`
290 * (`{ ids, fields: Record<name, Uint8Array> }`), matching `mergeProv`.
291 */
292export const colsFromSidecar = (parsed: ParsedSidecar): Record<string, any> => {
293 if (parsed.header.sidecarType === SidecarType.Provenance) {
294 const fields: Record<string, any> = {};
295 let ids: ColumnData | undefined;
296 for (const [name, data] of parsed.columns) {
297 if (name === 'ids') ids = data;
298 else fields[name] = data;
299 }
300 return { ids, fields };
301 }
302 const cols: Record<string, any> = {};
303 for (const [name, data] of parsed.columns) {
304 cols[name] = data;
305 }
306 if (parsed.relations) for (const [name, data] of parsed.relations) cols[name] = data;
307 if (parsed.authorIds) cols['authorIds'] = parsed.authorIds;
308 if (parsed.instances) cols['instances'] = parsed.instances;
309 return cols;
310}
311
312/**
313 * Extracts a single record as a cols object whose arrays have exactly one
314 * entry (index 0). Used by the worker's GetRecord command so the main thread
315 * can re-hydrate an evicted record with `merge*(cols, 0, lang)` without the
316 * worker knowing any model schema.
317 */
318export const extractRecordCols = (parsed: ParsedSidecar, index: number): Record<string, any> => {
319 const sliceCol = (data: ColumnData) =>
320 Array.isArray(data) ? [data[index] ?? null] : data.slice(index, index + 1);
321
322 if (parsed.header.sidecarType === SidecarType.Provenance) {
323 const fields: Record<string, any> = {};
324 let ids: ColumnData | undefined;
325 for (const [name, data] of parsed.columns) {
326 if (name === 'ids') ids = sliceCol(data);
327 else fields[name] = sliceCol(data);
328 }
329 return { ids, fields };
330 }
331
332 const cols: Record<string, any> = {};
333 for (const [name, data] of parsed.columns) {
334 cols[name] = sliceCol(data);
335 }
336 if (parsed.relations) for (const [name, data] of parsed.relations) cols[name] = [data[index] ?? []];
337 if (parsed.authorIds) cols['authorIds'] = [parsed.authorIds[index] ?? []];
338 if (parsed.instances) cols['instances'] = [parsed.instances[index] ?? []];
339 return cols;
340}
341
342// โโโ Writer (used by tests and the librarian sidecar build scripts) โโโโโโโโโโ
343
344class Writer {
345 private chunks: Uint8Array[] = [];
346 u8(v: number) { this.chunks.push(new Uint8Array([v & 0xFF])); }
347 u16(v: number) {
348 const b = new Uint8Array(2);
349 new DataView(b.buffer).setUint16(0, v);
350 this.chunks.push(b);
351 }
352 u24(v: number) {
353 const b = new Uint8Array(3);
354 b[0] = (v >>> 16) & 0xFF;
355 b[1] = (v >>> 8) & 0xFF;
356 b[2] = v & 0xFF;
357 this.chunks.push(b);
358 }
359 u32(v: number) {
360 const b = new Uint8Array(4);
361 new DataView(b.buffer).setUint32(0, v >>> 0);
362 this.chunks.push(b);
363 }
364 bytes(v: Uint8Array) { this.chunks.push(v); }
365 nullableString(v: string | null) {
366 if (v === null) {
367 this.u8(0);
368 return;
369 }
370 this.u8(1);
371 const encoded = textEncoder.encode(v);
372 this.u16(encoded.length);
373 this.bytes(encoded);
374 }
375 toBuffer(): ArrayBuffer {
376 const total = this.chunks.reduce((acc, c) => acc + c.length, 0);
377 const out = new Uint8Array(total);
378 let offset = 0;
379 for (const c of this.chunks) {
380 out.set(c, offset);
381 offset += c.length;
382 }
383 return out.buffer;
384 }
385}
386
387export interface SidecarColumn {
388 name: string;
389 type: ColumnType;
390 data: ColumnData;
391}
392
393/** One `has`-direction relation serialized into the Extended tail. */
394export interface SidecarRelationInput {
395 /** Schema relation name (e.g. 'authors', 'tags', 'books'). */
396 name: string;
397 /** Per-record arrays of target UIDs. */
398 data: number[][];
399}
400
401export interface SidecarInput {
402 sidecarType: SidecarType;
403 recordCount: number;
404 columns: SidecarColumn[];
405 /** `has`-direction relation tails (Extended only). */
406 relations?: SidecarRelationInput[];
407 /** Back-compat sugar โ serialized as the `authors` relation. */
408 authorIds?: number[][];
409 /**
410 * Encode order + types of the instance fields, derived from the schema
411 * via `instanceSpecFromFields`. `isDefault` is appended automatically.
412 * Required when `instances` is provided.
413 */
414 instanceFields?: InstanceFieldSpec[];
415 instances?: InstanceData[][];
416}
417
418/** Merges the `relations` array with the `authorIds` sugar into one list. */
419const collectRelations = (input: SidecarInput): SidecarRelationInput[] => {
420 const list = [...(input.relations ?? [])];
421 if (input.authorIds && !list.some(r => r.name === 'authors')) {
422 list.push({ name: 'authors', data: input.authorIds });
423 }
424 return list;
425}
426
427const writeFieldValue = (w: Writer, type: ColumnType, value: string | number | boolean | null | undefined) => {
428 switch (type) {
429 case ColumnType.Uint8: w.u8((value as number) ?? 0); break;
430 case ColumnType.Uint16: w.u16((value as number) ?? 0); break;
431 case ColumnType.Uint32: w.u32((value as number) ?? 0); break;
432 case ColumnType.Bool: w.u8(value ? 1 : 0); break;
433 case ColumnType.NullableString: w.nullableString((value as string) ?? null); break;
434 default:
435 throw new Error(`Unsupported instance field type 0x${type.toString(16)}`);
436 }
437}
438
439const writeInstanceDescriptor = (w: Writer, spec: InstanceFieldSpec[]) => {
440 if (spec.length > 255) throw new Error('Too many instance fields (max 255)');
441 w.u8(spec.length);
442 for (const field of spec) {
443 w.u8(field.type);
444 const nameBytes = textEncoder.encode(field.name);
445 w.u8(nameBytes.length);
446 w.bytes(nameBytes);
447 }
448}
449
450export const writeSidecar = (input: SidecarInput): ArrayBuffer => {
451 const w = new Writer();
452 w.u32(SIDECAR_MAGIC);
453 w.u8(SIDECAR_FORMAT_VERSION);
454 w.u24(input.recordCount);
455 w.u8(input.columns.length);
456 w.u8(input.sidecarType);
457
458 for (const col of input.columns) {
459 w.u8(col.type);
460 const nameBytes = textEncoder.encode(col.name);
461 w.u8(nameBytes.length);
462 w.bytes(nameBytes);
463
464 const data = col.data;
465 for (let i = 0; i < input.recordCount; i++) {
466 switch (col.type) {
467 case ColumnType.Uint8: w.u8((data as Uint8Array)[i] ?? 0); break;
468 case ColumnType.Uint16: w.u16((data as Uint16Array)[i] ?? 0); break;
469 case ColumnType.Uint32: w.u32((data as Uint32Array)[i] ?? 0); break;
470 case ColumnType.Bool: w.u8((data as Uint8Array)[i] ? 1 : 0); break;
471 case ColumnType.NullableString:
472 w.nullableString((data as Array<string | null>)[i] ?? null);
473 break;
474 default:
475 throw new Error(`Unsupported column type 0x${col.type.toString(16)}`);
476 }
477 }
478 }
479
480 if (input.sidecarType === SidecarType.Extended) {
481 // Self-describing relations section: descriptor (count + names) first,
482 // then per record, per relation -> count (u16) + uids (u16 each).
483 const relations = collectRelations(input);
484 w.u8(relations.length);
485 for (const rel of relations) {
486 const nameBytes = textEncoder.encode(rel.name);
487 w.u8(nameBytes.length);
488 w.bytes(nameBytes);
489 }
490 for (let i = 0; i < input.recordCount; i++) {
491 for (const rel of relations) {
492 const uids = rel.data[i] ?? [];
493 w.u16(uids.length);
494 for (const u of uids) w.u24(u);
495 }
496 }
497
498 // Self-describing instances section: descriptor first, then data.
499 const declared = input.instanceFields ?? [];
500 if ((input.instances?.length ?? 0) > 0 && declared.length === 0) {
501 throw new Error('writeSidecar: instances provided without instanceFields spec');
502 }
503 const spec = declared.some(f => f.name === IS_DEFAULT_SPEC.name)
504 ? declared
505 : [...declared, IS_DEFAULT_SPEC];
506 writeInstanceDescriptor(w, spec);
507
508 const instances = input.instances ?? [];
509 for (let i = 0; i < input.recordCount; i++) {
510 const list = instances[i] ?? [];
511 w.u16(list.length);
512 for (const inst of list) {
513 for (const field of spec) {
514 writeFieldValue(w, field.type, inst[field.name]);
515 }
516 }
517 }
518 }
519
520 return w.toBuffer();
521}
522