@polycode-projects/the-mechanical-code-talker 6.0.18 → 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.
- package/package.json +3 -1
- package/src/adapters/corpus/research-source.mjs +6 -2
- package/src/adapters/corpus/wikidata-live.mjs +92 -51
- package/src/adapters/memory/core.mjs +53 -5
- package/src/adapters/memory/rows.mjs +253 -21
- package/src/domain/news-feed.mjs +472 -68
- package/src/domain/sense-gate.mjs +220 -0
- package/src/domain/syllogise.mjs +39 -8
- package/src/domain/term-ledger.mjs +16 -1
- package/src/services/chat.mjs +17 -12
- package/src/services/extract-facts.mjs +284 -19
- package/src/services/news.mjs +51 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +142 -142
|
@@ -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
|
|
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
|
-
|
|
222
|
-
|
|
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
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
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. */
|