@polycode-projects/the-mechanical-code-talker 6.0.17 → 6.0.19

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.
@@ -206,6 +206,87 @@ function sortFactIndividualsById(individuals) {
206
206
  for (let i = 0; i < slots.length; i += 1) individuals[slots[i]] = facts[i];
207
207
  }
208
208
 
209
+ /** The prose tokens, forward supersession list and backward-pointer presence one
210
+ * individual carries, read in a single pass over its attributes. First match
211
+ * per field, which is what the `find`-based readers elsewhere in this file
212
+ * take. */
213
+ function derivationInputsOf(ind) {
214
+ let proseTokens = "";
215
+ let supersedes = "";
216
+ let carriesSupersededBy = false;
217
+ let seenProseTokens = false;
218
+ let seenSupersedes = false;
219
+ for (const a of ind?.attributes || []) {
220
+ if (!seenProseTokens && a?.key === "prose_tokens") { proseTokens = a.value || ""; seenProseTokens = true; }
221
+ if (!seenSupersedes && a?.prop === SUPERSEDES_PROP) { supersedes = a.value || ""; seenSupersedes = true; }
222
+ if (a?.prop === SUPERSEDED_BY_PROP) carriesSupersededBy = true;
223
+ }
224
+ return { proseTokens, supersedes, carriesSupersededBy };
225
+ }
226
+
227
+ // What a payload carries forward so the next write reconciles its derived
228
+ // structures instead of building them again: what each individual last put into
229
+ // the prose index, what each record last named as superseded, and the reverse of
230
+ // that. Non-enumerable and symbol-keyed, so no copy of the payload inherits
231
+ // state that describes a different array of individuals, and a payload without
232
+ // it (a fresh assembly, a clone, a hand-built fixture) derives from scratch.
233
+ const CARRIED_DERIVATIONS = Symbol("tmct.carriedDerivations");
234
+
235
+ function carryForward(payload, carried) {
236
+ carried.index = payload.proseIndex;
237
+ Object.defineProperty(payload, CARRIED_DERIVATIONS, {
238
+ value: carried, writable: true, configurable: true, enumerable: false,
239
+ });
240
+ }
241
+
242
+ /** What this payload can reconcile against, or null when it must derive
243
+ * instead. `index` identity is the guard: state that describes some other
244
+ * prose index cannot be applied to this one. `needSupersessions` narrows to
245
+ * the callers that maintain the backward pointers too. */
246
+ function carriedDerivationsOf(payload, { needSupersessions } = {}) {
247
+ const carried = payload[CARRIED_DERIVATIONS];
248
+ if (!carried || carried.index !== payload.proseIndex || !payload.proseIndex) return null;
249
+ if (needSupersessions && !carried.successorsById) return null;
250
+ return carried;
251
+ }
252
+
253
+ /** Where `id` sits in a sorted posting list, and whether it is there at all. */
254
+ function postingSlot(list, id) {
255
+ let low = 0;
256
+ let high = list.length;
257
+ while (low < high) {
258
+ const mid = (low + high) >> 1;
259
+ if (list[mid] < id) low = mid + 1;
260
+ else high = mid;
261
+ }
262
+ return low;
263
+ }
264
+
265
+ /** Move one individual's contribution to the prose index from `before` to
266
+ * `after`, in place.
267
+ *
268
+ * `buildProseIndex` produces, per word, the multiset of ids that named it —
269
+ * one entry per (individual, occurrence of the word in its token string) —
270
+ * sorted, with a word nobody names absent altogether. Dropping one entry per
271
+ * old occurrence and inserting one per new occurrence lands on exactly that
272
+ * multiset, and inserting in sorted position keeps exactly that order, so a
273
+ * reconciled index and a rebuilt one are the same object graph. */
274
+ function moveProseTokens(index, id, before, after) {
275
+ if (before === after) return;
276
+ for (const word of before ? before.split(" ") : []) {
277
+ const list = index[word];
278
+ if (!list) continue;
279
+ const at = postingSlot(list, id);
280
+ if (list[at] !== id) continue;
281
+ list.splice(at, 1);
282
+ if (!list.length) delete index[word];
283
+ }
284
+ for (const word of after ? after.split(" ") : []) {
285
+ const list = index[word] || (index[word] = []);
286
+ list.splice(postingSlot(list, id), 0, id);
287
+ }
288
+ }
289
+
209
290
  /** Re-derive each record's backward supersession pointer from the union of the
210
291
  * forward ones. Two turns that superseded the same record concurrently both
211
292
  * land, because each wrote its own row and neither touched the record they
@@ -215,36 +296,143 @@ function sortFactIndividualsById(individuals) {
215
296
  * used to carry. Rows never carry one (`storedIndividualForm` strips it), so
216
297
  * that arm is dead when this runs over a fresh projection and live when it
217
298
  * runs again over individuals it already derived. */
218
- function applyDerivedSupersessions(individuals) {
299
+ function writeSupersededBy(ind, successors) {
300
+ const carried = (ind?.attributes || []).some((a) => a?.prop === SUPERSEDED_BY_PROP);
301
+ if (!successors && !carried) return;
302
+ const rest = (ind.attributes || []).filter((a) => a?.prop !== SUPERSEDED_BY_PROP);
303
+ ind.attributes = successors
304
+ ? [...rest, { prop: SUPERSEDED_BY_PROP, key: "supersededBy", value: [...successors].sort().join(" ") }]
305
+ : rest;
306
+ }
307
+
308
+ /** The forward supersession list one individual states, as the derivation reads
309
+ * it: a Fact's own `mgx:supersedes` value, and nothing at all from anything
310
+ * else, which is the same narrowing the from-scratch pass applies. */
311
+ const supersedesStatedBy = (ind, supersedes) => (ind?.class === FACT_CLASS ? supersedes : "");
312
+
313
+ /** Both derived structures over one payload, built from nothing and recorded so
314
+ * the next write can reconcile rather than repeat this. */
315
+ function deriveProseAndSupersessions(payload, individuals) {
219
316
  const successorsById = new Map();
317
+ const tokensById = new Map();
318
+ const supersedesById = new Map();
220
319
  for (const ind of individuals) {
221
- if (ind?.class !== FACT_CLASS) continue;
222
- for (const replaced of attrValue(ind, SUPERSEDES_PROP).split(" ").filter(Boolean)) {
320
+ const { proseTokens, supersedes } = derivationInputsOf(ind);
321
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
322
+ const stated = supersedesStatedBy(ind, supersedes);
323
+ if (!stated) continue;
324
+ supersedesById.set(ind.id, stated);
325
+ for (const replaced of stated.split(" ").filter(Boolean)) {
223
326
  const successors = successorsById.get(replaced) || new Set();
224
327
  successors.add(ind.id);
225
328
  successorsById.set(replaced, successors);
226
329
  }
227
330
  }
331
+ for (const ind of individuals) writeSupersededBy(ind, successorsById.get(ind?.id));
332
+ // Released before the replacement is built, not after: over a seed-sized
333
+ // store the prose index is the largest derived structure here, and holding
334
+ // the outgoing one while the incoming one grows doubles it for no reason.
335
+ payload.proseIndex = null;
336
+ payload.proseIndex = buildProseIndex(individuals);
337
+ carryForward(payload, { tokensById, supersedesById, successorsById });
338
+ }
339
+
340
+ /** Both derived structures brought up to date from the ones this payload
341
+ * already carries. One pass over the individuals reads what each contributes
342
+ * now; only what disagrees with what it contributed last time is applied.
343
+ *
344
+ * A write touches a handful of records out of a seed's worth, so this pays for
345
+ * the walk and the handful, where the from-scratch build pays for every token
346
+ * in the store and sorts every posting list it produced. */
347
+ function reconcileProseAndSupersessions(payload, individuals, carried) {
348
+ const { tokensById, supersedesById, successorsById } = carried;
349
+ const index = payload.proseIndex;
350
+ const resettled = new Set();
351
+ const carriesPointer = new Set();
352
+ let tokenBearers = 0;
353
+ let supersedingRecords = 0;
354
+
355
+ const forgetSupersedes = (id, replaced) => {
356
+ const successors = successorsById.get(replaced);
357
+ if (!successors?.delete(id)) return;
358
+ if (!successors.size) successorsById.delete(replaced);
359
+ resettled.add(replaced);
360
+ };
361
+ const recordSupersedes = (id, replaced) => {
362
+ const successors = successorsById.get(replaced) || new Set();
363
+ if (successors.has(id)) return;
364
+ successors.add(id);
365
+ successorsById.set(replaced, successors);
366
+ resettled.add(replaced);
367
+ };
368
+
228
369
  for (const ind of individuals) {
229
- const successors = successorsById.get(ind?.id);
230
- const carried = (ind?.attributes || []).some((a) => a?.prop === SUPERSEDED_BY_PROP);
231
- if (!successors && !carried) continue;
232
- const rest = (ind.attributes || []).filter((a) => a?.prop !== SUPERSEDED_BY_PROP);
233
- ind.attributes = successors
234
- ? [...rest, { prop: SUPERSEDED_BY_PROP, key: "supersededBy", value: [...successors].sort().join(" ") }]
235
- : rest;
370
+ const id = ind?.id;
371
+ const { proseTokens, supersedes, carriesSupersededBy } = derivationInputsOf(ind);
372
+ if (carriesSupersededBy) carriesPointer.add(id);
373
+
374
+ if (proseTokens) tokenBearers += 1;
375
+ const wasTokens = tokensById.get(id) || "";
376
+ if (proseTokens !== wasTokens) {
377
+ moveProseTokens(index, id, wasTokens, proseTokens);
378
+ if (proseTokens) tokensById.set(id, proseTokens);
379
+ else tokensById.delete(id);
380
+ }
381
+
382
+ const stated = supersedesStatedBy(ind, supersedes);
383
+ if (stated) supersedingRecords += 1;
384
+ const wasStated = supersedesById.get(id) || "";
385
+ if (stated !== wasStated) {
386
+ for (const replaced of wasStated.split(" ").filter(Boolean)) forgetSupersedes(id, replaced);
387
+ for (const replaced of stated.split(" ").filter(Boolean)) recordSupersedes(id, replaced);
388
+ if (stated) supersedesById.set(id, stated);
389
+ else supersedesById.delete(id);
390
+ }
391
+ }
392
+
393
+ // A caller that only added or rewrote individuals leaves both maps holding
394
+ // exactly what the walk above just saw, and the counts say so. They disagree
395
+ // only when an individual left the payload, which costs one more walk to
396
+ // find and is the rarer write by far.
397
+ if (tokensById.size !== tokenBearers || supersedesById.size !== supersedingRecords) {
398
+ const present = new Set(individuals.map((ind) => ind?.id));
399
+ for (const [id, tokens] of tokensById) {
400
+ if (present.has(id)) continue;
401
+ moveProseTokens(index, id, tokens, "");
402
+ tokensById.delete(id);
403
+ }
404
+ for (const [id, stated] of supersedesById) {
405
+ if (present.has(id)) continue;
406
+ for (const replaced of stated.split(" ").filter(Boolean)) forgetSupersedes(id, replaced);
407
+ supersedesById.delete(id);
408
+ }
409
+ }
410
+
411
+ // Exactly the records the from-scratch pass rewrites: the ones with
412
+ // successors and the ones already carrying a pointer. Both sets are bounded
413
+ // by how many supersessions the store holds, which is a handful beside its
414
+ // individuals, so this rewrites the same records for the same cost and leaves
415
+ // the rest of the store alone.
416
+ for (const id of carriesPointer) resettled.add(id);
417
+ for (const id of successorsById.keys()) resettled.add(id);
418
+ if (!resettled.size) return;
419
+ for (const ind of individuals) {
420
+ if (resettled.has(ind?.id)) writeSupersededBy(ind, successorsById.get(ind?.id));
236
421
  }
237
422
  }
238
423
 
239
424
  /** Recount `classes[]` from the assembled individuals, the same count-and-
240
425
  * sample shape the store keeps. */
241
426
  function recountedClasses(individuals) {
242
- const classes = [];
243
- for (const name of [MEMORY_SESSION_CLASS, UTTERANCE_CLASS, FACT_CLASS, SOURCE_CLASS, RULE_CLASS]) {
244
- const of = individuals.filter((i) => i?.class === name);
245
- if (of.length) classes.push({ name, count: of.length, sample: of.slice(0, 3).map((i) => i.label) });
427
+ const order = [MEMORY_SESSION_CLASS, UTTERANCE_CLASS, FACT_CLASS, SOURCE_CLASS, RULE_CLASS];
428
+ const counted = new Map(order.map((name) => [name, { name, count: 0, sample: [] }]));
429
+ for (const ind of individuals) {
430
+ const row = counted.get(ind?.class);
431
+ if (!row) continue;
432
+ row.count += 1;
433
+ if (row.sample.length < 3) row.sample.push(ind.label);
246
434
  }
247
- return classes;
435
+ return order.map((name) => counted.get(name)).filter((row) => row.count);
248
436
  }
249
437
 
250
438
  /** The store's `generated_at`: the latest utterance timestamp it holds, which
@@ -296,17 +484,61 @@ export function rowsToPayload(rows, { meta = null } = {}) {
296
484
  export function renormalizeAssembledPayload(payload) {
297
485
  const individuals = payload.individuals || [];
298
486
  sortFactIndividualsById(individuals);
299
- applyDerivedSupersessions(individuals);
487
+ const carried = carriedDerivationsOf(payload, { needSupersessions: true });
488
+ if (carried) reconcileProseAndSupersessions(payload, individuals, carried);
489
+ else deriveProseAndSupersessions(payload, individuals);
300
490
  payload.classes = recountedClasses(individuals);
301
- // Released before the replacement is built, not after: over a seed-sized
302
- // store the prose index is the largest derived structure here, and holding
303
- // the outgoing one while the incoming one grows doubles it for no reason.
304
- payload.proseIndex = null;
305
- payload.proseIndex = buildProseIndex(individuals);
306
491
  payload.generated_at = latestUtteranceTimestamp(individuals);
307
492
  return payload;
308
493
  }
309
494
 
495
+ /** Only the prose index, reconciled the same way — for a caller that derives
496
+ * everything else itself and whose payload is not a row assembly
497
+ * (`mutateMemory` over a non-row backend). A payload with nothing to reconcile
498
+ * against gets the full build, and carries the state on so the next write
499
+ * reconciles. Mutates and returns `payload`. */
500
+ export function renormalizeProseIndex(payload) {
501
+ const individuals = payload.individuals || [];
502
+ const carried = carriedDerivationsOf(payload);
503
+ if (!carried) {
504
+ const tokensById = new Map();
505
+ for (const ind of individuals) {
506
+ const { proseTokens } = derivationInputsOf(ind);
507
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
508
+ }
509
+ payload.proseIndex = null;
510
+ payload.proseIndex = buildProseIndex(individuals);
511
+ carryForward(payload, { tokensById, supersedesById: null, successorsById: null });
512
+ return payload;
513
+ }
514
+ const { tokensById } = carried;
515
+ // This pass maintains the index and nothing else, so any supersession state
516
+ // beside it stops describing the individuals it claims to and goes now,
517
+ // rather than being reconciled against later.
518
+ carried.supersedesById = null;
519
+ carried.successorsById = null;
520
+ const index = payload.proseIndex;
521
+ let tokenBearers = 0;
522
+ for (const ind of individuals) {
523
+ const { proseTokens } = derivationInputsOf(ind);
524
+ if (proseTokens) tokenBearers += 1;
525
+ const wasTokens = tokensById.get(ind?.id) || "";
526
+ if (proseTokens === wasTokens) continue;
527
+ moveProseTokens(index, ind?.id, wasTokens, proseTokens);
528
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
529
+ else tokensById.delete(ind?.id);
530
+ }
531
+ if (tokensById.size !== tokenBearers) {
532
+ const present = new Set(individuals.map((ind) => ind?.id));
533
+ for (const [id, tokens] of tokensById) {
534
+ if (present.has(id)) continue;
535
+ moveProseTokens(index, id, tokens, "");
536
+ tokensById.delete(id);
537
+ }
538
+ }
539
+ return payload;
540
+ }
541
+
310
542
  /** The rows to write and the row keys to delete to turn `before` into `after`.
311
543
  * A row whose stored bytes did not change is not in either list, so a store
312
544
  * pays only for what actually moved. */