agent-avatars 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1399 @@
1
+ /**
2
+ * Deterministic Agent Avatars.
3
+ *
4
+ * Deterministic, zero-dependency 5x4 avatar generation backed by an
5
+ * exhaustive, uniformly addressable catalog of symmetric masks.
6
+ */
7
+
8
+ const { buildPaletteDistanceMatrix, shapeHammingDistance } = require("./visual-distance.cjs");
9
+ const { createBoundedLruCache } = require("./catalog-cache.cjs");
10
+ const { normalizeHexColor: normalizeColor, snapshotRenderableDescriptor } = require("./render-descriptor.cjs");
11
+
12
+ const STYLE_VERSION = "1";
13
+ // Compatibility identifier: changing this would change deterministic outputs.
14
+ const LIBRARY_ID = "deterministic-agent-avatars";
15
+ const GRID_W = 5;
16
+ const GRID_H = 4;
17
+ const HALF_W = 3;
18
+ const CELL = 10;
19
+ const GLYPH_X = 39;
20
+ const GLYPH_Y = 44;
21
+ const CELL_RADIUS = 2;
22
+ const MAX_SIZE = 4096;
23
+ const RAW_SYMMETRIC_MASKS = 1 << (GRID_H * HALF_W); // 4096
24
+ const DEFAULT_NAMESPACE = "default";
25
+ const DEFAULT_SEED_MODE = "human";
26
+ const DEFAULT_THEME = "light";
27
+ const MIN_CUSTOM_CONTRAST = 4.5;
28
+ const MAX_CUSTOM_PALETTES = 256;
29
+ const MAX_IDENTITY_SET_ITEMS = 10_000;
30
+ const MAX_MANIFEST_ENTRIES = 10_000;
31
+
32
+ // Sixteen deliberately separated pastel families. Every family contains a
33
+ // light and dark pair with WCAG contrast comfortably above 4.5:1.
34
+ const BUILTIN_PALETTES = Object.freeze([
35
+ palette("rose", "#F1DADA", "#492727", "#3D1F1F", "#EED3D3"),
36
+ palette("leaf", "#BCF1BC", "#274927", "#1F3D1F", "#D3EED3"),
37
+ palette("indigo", "#BCBCF1", "#272749", "#1F1F3D", "#D3D3EE"),
38
+ palette("aqua", "#BCF1F1", "#274949", "#1F3D3D", "#D3EEEE"),
39
+ palette("sand", "#F1E8BC", "#494327", "#3D381F", "#EEEAD3"),
40
+ palette("orchid", "#F1BCDF", "#49273E", "#3D1F33", "#EED3E5"),
41
+ palette("sky", "#C8DAF3", "#273549", "#1F2B3D", "#D3DEEE"),
42
+ palette("mist", "#EAF5E5", "#324927", "#293D1F", "#DCEED3"),
43
+ palette("coral", "#F1BCBC", "#492727", "#3D1F1F", "#EED3D3"),
44
+ palette("mint", "#C0EDD2", "#274935", "#1F3D2B", "#D3EEDE"),
45
+ palette("lilac", "#EDD7F4", "#412749", "#351F3D", "#E7D3EE"),
46
+ palette("apricot", "#F1D2BC", "#493527", "#3D2B1F", "#EEDED3"),
47
+ palette("ice", "#E5EEF5", "#273B49", "#1F303D", "#D3E3EE"),
48
+ palette("violet", "#DFBCF1", "#3E2749", "#331F3D", "#E5D3EE"),
49
+ palette("lime", "#D6EDC0", "#384927", "#2E3D1F", "#E0EED3"),
50
+ palette("berry", "#E9C4D3", "#492735", "#3D1F2B", "#EED3DE"),
51
+ ]);
52
+
53
+ const STANDARD_CONSTRAINTS = Object.freeze({
54
+ minPixels: 6,
55
+ maxPixels: 12,
56
+ minDensity: 0.18,
57
+ maxDensity: 0.82,
58
+ maxDiagonalConnections: 2,
59
+ connectivity: 8,
60
+ maxHoles: Infinity,
61
+ });
62
+
63
+ const UTF8 = new TextEncoder();
64
+ const POP5 = Object.freeze(Array.from({ length: 32 }, (_, value) => popCount(value)));
65
+ const CATALOG_CACHE = createBoundedLruCache(64);
66
+ const SHAPE_NEIGHBOR_CACHES = new WeakMap();
67
+ const PALETTE_NEIGHBOR_CACHES = new WeakMap();
68
+
69
+ function palette(id, lightBackground, lightForeground, darkBackground, darkForeground) {
70
+ return Object.freeze({
71
+ id,
72
+ light: Object.freeze({
73
+ background: lightBackground,
74
+ foreground: lightForeground,
75
+ }),
76
+ dark: Object.freeze({
77
+ background: darkBackground,
78
+ foreground: darkForeground,
79
+ }),
80
+ visualKey: `${lightBackground}/${lightForeground}/${darkBackground}/${darkForeground}`,
81
+ });
82
+ }
83
+
84
+ function popCount(value) {
85
+ let count = 0;
86
+ let x = value >>> 0;
87
+ while (x !== 0) {
88
+ x &= x - 1;
89
+ count++;
90
+ }
91
+ return count;
92
+ }
93
+
94
+ function trimNumber(value) {
95
+ const text = Number(value).toFixed(4).replace(/\.?0+$/, "");
96
+ return text === "" || text === "-0" ? "0" : text;
97
+ }
98
+
99
+ function normalizeSize(value) {
100
+ const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
101
+ if (typeof number !== "number" || !Number.isFinite(number) || number <= 0 || number > MAX_SIZE) {
102
+ throw new TypeError(`Avatar size must be a finite number in (0, ${MAX_SIZE}].`);
103
+ }
104
+ const normalized = trimNumber(number);
105
+ if (Number(normalized) <= 0) {
106
+ throw new RangeError("Avatar size rounds to zero at the supported four-decimal precision.");
107
+ }
108
+ return normalized;
109
+ }
110
+
111
+ function canonicalSeed(value, mode = DEFAULT_SEED_MODE) {
112
+ const string = String(value ?? "");
113
+ if (mode === "raw") return string;
114
+ if (mode !== "human") throw new TypeError(`Unsupported seedMode: ${mode}`);
115
+ return string.normalize("NFKC").trim().toLowerCase();
116
+ }
117
+
118
+ function canonicalNamespace(value, mode = "human") {
119
+ const normalized = canonicalSeed(value ?? DEFAULT_NAMESPACE, mode);
120
+ if (normalized.length === 0) {
121
+ throw new TypeError("namespace must not be empty after canonicalization.");
122
+ }
123
+ return normalized;
124
+ }
125
+
126
+ function encodePart(value) {
127
+ return `${value.length}:${value}`;
128
+ }
129
+
130
+ function domainMessage(domain, canonical, namespace, nonce = 0) {
131
+ return `${LIBRARY_ID}\u0000${STYLE_VERSION}\u0000${domain}\u0000${encodePart(namespace)}\u0000${encodePart(canonical)}\u0000${nonce}`;
132
+ }
133
+
134
+ function hash32String(input) {
135
+ const bytes = UTF8.encode(input);
136
+ let hash = 2166136261;
137
+ for (const byte of bytes) {
138
+ hash ^= byte;
139
+ hash = Math.imul(hash, 16777619);
140
+ }
141
+
142
+ // Final avalanche improves distribution for structured and sequential IDs.
143
+ hash ^= hash >>> 16;
144
+ hash = Math.imul(hash, 0x85ebca6b);
145
+ hash ^= hash >>> 13;
146
+ hash = Math.imul(hash, 0xc2b2ae35);
147
+ hash ^= hash >>> 16;
148
+ return hash >>> 0;
149
+ }
150
+
151
+ function hash32(value, options = {}) {
152
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
153
+ throw new TypeError("hash options must be an object.");
154
+ }
155
+ const seedMode = options.seedMode ?? DEFAULT_SEED_MODE;
156
+ const namespaceMode = options.namespaceMode ?? "human";
157
+ const canonical = canonicalSeed(value, seedMode);
158
+ const namespace = canonicalNamespace(options.namespace ?? DEFAULT_NAMESPACE, namespaceMode);
159
+ const domain = options.domain ?? "public";
160
+ if (typeof domain !== "string" || domain.length === 0 || domain.includes("\u0000")) {
161
+ throw new TypeError("domain must be a non-empty string without null characters.");
162
+ }
163
+ return hash32String(domainMessage(domain, canonical, namespace, 0));
164
+ }
165
+
166
+ function hashHex128(message) {
167
+ let output = "";
168
+ for (let index = 0; index < 4; index++) {
169
+ output += hash32String(`${index}\u0000${message}`).toString(16).padStart(8, "0");
170
+ }
171
+ return output;
172
+ }
173
+
174
+ function xorshift32(seed) {
175
+ let state = seed || 0x9e3779b9;
176
+ return function next() {
177
+ state ^= state << 13;
178
+ state ^= state >>> 17;
179
+ state ^= state << 5;
180
+ return state >>> 0;
181
+ };
182
+ }
183
+
184
+ function randInt(next, max) {
185
+ if (!Number.isInteger(max) || max <= 0 || max > 0x100000000) {
186
+ throw new RangeError("max must be an integer in [1, 2^32].");
187
+ }
188
+
189
+ const limit = 0x100000000 - (0x100000000 % max);
190
+ let value = next();
191
+ while (value >= limit) value = next();
192
+ return value % max;
193
+ }
194
+
195
+ function pickIndex(hash, length) {
196
+ return randInt(xorshift32(hash), length);
197
+ }
198
+
199
+ function rowMask(x) {
200
+ return 1 << (GRID_W - 1 - x);
201
+ }
202
+
203
+ function mirrorX(x) {
204
+ return GRID_W - 1 - x;
205
+ }
206
+
207
+ function rowsFromSymmetricMask(mask) {
208
+ if (!Number.isInteger(mask) || mask < 0 || mask >= RAW_SYMMETRIC_MASKS) {
209
+ throw new RangeError(`mask must be an integer in [0, ${RAW_SYMMETRIC_MASKS - 1}].`);
210
+ }
211
+
212
+ const rows = Array(GRID_H).fill(0);
213
+ for (let y = 0; y < GRID_H; y++) {
214
+ for (let halfX = 0; halfX < HALF_W; halfX++) {
215
+ const bitIndex = y * HALF_W + halfX;
216
+ if (((mask >>> bitIndex) & 1) === 0) continue;
217
+ rows[y] |= rowMask(halfX);
218
+ rows[y] |= rowMask(mirrorX(halfX));
219
+ }
220
+ }
221
+ return rows;
222
+ }
223
+
224
+ function symmetricMaskFromRows(rows) {
225
+ const normalizedRows = normalizeNumericRows(rows);
226
+ if (!isMirroredRows(normalizedRows)) {
227
+ throw new TypeError("Rows are not mirrored across the vertical axis.");
228
+ }
229
+
230
+ let mask = 0;
231
+ for (let y = 0; y < GRID_H; y++) {
232
+ for (let halfX = 0; halfX < HALF_W; halfX++) {
233
+ if (cellOn(normalizedRows, halfX, y)) mask |= 1 << (y * HALF_W + halfX);
234
+ }
235
+ }
236
+ return mask >>> 0;
237
+ }
238
+
239
+ function cellOn(rows, x, y) {
240
+ return y >= 0 && y < GRID_H && x >= 0 && x < GRID_W && ((rows[y] >>> (GRID_W - 1 - x)) & 1) === 1;
241
+ }
242
+
243
+ function rowsToGrid(rows) {
244
+ return Array.from({ length: GRID_H }, (_, y) =>
245
+ Array.from({ length: GRID_W }, (_, x) => cellOn(rows, x, y))
246
+ );
247
+ }
248
+
249
+ function gridToRows(grid) {
250
+ if (
251
+ !Array.isArray(grid)
252
+ || grid.length !== GRID_H
253
+ || grid.some((row) => (
254
+ !Array.isArray(row)
255
+ || row.length !== GRID_W
256
+ || row.some((cell) => typeof cell !== "boolean")
257
+ ))
258
+ ) {
259
+ throw new TypeError(`grid must be a ${GRID_H}x${GRID_W} boolean matrix.`);
260
+ }
261
+
262
+ const rows = Array(GRID_H).fill(0);
263
+ for (let y = 0; y < GRID_H; y++) {
264
+ for (let x = 0; x < GRID_W; x++) {
265
+ if (grid[y][x]) rows[y] |= rowMask(x);
266
+ }
267
+ }
268
+ return rows;
269
+ }
270
+
271
+ function normalizeNumericRows(input) {
272
+ if (!Array.isArray(input) || input.length !== GRID_H || input.some((row) => !Number.isInteger(row) || row < 0 || row > 31)) {
273
+ throw new TypeError(`rows must contain ${GRID_H} integers in [0, 31].`);
274
+ }
275
+ return input.slice();
276
+ }
277
+
278
+ function normalizeRows(input) {
279
+ if (Array.isArray(input?.[0])) return gridToRows(input);
280
+ return normalizeNumericRows(input);
281
+ }
282
+
283
+ function cellCountRows(rows) {
284
+ let count = 0;
285
+ for (const row of rows) count += POP5[row & 0b11111];
286
+ return count;
287
+ }
288
+
289
+ function boundsRows(rows) {
290
+ let minX = GRID_W;
291
+ let minY = GRID_H;
292
+ let maxX = -1;
293
+ let maxY = -1;
294
+
295
+ for (let y = 0; y < GRID_H; y++) {
296
+ for (let x = 0; x < GRID_W; x++) {
297
+ if (!cellOn(rows, x, y)) continue;
298
+ minX = Math.min(minX, x);
299
+ minY = Math.min(minY, y);
300
+ maxX = Math.max(maxX, x);
301
+ maxY = Math.max(maxY, y);
302
+ }
303
+ }
304
+
305
+ if (maxX < minX || maxY < minY) {
306
+ return Object.freeze({ minX: 0, minY: 0, maxX: -1, maxY: -1, width: 0, height: 0 });
307
+ }
308
+
309
+ return Object.freeze({
310
+ minX,
311
+ minY,
312
+ maxX,
313
+ maxY,
314
+ width: maxX - minX + 1,
315
+ height: maxY - minY + 1,
316
+ });
317
+ }
318
+
319
+ function densityRows(rows, bounds = boundsRows(rows), count = cellCountRows(rows)) {
320
+ if (count === 0) return 0;
321
+ return count / Math.max(1, bounds.width * bounds.height);
322
+ }
323
+
324
+ function isMirroredRows(rows) {
325
+ for (let y = 0; y < GRID_H; y++) {
326
+ for (let x = 0; x < GRID_W; x++) {
327
+ if (cellOn(rows, x, y) !== cellOn(rows, mirrorX(x), y)) return false;
328
+ }
329
+ }
330
+ return true;
331
+ }
332
+
333
+ function componentCountRows(rows, connectivity = 8) {
334
+ const offsets = connectivity === 4
335
+ ? [[-1, 0], [1, 0], [0, -1], [0, 1]]
336
+ : [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]];
337
+ const seen = Array.from({ length: GRID_H }, () => Array(GRID_W).fill(false));
338
+ let components = 0;
339
+
340
+ for (let y = 0; y < GRID_H; y++) {
341
+ for (let x = 0; x < GRID_W; x++) {
342
+ if (!cellOn(rows, x, y) || seen[y][x]) continue;
343
+ components++;
344
+ const stack = [[x, y]];
345
+ seen[y][x] = true;
346
+
347
+ while (stack.length > 0) {
348
+ const [currentX, currentY] = stack.pop();
349
+ for (const [dx, dy] of offsets) {
350
+ const nextX = currentX + dx;
351
+ const nextY = currentY + dy;
352
+ if (nextY < 0 || nextY >= GRID_H || nextX < 0 || nextX >= GRID_W) continue;
353
+ if (!cellOn(rows, nextX, nextY) || seen[nextY][nextX]) continue;
354
+ seen[nextY][nextX] = true;
355
+ stack.push([nextX, nextY]);
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ return components;
362
+ }
363
+
364
+ function diagonalTouchCountRows(rows) {
365
+ let count = 0;
366
+ for (let y = 0; y < GRID_H - 1; y++) {
367
+ for (let x = 0; x < GRID_W - 1; x++) {
368
+ const a = cellOn(rows, x, y);
369
+ const b = cellOn(rows, x + 1, y);
370
+ const c = cellOn(rows, x, y + 1);
371
+ const d = cellOn(rows, x + 1, y + 1);
372
+ if ((a && d && !b && !c) || (b && c && !a && !d)) count++;
373
+ }
374
+ }
375
+ return count;
376
+ }
377
+
378
+ function diagonalTouchCountRowsIgnoringMirror(rows) {
379
+ let count = 0;
380
+ const maxUniqueWindowX = Math.floor((GRID_W - 2) / 2);
381
+ for (let y = 0; y < GRID_H - 1; y++) {
382
+ for (let x = 0; x <= maxUniqueWindowX; x++) {
383
+ const a = cellOn(rows, x, y);
384
+ const b = cellOn(rows, x + 1, y);
385
+ const c = cellOn(rows, x, y + 1);
386
+ const d = cellOn(rows, x + 1, y + 1);
387
+ if ((a && d && !b && !c) || (b && c && !a && !d)) count++;
388
+ }
389
+ }
390
+ return count;
391
+ }
392
+
393
+ function holeCountRows(rows) {
394
+ const paddedWidth = GRID_W + 2;
395
+ const paddedHeight = GRID_H + 2;
396
+ const outside = Array.from({ length: paddedHeight }, () => Array(paddedWidth).fill(false));
397
+ const outsideStack = [[0, 0]];
398
+ outside[0][0] = true;
399
+
400
+ while (outsideStack.length > 0) {
401
+ const [currentX, currentY] = outsideStack.pop();
402
+ for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
403
+ const nextX = currentX + dx;
404
+ const nextY = currentY + dy;
405
+ if (nextY < 0 || nextY >= paddedHeight || nextX < 0 || nextX >= paddedWidth || outside[nextY][nextX]) continue;
406
+ const gridX = nextX - 1;
407
+ const gridY = nextY - 1;
408
+ if (gridX >= 0 && gridX < GRID_W && gridY >= 0 && gridY < GRID_H && cellOn(rows, gridX, gridY)) continue;
409
+ outside[nextY][nextX] = true;
410
+ outsideStack.push([nextX, nextY]);
411
+ }
412
+ }
413
+
414
+ let holes = 0;
415
+ const seenHole = Array.from({ length: GRID_H }, () => Array(GRID_W).fill(false));
416
+ for (let y = 0; y < GRID_H; y++) {
417
+ for (let x = 0; x < GRID_W; x++) {
418
+ if (cellOn(rows, x, y) || outside[y + 1][x + 1] || seenHole[y][x]) continue;
419
+ holes++;
420
+ const stack = [[x, y]];
421
+ seenHole[y][x] = true;
422
+ while (stack.length > 0) {
423
+ const [currentX, currentY] = stack.pop();
424
+ for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
425
+ const nextX = currentX + dx;
426
+ const nextY = currentY + dy;
427
+ if (nextY < 0 || nextY >= GRID_H || nextX < 0 || nextX >= GRID_W) continue;
428
+ if (cellOn(rows, nextX, nextY) || outside[nextY + 1][nextX + 1] || seenHole[nextY][nextX]) continue;
429
+ seenHole[nextY][nextX] = true;
430
+ stack.push([nextX, nextY]);
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ return holes;
437
+ }
438
+
439
+ function analyzeRows(rows) {
440
+ const bounds = boundsRows(rows);
441
+ const cellCount = cellCountRows(rows);
442
+ return Object.freeze({
443
+ rows: Object.freeze(rows.slice()),
444
+ cellCount,
445
+ density: densityRows(rows, bounds, cellCount),
446
+ bounds,
447
+ mirrored: isMirroredRows(rows),
448
+ connectedComponents4: componentCountRows(rows, 4),
449
+ connectedComponents8: componentCountRows(rows, 8),
450
+ holeCount: holeCountRows(rows),
451
+ diagonalTouchCount: diagonalTouchCountRows(rows),
452
+ diagonalTouchCountIgnoringMirror: diagonalTouchCountRowsIgnoringMirror(rows),
453
+ });
454
+ }
455
+
456
+ function normalizeConstraints(options = {}) {
457
+ const base = STANDARD_CONSTRAINTS;
458
+ const minPixels = Number(options.minPixels ?? base.minPixels);
459
+ const maxPixels = Number(options.maxPixels ?? base.maxPixels);
460
+ const minDensity = Number(options.minDensity ?? base.minDensity);
461
+ const maxDensity = Number(options.maxDensity ?? base.maxDensity);
462
+ const maxDiagonalConnections = Number(options.maxDiagonalConnections ?? base.maxDiagonalConnections);
463
+ const connectivity = Number(options.connectivity ?? base.connectivity);
464
+ const maxHolesValue = options.maxHoles ?? base.maxHoles;
465
+ const maxHoles = maxHolesValue === Infinity ? Infinity : Number(maxHolesValue);
466
+
467
+ if (!Number.isInteger(minPixels) || !Number.isInteger(maxPixels)) {
468
+ throw new TypeError("minPixels and maxPixels must be integers.");
469
+ }
470
+ if (minPixels < 1 || maxPixels < minPixels || maxPixels > GRID_W * GRID_H) {
471
+ throw new RangeError(`Pixel limits must satisfy 1 <= minPixels <= maxPixels <= ${GRID_W * GRID_H}.`);
472
+ }
473
+ if (!Number.isFinite(minDensity) || !Number.isFinite(maxDensity)) {
474
+ throw new TypeError("minDensity and maxDensity must be finite numbers.");
475
+ }
476
+ if (minDensity < 0 || maxDensity > 1 || maxDensity < minDensity) {
477
+ throw new RangeError("Density limits must satisfy 0 <= minDensity <= maxDensity <= 1.");
478
+ }
479
+ if (!Number.isInteger(maxDiagonalConnections) || maxDiagonalConnections < 0) {
480
+ throw new TypeError("maxDiagonalConnections must be a non-negative integer.");
481
+ }
482
+ if (connectivity !== 4 && connectivity !== 8) {
483
+ throw new TypeError("connectivity must be 4 or 8.");
484
+ }
485
+ if (maxHoles !== Infinity && (!Number.isInteger(maxHoles) || maxHoles < 0)) {
486
+ throw new TypeError("maxHoles must be a non-negative integer or Infinity.");
487
+ }
488
+
489
+ return Object.freeze({
490
+ minPixels,
491
+ maxPixels,
492
+ minDensity,
493
+ maxDensity,
494
+ maxDiagonalConnections,
495
+ connectivity,
496
+ maxHoles,
497
+ });
498
+ }
499
+
500
+ function shapePassesConstraints(shape, constraints) {
501
+ if (shape.cellCount < constraints.minPixels || shape.cellCount > constraints.maxPixels) return false;
502
+ if (shape.density < constraints.minDensity || shape.density > constraints.maxDensity) return false;
503
+ if (shape.bounds.width < 2 || shape.bounds.height < 2) return false;
504
+ if (!shape.mirrored) return false;
505
+ if ((constraints.connectivity === 4 ? shape.connectedComponents4 : shape.connectedComponents8) !== 1) return false;
506
+ if (shape.diagonalTouchCountIgnoringMirror > constraints.maxDiagonalConnections) return false;
507
+ if (shape.holeCount > constraints.maxHoles) return false;
508
+ return true;
509
+ }
510
+
511
+ function constraintsKey(constraints) {
512
+ return [
513
+ constraints.minPixels,
514
+ constraints.maxPixels,
515
+ constraints.minDensity,
516
+ constraints.maxDensity,
517
+ constraints.maxDiagonalConnections,
518
+ constraints.connectivity,
519
+ constraints.maxHoles === Infinity ? "inf" : constraints.maxHoles,
520
+ ].join("|");
521
+ }
522
+
523
+ const STANDARD_CONSTRAINTS_KEY = constraintsKey(STANDARD_CONSTRAINTS);
524
+ let allShapes;
525
+ let standardCatalog;
526
+
527
+ function getAllShapes() {
528
+ if (allShapes) return allShapes;
529
+ allShapes = Object.freeze(Array.from({ length: RAW_SYMMETRIC_MASKS }, (_, mask) => {
530
+ const analysis = analyzeRows(rowsFromSymmetricMask(mask));
531
+ return Object.freeze({
532
+ id: `s${mask.toString(16).padStart(3, "0")}`,
533
+ mask,
534
+ ...analysis,
535
+ });
536
+ }));
537
+ return allShapes;
538
+ }
539
+
540
+ function getStandardCatalog() {
541
+ if (!standardCatalog) {
542
+ standardCatalog = Object.freeze(
543
+ getAllShapes().filter((shape) => shapePassesConstraints(shape, STANDARD_CONSTRAINTS))
544
+ );
545
+ }
546
+ return standardCatalog;
547
+ }
548
+
549
+ function getCatalog(constraints) {
550
+ const key = constraintsKey(constraints);
551
+ if (key === STANDARD_CONSTRAINTS_KEY) return getStandardCatalog();
552
+ const cached = CATALOG_CACHE.get(key);
553
+ if (cached) return cached;
554
+ return CATALOG_CACHE.set(
555
+ key,
556
+ Object.freeze(getAllShapes().filter((shape) => shapePassesConstraints(shape, constraints)))
557
+ );
558
+ }
559
+
560
+ function validateAvatarBitmap(gridOrRows, options = {}) {
561
+ const rows = normalizeRows(gridOrRows);
562
+ const constraints = normalizeConstraints(options);
563
+ const analysis = analyzeRows(rows);
564
+ return {
565
+ valid: shapePassesConstraints(analysis, constraints),
566
+ connectedComponents4: analysis.connectedComponents4,
567
+ connectedComponents8: analysis.connectedComponents8,
568
+ cellCount: analysis.cellCount,
569
+ density: analysis.density,
570
+ mirrored: analysis.mirrored,
571
+ holeCount: analysis.holeCount,
572
+ diagonalTouchCount: analysis.diagonalTouchCount,
573
+ diagonalTouchCountIgnoringMirror: analysis.diagonalTouchCountIgnoringMirror,
574
+ bounds: analysis.bounds,
575
+ constraints,
576
+ };
577
+ }
578
+
579
+ function colorChannels(hex) {
580
+ return [
581
+ Number.parseInt(hex.slice(1, 3), 16),
582
+ Number.parseInt(hex.slice(3, 5), 16),
583
+ Number.parseInt(hex.slice(5, 7), 16),
584
+ ];
585
+ }
586
+
587
+ function relativeLuminance(hex) {
588
+ const channels = colorChannels(hex).map((channel) => {
589
+ const value = channel / 255;
590
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
591
+ });
592
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
593
+ }
594
+
595
+ function contrastRatio(first, second) {
596
+ const firstLuminance = relativeLuminance(normalizeColor(first, "first"));
597
+ const secondLuminance = relativeLuminance(normalizeColor(second, "second"));
598
+ return (Math.max(firstLuminance, secondLuminance) + 0.05) / (Math.min(firstLuminance, secondLuminance) + 0.05);
599
+ }
600
+
601
+ function normalizeThemeColors(value, fallback, label) {
602
+ if (Array.isArray(value)) {
603
+ value = { background: value[0], foreground: value[1] };
604
+ }
605
+ const source = value && typeof value === "object" ? value : fallback;
606
+ if (!source || typeof source !== "object") {
607
+ throw new TypeError(`${label} palette colors are required.`);
608
+ }
609
+ const background = normalizeColor(source.background, `${label}.background`);
610
+ const foreground = normalizeColor(source.foreground, `${label}.foreground`);
611
+ return Object.freeze({ background, foreground });
612
+ }
613
+
614
+ function normalizeOptionalBoolean(value, defaultValue, label) {
615
+ if (value === undefined) return defaultValue;
616
+ if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean.`);
617
+ return value;
618
+ }
619
+
620
+ function normalizePaletteEntry(input, index, options, allowLowContrast) {
621
+ if (Array.isArray(input)) {
622
+ input = { light: input, dark: input };
623
+ }
624
+ if (!input || typeof input !== "object") {
625
+ throw new TypeError(`palettes[${index}] must be an object or color tuple.`);
626
+ }
627
+
628
+ const id = String(input.id ?? `custom-${index + 1}`);
629
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(id)) {
630
+ throw new TypeError(`Palette id "${id}" must contain 1-64 letters, digits, periods, underscores, or hyphens.`);
631
+ }
632
+
633
+ const common = input.background || input.foreground ? input : undefined;
634
+ const light = normalizeThemeColors(input.light ?? common, undefined, `palettes[${index}].light`);
635
+ const dark = normalizeThemeColors(input.dark ?? input.light ?? common, light, `palettes[${index}].dark`);
636
+ const minimumContrast = Number(options.minimumContrast ?? MIN_CUSTOM_CONTRAST);
637
+ if (!Number.isFinite(minimumContrast) || minimumContrast < 1 || minimumContrast > 21) {
638
+ throw new TypeError("minimumContrast must be a finite number in [1, 21].");
639
+ }
640
+
641
+ if (!allowLowContrast) {
642
+ const lightContrast = contrastRatio(light.background, light.foreground);
643
+ const darkContrast = contrastRatio(dark.background, dark.foreground);
644
+ if (lightContrast < minimumContrast || darkContrast < minimumContrast) {
645
+ throw new RangeError(
646
+ `Palette "${id}" has insufficient contrast: light ${lightContrast.toFixed(2)}:1, dark ${darkContrast.toFixed(2)}:1; required ${minimumContrast.toFixed(2)}:1.`
647
+ );
648
+ }
649
+ }
650
+
651
+ return Object.freeze({
652
+ id,
653
+ light,
654
+ dark,
655
+ visualKey: `${light.background}/${light.foreground}/${dark.background}/${dark.foreground}`,
656
+ });
657
+ }
658
+
659
+ function normalizePaletteCollection(options, allowLowContrast) {
660
+ let palettes;
661
+ let paletteValue = options.palette ?? "auto";
662
+
663
+ if (paletteValue && typeof paletteValue === "object" && !Array.isArray(paletteValue)) {
664
+ palettes = Object.freeze([normalizePaletteEntry(paletteValue, 0, options, allowLowContrast)]);
665
+ paletteValue = 0;
666
+ } else if (options.palettes !== undefined) {
667
+ if (!Array.isArray(options.palettes) || options.palettes.length === 0) {
668
+ throw new TypeError("palettes must be a non-empty array.");
669
+ }
670
+ if (options.palettes.length > MAX_CUSTOM_PALETTES) {
671
+ throw new RangeError(`palettes must contain at most ${MAX_CUSTOM_PALETTES} entries.`);
672
+ }
673
+ palettes = Object.freeze(options.palettes.map((entry, index) => (
674
+ normalizePaletteEntry(entry, index, options, allowLowContrast)
675
+ )));
676
+ } else {
677
+ palettes = BUILTIN_PALETTES;
678
+ }
679
+
680
+ const ids = new Set();
681
+ const visualKeys = new Set();
682
+ for (const item of palettes) {
683
+ if (ids.has(item.id)) throw new TypeError(`Duplicate palette id: ${item.id}.`);
684
+ if (visualKeys.has(item.visualKey)) throw new TypeError(`Duplicate visual palette: ${item.id}.`);
685
+ ids.add(item.id);
686
+ visualKeys.add(item.visualKey);
687
+ }
688
+
689
+ const choices = normalizeChoice(paletteValue, palettes.map((item) => item.id), "palette");
690
+ const paletteBySignatureKey = new Map();
691
+ for (const paletteIndex of choices) {
692
+ const item = palettes[paletteIndex];
693
+ const signatureKey = paletteSignatureKey(item);
694
+ const previous = paletteBySignatureKey.get(signatureKey);
695
+ if (previous && previous.visualKey !== item.visualKey) {
696
+ throw new TypeError(
697
+ `Selected palettes have a signature-key collision (${signatureKey}): ${previous.id} and ${item.id}.`
698
+ );
699
+ }
700
+ paletteBySignatureKey.set(signatureKey, item);
701
+ }
702
+ return { palettes, choices };
703
+ }
704
+
705
+ function normalizeChoice(value, names, label) {
706
+ if (value === undefined || value === null || value === "auto") {
707
+ return Object.freeze(Array.from({ length: names.length }, (_, index) => index));
708
+ }
709
+
710
+ if (typeof value === "number") {
711
+ if (!Number.isInteger(value) || value < 0 || value >= names.length) {
712
+ throw new RangeError(`${label} index must be an integer in [0, ${names.length - 1}].`);
713
+ }
714
+ return Object.freeze([value]);
715
+ }
716
+
717
+ if (typeof value === "string") {
718
+ const index = names.indexOf(value);
719
+ if (index < 0) throw new TypeError(`Unsupported ${label}: ${value}. Expected "auto" or one of: ${names.join(", ")}.`);
720
+ return Object.freeze([index]);
721
+ }
722
+
723
+ throw new TypeError(`${label} must be "auto", a valid name, or a numeric index.`);
724
+ }
725
+
726
+ function normalizeDistinguishability(options) {
727
+ let minimumShapeDistance = options.minimumShapeDistance === undefined ? 0 : options.minimumShapeDistance;
728
+ if (typeof minimumShapeDistance !== "number" || !Number.isInteger(minimumShapeDistance) || minimumShapeDistance < 0 || minimumShapeDistance > 20) {
729
+ throw new TypeError("minimumShapeDistance must be an integer in [0, 20].");
730
+ }
731
+ if (minimumShapeDistance === 0) minimumShapeDistance = 0;
732
+
733
+ let minimumPaletteDistance = options.minimumPaletteDistance === undefined ? 0 : options.minimumPaletteDistance;
734
+ if (typeof minimumPaletteDistance !== "number" || !Number.isFinite(minimumPaletteDistance) || minimumPaletteDistance < 0 || minimumPaletteDistance > 100) {
735
+ throw new TypeError("minimumPaletteDistance must be a finite number in [0, 100].");
736
+ }
737
+ if (minimumPaletteDistance === 0) minimumPaletteDistance = 0;
738
+
739
+ const mode = options.distanceMode === undefined ? "either" : options.distanceMode;
740
+ if (mode !== "either" && mode !== "both") {
741
+ throw new TypeError('distanceMode must be "either" or "both".');
742
+ }
743
+
744
+ if (minimumShapeDistance === 0 && minimumPaletteDistance === 0) return undefined;
745
+ return Object.freeze({
746
+ schema: "visual-distance/v1",
747
+ minimumShapeDistance,
748
+ minimumPaletteDistance,
749
+ mode,
750
+ });
751
+ }
752
+
753
+ function normalizeOptions(options = {}) {
754
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
755
+ throw new TypeError("options must be an object.");
756
+ }
757
+
758
+ const allowLowContrast = normalizeOptionalBoolean(options.allowLowContrast, false, "allowLowContrast");
759
+ const constraints = normalizeConstraints(options);
760
+ const catalog = getCatalog(constraints);
761
+ if (catalog.length === 0) {
762
+ throw new RangeError(`No symmetric 5x4 shape satisfies the requested constraints: ${JSON.stringify({
763
+ ...constraints,
764
+ maxHoles: constraints.maxHoles === Infinity ? "Infinity" : constraints.maxHoles,
765
+ })}`);
766
+ }
767
+
768
+ const seedMode = options.seedMode ?? DEFAULT_SEED_MODE;
769
+ if (seedMode !== "human" && seedMode !== "raw") {
770
+ throw new TypeError(`Unsupported seedMode: ${seedMode}`);
771
+ }
772
+ const namespaceMode = options.namespaceMode ?? "human";
773
+ if (namespaceMode !== "human" && namespaceMode !== "raw") {
774
+ throw new TypeError(`Unsupported namespaceMode: ${namespaceMode}`);
775
+ }
776
+ const namespace = canonicalNamespace(options.namespace ?? DEFAULT_NAMESPACE, namespaceMode);
777
+ const theme = options.theme ?? DEFAULT_THEME;
778
+ if (theme !== "light" && theme !== "dark") {
779
+ throw new TypeError(`Unsupported theme: ${theme}. Expected "light" or "dark".`);
780
+ }
781
+ const collisionNonce = Number(options.collisionNonce ?? 0);
782
+ if (!Number.isSafeInteger(collisionNonce) || collisionNonce < 0) {
783
+ throw new TypeError("collisionNonce must be a non-negative safe integer.");
784
+ }
785
+
786
+ const paletteSelection = normalizePaletteCollection(options, allowLowContrast);
787
+ const stateSpace = catalog.length * paletteSelection.choices.length;
788
+ const distinguishability = normalizeDistinguishability(options);
789
+
790
+ return Object.freeze({
791
+ constraints,
792
+ catalog,
793
+ seedMode,
794
+ namespaceMode,
795
+ namespace,
796
+ theme,
797
+ collisionNonce,
798
+ palettes: paletteSelection.palettes,
799
+ paletteChoices: paletteSelection.choices,
800
+ stateSpace,
801
+ distinguishability,
802
+ });
803
+ }
804
+
805
+ function selectionHash(domain, canonical, normalized, nonce) {
806
+ return hash32String(domainMessage(domain, canonical, normalized.namespace, nonce));
807
+ }
808
+
809
+ function paletteSignatureKey(item) {
810
+ return hash32String(`palette\u0000${item.visualKey}`).toString(16).padStart(8, "0");
811
+ }
812
+
813
+ function visualSignature(shape, selectedPalette) {
814
+ return `${STYLE_VERSION}:${shape.id}:p${paletteSignatureKey(selectedPalette)}`;
815
+ }
816
+
817
+ function createDescriptorFromNormalized(seed, normalized, collisionNonce = normalized.collisionNonce) {
818
+ const canonical = canonicalSeed(seed, normalized.seedMode);
819
+ const shapeIndex = pickIndex(selectionHash("shape", canonical, normalized, collisionNonce), normalized.catalog.length);
820
+ const paletteChoiceIndex = pickIndex(selectionHash("palette", canonical, normalized, collisionNonce), normalized.paletteChoices.length);
821
+ const shape = normalized.catalog[shapeIndex];
822
+ const paletteIndex = normalized.paletteChoices[paletteChoiceIndex];
823
+ const selectedPalette = normalized.palettes[paletteIndex];
824
+ const colors = selectedPalette[normalized.theme];
825
+ const identityMessage = domainMessage("identity-key", canonical, normalized.namespace, 0);
826
+ const identityKey = hashHex128(identityMessage);
827
+ const signature = visualSignature(shape, selectedPalette);
828
+ const rows = shape.rows.slice();
829
+
830
+ return {
831
+ styleVersion: STYLE_VERSION,
832
+ namespace: normalized.namespace,
833
+ canonicalSeed: canonical,
834
+ identityKey,
835
+ collisionNonce,
836
+ hash: selectionHash("identity", canonical, normalized, collisionNonce),
837
+ shapeId: shape.id,
838
+ shapeMask: shape.mask,
839
+ shapeIndex,
840
+ rows,
841
+ grid: rowsToGrid(rows),
842
+ paletteId: selectedPalette.id,
843
+ paletteIndex,
844
+ palette: selectedPalette,
845
+ theme: normalized.theme,
846
+ colors: { ...colors },
847
+ constraints: normalized.constraints,
848
+ stateSpace: normalized.stateSpace,
849
+ signature,
850
+ metrics: {
851
+ cellCount: shape.cellCount,
852
+ density: shape.density,
853
+ bounds: shape.bounds,
854
+ connectedComponents4: shape.connectedComponents4,
855
+ connectedComponents8: shape.connectedComponents8,
856
+ holeCount: shape.holeCount,
857
+ diagonalTouchCount: shape.diagonalTouchCount,
858
+ diagonalTouchCountIgnoringMirror: shape.diagonalTouchCountIgnoringMirror,
859
+ },
860
+ };
861
+ }
862
+
863
+ function createAvatarDescriptor(seed, options = {}) {
864
+ return createDescriptorFromNormalized(seed, normalizeOptions(options));
865
+ }
866
+
867
+ function roundedCell(x, y, size, radius, topLeft, topRight, bottomRight, bottomLeft) {
868
+ return [
869
+ `M${x + (topLeft ? radius : 0)} ${y}`,
870
+ `H${x + size - (topRight ? radius : 0)}`,
871
+ topRight ? `Q${x + size} ${y} ${x + size} ${y + radius}` : `L${x + size} ${y}`,
872
+ `V${y + size - (bottomRight ? radius : 0)}`,
873
+ bottomRight ? `Q${x + size} ${y + size} ${x + size - radius} ${y + size}` : `L${x + size} ${y + size}`,
874
+ `H${x + (bottomLeft ? radius : 0)}`,
875
+ bottomLeft ? `Q${x} ${y + size} ${x} ${y + size - radius}` : `L${x} ${y + size}`,
876
+ `V${y + (topLeft ? radius : 0)}`,
877
+ topLeft ? `Q${x} ${y} ${x + radius} ${y}` : `L${x} ${y}`,
878
+ "Z",
879
+ ].join("");
880
+ }
881
+
882
+ function glyphPath(rows, radius = CELL_RADIUS) {
883
+ let path = "";
884
+ for (let y = 0; y < GRID_H; y++) {
885
+ for (let x = 0; x < GRID_W; x++) {
886
+ if (!cellOn(rows, x, y)) continue;
887
+ const up = cellOn(rows, x, y - 1);
888
+ const down = cellOn(rows, x, y + 1);
889
+ const left = cellOn(rows, x - 1, y);
890
+ const right = cellOn(rows, x + 1, y);
891
+ path += roundedCell(
892
+ GLYPH_X + x * CELL,
893
+ GLYPH_Y + y * CELL,
894
+ CELL,
895
+ radius,
896
+ !up && !left,
897
+ !up && !right,
898
+ !down && !right,
899
+ !down && !left
900
+ );
901
+ }
902
+ }
903
+ return path;
904
+ }
905
+
906
+ function backgroundMarkup(background) {
907
+ return `<circle cx="64" cy="64" r="48" fill="${background}"/>`;
908
+ }
909
+
910
+ function createHashAvatarFromDescriptor(descriptor, size = 96) {
911
+ const snapshot = snapshotRenderableDescriptor(descriptor);
912
+ const safeSize = normalizeSize(size);
913
+ const path = glyphPath(snapshot.rows, CELL_RADIUS);
914
+ const { background, foreground } = snapshot.colors;
915
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="${safeSize}" height="${safeSize}">${backgroundMarkup(background)}<path d="${path}" fill="${foreground}" shape-rendering="geometricPrecision"/></svg>`;
916
+ }
917
+
918
+ function optionsFromArgs(sizeOrOptions, explicitOptions) {
919
+ if (typeof sizeOrOptions === "object" && sizeOrOptions !== null) {
920
+ return {
921
+ size: sizeOrOptions.size ?? 96,
922
+ options: { ...sizeOrOptions },
923
+ };
924
+ }
925
+ return {
926
+ size: sizeOrOptions ?? 96,
927
+ options: { ...(explicitOptions ?? {}) },
928
+ };
929
+ }
930
+
931
+ function createHashAvatar(seed, sizeOrOptions = 96, explicitOptions = {}) {
932
+ const { size, options } = optionsFromArgs(sizeOrOptions, explicitOptions);
933
+ const descriptor = createAvatarDescriptor(seed, options);
934
+ return createHashAvatarFromDescriptor(descriptor, size);
935
+ }
936
+
937
+ function avatarDataUri(seed, sizeOrOptions = 96, explicitOptions = {}) {
938
+ return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(createHashAvatar(seed, sizeOrOptions, explicitOptions))}`;
939
+ }
940
+
941
+ function getCatalogStats(options = {}) {
942
+ const normalized = normalizeOptions(options);
943
+ return {
944
+ styleVersion: STYLE_VERSION,
945
+ rawSymmetricMasks: RAW_SYMMETRIC_MASKS,
946
+ validShapes: normalized.catalog.length,
947
+ availablePalettes: normalized.paletteChoices.length,
948
+ signatureStates: normalized.stateSpace,
949
+ constraints: normalized.constraints,
950
+ };
951
+ }
952
+
953
+ function getAvatarCatalog(options = {}) {
954
+ const normalized = normalizeOptions(options);
955
+ return normalized.catalog.map((shape) => ({
956
+ id: shape.id,
957
+ mask: shape.mask,
958
+ rows: shape.rows.slice(),
959
+ cellCount: shape.cellCount,
960
+ density: shape.density,
961
+ bounds: shape.bounds,
962
+ connectedComponents4: shape.connectedComponents4,
963
+ connectedComponents8: shape.connectedComponents8,
964
+ holeCount: shape.holeCount,
965
+ diagonalTouchCount: shape.diagonalTouchCount,
966
+ diagonalTouchCountIgnoringMirror: shape.diagonalTouchCountIgnoringMirror,
967
+ }));
968
+ }
969
+
970
+ function optionsFingerprint(normalized) {
971
+ const fingerprintOptions = {
972
+ version: STYLE_VERSION,
973
+ seedMode: normalized.seedMode,
974
+ constraints: {
975
+ ...normalized.constraints,
976
+ maxHoles: normalized.constraints.maxHoles === Infinity ? "Infinity" : normalized.constraints.maxHoles,
977
+ },
978
+ palettes: normalized.palettes.map((item) => item.visualKey),
979
+ paletteChoices: normalized.paletteChoices,
980
+ };
981
+ if (normalized.distinguishability) fingerprintOptions.distinguishability = normalized.distinguishability;
982
+ const payload = JSON.stringify(fingerprintOptions);
983
+ return hashHex128(payload);
984
+ }
985
+
986
+ function namespaceFingerprint(namespace) {
987
+ return hashHex128(`${STYLE_VERSION}\u0000namespace\u0000${namespace}`);
988
+ }
989
+
990
+ function manifestPolicyMatches(policy, expected) {
991
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
992
+ const expectedKeys = ["schema", "minimumShapeDistance", "minimumPaletteDistance", "mode"];
993
+ try {
994
+ if (Object.getPrototypeOf(policy) !== Object.prototype) return false;
995
+ const keys = Reflect.ownKeys(policy);
996
+ if (keys.length !== expectedKeys.length || !expectedKeys.every((key) => keys.includes(key))) return false;
997
+ return expectedKeys.every((key) => {
998
+ const descriptor = Object.getOwnPropertyDescriptor(policy, key);
999
+ return descriptor?.enumerable === true
1000
+ && Object.hasOwn(descriptor, "value")
1001
+ && Object.is(descriptor.value, expected[key]);
1002
+ });
1003
+ } catch {
1004
+ return false;
1005
+ }
1006
+ }
1007
+
1008
+ function validateManifest(manifest, normalized) {
1009
+ if (manifest === undefined) return { entries: {} };
1010
+ const requiredKeys = ["schema", "styleVersion", "namespaceKey", "optionsKey", "entries"];
1011
+ const allowedKeys = new Set([...requiredKeys, "distinguishability"]);
1012
+ const snapshot = {};
1013
+ try {
1014
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)
1015
+ || Object.getPrototypeOf(manifest) !== Object.prototype) {
1016
+ throw new TypeError();
1017
+ }
1018
+ const keys = Reflect.ownKeys(manifest);
1019
+ if (keys.some((key) => typeof key !== "string" || !allowedKeys.has(key))
1020
+ || requiredKeys.some((key) => !keys.includes(key))) {
1021
+ throw new TypeError();
1022
+ }
1023
+ for (const key of keys) {
1024
+ const descriptor = Object.getOwnPropertyDescriptor(manifest, key);
1025
+ if (!descriptor?.enumerable || !Object.hasOwn(descriptor, "value")) throw new TypeError();
1026
+ snapshot[key] = descriptor.value;
1027
+ }
1028
+ } catch {
1029
+ throw new TypeError("manifest must be a plain JSON object with the expected enumerable data properties.");
1030
+ }
1031
+
1032
+ if (snapshot.schema !== "deterministic-agent-avatars-manifest/v1") {
1033
+ throw new TypeError("Unsupported manifest schema.");
1034
+ }
1035
+ if (snapshot.styleVersion !== STYLE_VERSION) {
1036
+ throw new TypeError(`Manifest styleVersion must be ${STYLE_VERSION}.`);
1037
+ }
1038
+ if (snapshot.namespaceKey !== namespaceFingerprint(normalized.namespace)) {
1039
+ throw new TypeError("Manifest namespace does not match the requested namespace.");
1040
+ }
1041
+ const hasManifestPolicy = Object.hasOwn(snapshot, "distinguishability");
1042
+ if (hasManifestPolicy !== Boolean(normalized.distinguishability)
1043
+ || (normalized.distinguishability
1044
+ && !manifestPolicyMatches(snapshot.distinguishability, normalized.distinguishability))) {
1045
+ throw new TypeError("Manifest distinguishability policy does not match the requested policy.");
1046
+ }
1047
+ if (snapshot.optionsKey !== optionsFingerprint(normalized)) {
1048
+ throw new TypeError("Manifest options do not match the requested palette, constraints, or seed mode.");
1049
+ }
1050
+ const entries = snapshot.entries;
1051
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
1052
+ throw new TypeError("manifest.entries must be an object.");
1053
+ }
1054
+ return { entries };
1055
+ }
1056
+
1057
+ function invalidManifestEntry(identityKey) {
1058
+ const safeIdentityKey = typeof identityKey === "string" && /^[0-9a-f]{32}$/.test(identityKey)
1059
+ ? identityKey
1060
+ : "<invalid identity key>";
1061
+ return new TypeError(`Invalid manifest entry for ${safeIdentityKey}.`);
1062
+ }
1063
+
1064
+ function hydrateManifestEntries(entries, normalized, ensureUnique) {
1065
+ let identityKeys;
1066
+ try {
1067
+ identityKeys = Reflect.ownKeys(entries);
1068
+ } catch {
1069
+ throw invalidManifestEntry(undefined);
1070
+ }
1071
+ if (identityKeys.length > MAX_MANIFEST_ENTRIES) {
1072
+ throw new RangeError(`manifest.entries must contain at most ${MAX_MANIFEST_ENTRIES} entries.`);
1073
+ }
1074
+ if (ensureUnique && identityKeys.length > normalized.stateSpace) {
1075
+ throw new RangeError(
1076
+ `Manifest contains ${identityKeys.length} entries, but the configured state space contains only ${normalized.stateSpace}.`
1077
+ );
1078
+ }
1079
+ if (identityKeys.some((key) => typeof key !== "string")) throw invalidManifestEntry(undefined);
1080
+
1081
+ const shapeIndexById = new Map(normalized.catalog.map((shape, index) => [shape.id, index]));
1082
+ const selectedPalettes = normalized.paletteChoices.map((paletteIndex) => normalized.palettes[paletteIndex]);
1083
+ const palettePositionById = new Map(selectedPalettes.map((selectedPalette, position) => [selectedPalette.id, position]));
1084
+ const manifestEntries = {};
1085
+ const resolvedEntries = [];
1086
+ const usedSignatures = new Map();
1087
+
1088
+ for (const identityKey of identityKeys) {
1089
+ if (!/^[0-9a-f]{32}$/.test(identityKey)) throw invalidManifestEntry(identityKey);
1090
+ let entryDescriptor;
1091
+ try {
1092
+ entryDescriptor = Object.getOwnPropertyDescriptor(entries, identityKey);
1093
+ } catch {
1094
+ throw invalidManifestEntry(identityKey);
1095
+ }
1096
+ if (!entryDescriptor || !Object.hasOwn(entryDescriptor, "value")) throw invalidManifestEntry(identityKey);
1097
+ const entry = entryDescriptor.value;
1098
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw invalidManifestEntry(identityKey);
1099
+
1100
+ const values = {};
1101
+ for (const field of ["nonce", "signature", "shapeId", "paletteId"]) {
1102
+ let descriptor;
1103
+ try {
1104
+ descriptor = Object.getOwnPropertyDescriptor(entry, field);
1105
+ } catch {
1106
+ throw invalidManifestEntry(identityKey);
1107
+ }
1108
+ if (!descriptor || !Object.hasOwn(descriptor, "value")) throw invalidManifestEntry(identityKey);
1109
+ values[field] = descriptor.value;
1110
+ }
1111
+ if (!Number.isSafeInteger(values.nonce) || values.nonce < 0
1112
+ || typeof values.signature !== "string"
1113
+ || typeof values.shapeId !== "string"
1114
+ || typeof values.paletteId !== "string") {
1115
+ throw invalidManifestEntry(identityKey);
1116
+ }
1117
+
1118
+ const shapeIndex = shapeIndexById.get(values.shapeId);
1119
+ const palettePosition = palettePositionById.get(values.paletteId);
1120
+ if (shapeIndex === undefined || palettePosition === undefined) throw invalidManifestEntry(identityKey);
1121
+ const shape = normalized.catalog[shapeIndex];
1122
+ const selectedPalette = selectedPalettes[palettePosition];
1123
+ if (values.signature !== visualSignature(shape, selectedPalette)) throw invalidManifestEntry(identityKey);
1124
+
1125
+ const previous = usedSignatures.get(values.signature);
1126
+ if (ensureUnique && previous && previous !== identityKey) {
1127
+ throw new Error(`Manifest contains a duplicate visual signature for ${previous} and ${identityKey}.`);
1128
+ }
1129
+ usedSignatures.set(values.signature, identityKey);
1130
+ const copiedEntry = {
1131
+ nonce: values.nonce,
1132
+ signature: values.signature,
1133
+ shapeId: values.shapeId,
1134
+ paletteId: values.paletteId,
1135
+ };
1136
+ manifestEntries[identityKey] = copiedEntry;
1137
+ resolvedEntries.push({ identityKey, entry: copiedEntry, shapeIndex, palettePosition });
1138
+ }
1139
+
1140
+ return { manifestEntries, resolvedEntries, usedSignatures };
1141
+ }
1142
+
1143
+ function cachedNearShapes(catalog, minimumDistance) {
1144
+ let cache = SHAPE_NEIGHBOR_CACHES.get(catalog);
1145
+ if (!cache) {
1146
+ cache = createBoundedLruCache(4);
1147
+ SHAPE_NEIGHBOR_CACHES.set(catalog, cache);
1148
+ }
1149
+ const cached = cache.get(minimumDistance);
1150
+ if (cached) return cached;
1151
+ const nearShapes = Object.freeze(catalog.map((left) => {
1152
+ const near = [];
1153
+ for (let right = 0; right < catalog.length; right++) {
1154
+ if (shapeHammingDistance(left.rows, catalog[right].rows) < minimumDistance) near.push(right);
1155
+ }
1156
+ return Object.freeze(near);
1157
+ }));
1158
+ return cache.set(minimumDistance, nearShapes);
1159
+ }
1160
+
1161
+ function cachedNearPalettes(palettes, choices, minimumDistance) {
1162
+ let cache = PALETTE_NEIGHBOR_CACHES.get(palettes);
1163
+ if (!cache) {
1164
+ cache = createBoundedLruCache(16);
1165
+ PALETTE_NEIGHBOR_CACHES.set(palettes, cache);
1166
+ }
1167
+ const key = `${minimumDistance}|${choices.join(",")}`;
1168
+ const cached = cache.get(key);
1169
+ if (cached) return cached;
1170
+ const selectedPalettes = choices.map((paletteIndex) => palettes[paletteIndex]);
1171
+ const paletteCount = selectedPalettes.length;
1172
+ const paletteDistances = buildPaletteDistanceMatrix(selectedPalettes);
1173
+ const nearPalettes = Object.freeze(selectedPalettes.map((_, left) => {
1174
+ const near = [];
1175
+ for (let right = 0; right < paletteCount; right++) {
1176
+ if (paletteDistances[left * paletteCount + right] < minimumDistance) near.push(right);
1177
+ }
1178
+ return Object.freeze(near);
1179
+ }));
1180
+ return cache.set(key, nearPalettes);
1181
+ }
1182
+
1183
+ function createDistanceAllocator(normalized, resolvedEntries) {
1184
+ const policy = normalized.distinguishability;
1185
+ const paletteCount = normalized.paletteChoices.length;
1186
+ const shapeCount = normalized.catalog.length;
1187
+ const shapeEnabled = policy.minimumShapeDistance > 0;
1188
+ const paletteEnabled = policy.minimumPaletteDistance > 0;
1189
+ const palettePositionByIndex = new Map(normalized.paletteChoices.map((paletteIndex, position) => [paletteIndex, position]));
1190
+ const nearShapes = shapeEnabled
1191
+ ? cachedNearShapes(normalized.catalog, policy.minimumShapeDistance)
1192
+ : undefined;
1193
+ const nearPalettes = paletteEnabled
1194
+ ? cachedNearPalettes(normalized.palettes, normalized.paletteChoices, policy.minimumPaletteDistance)
1195
+ : undefined;
1196
+ const blockedStates = new Uint8Array(normalized.stateSpace);
1197
+
1198
+ function stateIndex(shapeIndex, palettePosition) {
1199
+ return shapeIndex * paletteCount + palettePosition;
1200
+ }
1201
+
1202
+ function blockShapeBand(shapeIndex) {
1203
+ for (const nearShape of nearShapes[shapeIndex]) {
1204
+ const offset = nearShape * paletteCount;
1205
+ blockedStates.fill(1, offset, offset + paletteCount);
1206
+ }
1207
+ }
1208
+
1209
+ function blockPaletteBand(palettePosition) {
1210
+ for (let shapeIndex = 0; shapeIndex < shapeCount; shapeIndex++) {
1211
+ for (const nearPalette of nearPalettes[palettePosition]) {
1212
+ blockedStates[stateIndex(shapeIndex, nearPalette)] = 1;
1213
+ }
1214
+ }
1215
+ }
1216
+
1217
+ function accept(shapeIndex, palettePosition) {
1218
+ if (shapeEnabled && !paletteEnabled) {
1219
+ blockShapeBand(shapeIndex);
1220
+ } else if (!shapeEnabled && paletteEnabled) {
1221
+ blockPaletteBand(palettePosition);
1222
+ } else if (policy.mode === "either") {
1223
+ for (const nearShape of nearShapes[shapeIndex]) {
1224
+ for (const nearPalette of nearPalettes[palettePosition]) {
1225
+ blockedStates[stateIndex(nearShape, nearPalette)] = 1;
1226
+ }
1227
+ }
1228
+ } else {
1229
+ blockShapeBand(shapeIndex);
1230
+ blockPaletteBand(palettePosition);
1231
+ }
1232
+ }
1233
+
1234
+ for (const { identityKey, shapeIndex, palettePosition } of resolvedEntries) {
1235
+ if (blockedStates[stateIndex(shapeIndex, palettePosition)] !== 0) {
1236
+ throw new Error(`Manifest contains an invalid visual-distance assignment for identity ${identityKey}.`);
1237
+ }
1238
+ accept(shapeIndex, palettePosition);
1239
+ }
1240
+
1241
+ return {
1242
+ isBlocked(descriptor) {
1243
+ const palettePosition = palettePositionByIndex.get(descriptor.paletteIndex);
1244
+ return blockedStates[stateIndex(descriptor.shapeIndex, palettePosition)] !== 0;
1245
+ },
1246
+ accept(descriptor) {
1247
+ accept(descriptor.shapeIndex, palettePositionByIndex.get(descriptor.paletteIndex));
1248
+ },
1249
+ };
1250
+ }
1251
+
1252
+ function createIdentitySet(seeds, options = {}) {
1253
+ if (!Array.isArray(seeds)) throw new TypeError("seeds must be an array.");
1254
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
1255
+ throw new TypeError("options must be an object.");
1256
+ }
1257
+ if (seeds.length > MAX_IDENTITY_SET_ITEMS) {
1258
+ throw new RangeError(`seeds must contain at most ${MAX_IDENTITY_SET_ITEMS} entries.`);
1259
+ }
1260
+ const includeSvg = normalizeOptionalBoolean(options.includeSvg, true, "includeSvg");
1261
+ const ensureUnique = normalizeOptionalBoolean(options.ensureUnique, true, "ensureUnique");
1262
+ if (options.collisionNonce !== undefined) {
1263
+ throw new TypeError("collisionNonce is not supported by createIdentitySet; the allocator manages per-identity nonces.");
1264
+ }
1265
+ const normalized = normalizeOptions(options);
1266
+ if (normalized.distinguishability && !ensureUnique) {
1267
+ throw new TypeError("ensureUnique must not be false when a distinguishability policy is enabled.");
1268
+ }
1269
+ const size = options.size ?? 96;
1270
+ const suppliedManifest = validateManifest(options.manifest, normalized);
1271
+ const hydratedManifest = hydrateManifestEntries(suppliedManifest.entries, normalized, ensureUnique);
1272
+ const manifestEntries = hydratedManifest.manifestEntries;
1273
+ const optionsKey = optionsFingerprint(normalized);
1274
+ const namespaceKey = namespaceFingerprint(normalized.namespace);
1275
+ const usedSignatures = hydratedManifest.usedSignatures;
1276
+
1277
+ const records = seeds.map((input, index) => {
1278
+ const canonical = canonicalSeed(input, normalized.seedMode);
1279
+ const identityKey = hashHex128(domainMessage("identity-key", canonical, normalized.namespace, 0));
1280
+ return { input, index, canonical, identityKey };
1281
+ });
1282
+
1283
+ const uniqueRecords = new Map();
1284
+ for (const record of records) {
1285
+ const existing = uniqueRecords.get(record.identityKey);
1286
+ if (existing && existing.canonical !== record.canonical) {
1287
+ throw new Error("A 128-bit identity-key collision occurred. Use private HMAC-derived seeds or a different namespace.");
1288
+ }
1289
+ if (!existing) uniqueRecords.set(record.identityKey, record);
1290
+ }
1291
+
1292
+ const unassignedCount = Array.from(uniqueRecords.values()).filter((record) => !manifestEntries[record.identityKey]).length;
1293
+ if (ensureUnique && usedSignatures.size + unassignedCount > normalized.stateSpace) {
1294
+ throw new RangeError(`The requested set needs ${usedSignatures.size + unassignedCount} unique signatures, but the configured state space contains only ${normalized.stateSpace}.`);
1295
+ }
1296
+
1297
+ const assigned = new Map();
1298
+ const sortedUnique = Array.from(uniqueRecords.values()).sort((a, b) => a.identityKey.localeCompare(b.identityKey));
1299
+ const maxAttempts = options.maxAttempts === undefined
1300
+ ? Math.min(Math.max(4096, sortedUnique.length * 128), Math.max(4096, normalized.stateSpace * 2))
1301
+ : Number(options.maxAttempts);
1302
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
1303
+ throw new TypeError("maxAttempts must be a positive safe integer.");
1304
+ }
1305
+ const distanceAllocator = normalized.distinguishability
1306
+ ? createDistanceAllocator(normalized, hydratedManifest.resolvedEntries)
1307
+ : undefined;
1308
+
1309
+ for (const record of sortedUnique) {
1310
+ const existingEntry = manifestEntries[record.identityKey];
1311
+ if (existingEntry) {
1312
+ const descriptor = createDescriptorFromNormalized(record.input, normalized, existingEntry.nonce);
1313
+ if (descriptor.signature !== existingEntry.signature
1314
+ || descriptor.shapeId !== existingEntry.shapeId
1315
+ || descriptor.paletteId !== existingEntry.paletteId) {
1316
+ throw new Error(`Manifest entry for ${record.identityKey} no longer resolves to its stored signature.`);
1317
+ }
1318
+ assigned.set(record.identityKey, descriptor);
1319
+ continue;
1320
+ }
1321
+
1322
+ let descriptor;
1323
+ for (let nonce = 0; nonce < maxAttempts; nonce++) {
1324
+ const candidate = createDescriptorFromNormalized(record.input, normalized, nonce);
1325
+ if ((!ensureUnique || !usedSignatures.has(candidate.signature)) && !distanceAllocator?.isBlocked(candidate)) {
1326
+ descriptor = candidate;
1327
+ break;
1328
+ }
1329
+ }
1330
+ if (!descriptor) {
1331
+ if (normalized.distinguishability) {
1332
+ const policy = normalized.distinguishability;
1333
+ throw new Error(
1334
+ `deterministic allocation attempts exhausted for identity ${record.identityKey}; `
1335
+ + `accepted unique count ${usedSignatures.size}; thresholds shape=${policy.minimumShapeDistance}, `
1336
+ + `palette=${policy.minimumPaletteDistance}, mode=${policy.mode}; attempts=${maxAttempts}/${maxAttempts}. `
1337
+ + "Try lower thresholds, add palettes, or increase maxAttempts."
1338
+ );
1339
+ }
1340
+ throw new Error(`Unable to allocate a unique avatar for identity ${record.identityKey} within ${maxAttempts} deterministic attempts.`);
1341
+ }
1342
+
1343
+ usedSignatures.set(descriptor.signature, record.identityKey);
1344
+ distanceAllocator?.accept(descriptor);
1345
+ manifestEntries[record.identityKey] = {
1346
+ nonce: descriptor.collisionNonce,
1347
+ signature: descriptor.signature,
1348
+ shapeId: descriptor.shapeId,
1349
+ paletteId: descriptor.paletteId,
1350
+ };
1351
+ assigned.set(record.identityKey, descriptor);
1352
+ }
1353
+
1354
+ const items = records.map((record) => {
1355
+ const descriptor = assigned.get(record.identityKey);
1356
+ return {
1357
+ input: record.input,
1358
+ identityKey: record.identityKey,
1359
+ nonce: descriptor.collisionNonce,
1360
+ signature: descriptor.signature,
1361
+ descriptor,
1362
+ ...(includeSvg ? { svg: createHashAvatarFromDescriptor(descriptor, size) } : {}),
1363
+ };
1364
+ });
1365
+
1366
+ const sortedEntries = {};
1367
+ for (const identityKey of Object.keys(manifestEntries).sort()) sortedEntries[identityKey] = manifestEntries[identityKey];
1368
+ const manifest = {
1369
+ schema: "deterministic-agent-avatars-manifest/v1",
1370
+ styleVersion: STYLE_VERSION,
1371
+ namespaceKey,
1372
+ optionsKey,
1373
+ ...(normalized.distinguishability ? { distinguishability: normalized.distinguishability } : {}),
1374
+ entries: sortedEntries,
1375
+ };
1376
+
1377
+ return { items, manifest, stateSpace: normalized.stateSpace };
1378
+ }
1379
+
1380
+ module.exports = {
1381
+ STYLE_VERSION,
1382
+ GRID_W,
1383
+ GRID_H,
1384
+ RAW_SYMMETRIC_MASKS,
1385
+ BUILTIN_PALETTES,
1386
+ canonicalSeed,
1387
+ hash32,
1388
+ contrastRatio,
1389
+ rowsFromSymmetricMask,
1390
+ symmetricMaskFromRows,
1391
+ getAvatarCatalog,
1392
+ getCatalogStats,
1393
+ validateAvatarBitmap,
1394
+ createAvatarDescriptor,
1395
+ createHashAvatarFromDescriptor,
1396
+ createHashAvatar,
1397
+ avatarDataUri,
1398
+ createIdentitySet,
1399
+ };