@polycode-projects/the-mechanical-code-talker 6.0.18 → 6.0.20

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.
Files changed (76) hide show
  1. package/README.md +20 -23
  2. package/bin/tmct.mjs +16 -33
  3. package/corpus/LICENSES.json +0 -21
  4. package/corpus/README.md +10 -13
  5. package/corpus/reference/manifest.json +19 -19
  6. package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
  7. package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
  8. package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
  9. package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
  10. package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
  11. package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
  12. package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
  13. package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
  14. package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
  15. package/corpus/tier2/generate.mjs +6 -142
  16. package/corpus/tier2/manifest.json +0 -42
  17. package/package.json +6 -4
  18. package/src/adapters/corpus/child-seed.mjs +74 -0
  19. package/src/adapters/corpus/conceptnet.mjs +45 -26
  20. package/src/adapters/corpus/research-source.mjs +6 -2
  21. package/src/adapters/corpus/wikidata-live.mjs +92 -51
  22. package/src/adapters/memory/blocks.mjs +7 -1
  23. package/src/adapters/memory/core.mjs +505 -107
  24. package/src/adapters/memory/corpus-bands.mjs +27 -10
  25. package/src/adapters/memory/inspect.mjs +24 -5
  26. package/src/adapters/memory/rows.mjs +359 -30
  27. package/src/adapters/memory/shacl.mjs +10 -3
  28. package/src/domain/ask.mjs +27 -10
  29. package/src/domain/cli-verbs.mjs +3 -4
  30. package/src/domain/completions/group.mjs +8 -3
  31. package/src/domain/completions/infer.mjs +7 -2
  32. package/src/domain/completions/prune.mjs +5 -1
  33. package/src/domain/completions/rank.mjs +7 -2
  34. package/src/domain/digest/compose.mjs +5 -1
  35. package/src/domain/digest/select.mjs +12 -6
  36. package/src/domain/domain.mjs +15 -8
  37. package/src/domain/el-classify.mjs +11 -2
  38. package/src/domain/fact-phrase.mjs +86 -4
  39. package/src/domain/hash.mjs +9 -0
  40. package/src/domain/memory/bias.mjs +8 -4
  41. package/src/domain/memory/capability.mjs +12 -6
  42. package/src/domain/memory/fact-order.mjs +29 -0
  43. package/src/domain/memory/resolution.mjs +3 -0
  44. package/src/domain/news-feed.mjs +862 -92
  45. package/src/domain/reference-pack.mjs +5 -0
  46. package/src/domain/sense-gate.mjs +220 -0
  47. package/src/domain/sense-scope.mjs +116 -0
  48. package/src/domain/sense-split.mjs +1 -1
  49. package/src/domain/syllogise.mjs +60 -21
  50. package/src/domain/tableau.mjs +23 -14
  51. package/src/domain/term-ledger.mjs +16 -1
  52. package/src/domain/worlds-pack.mjs +5 -1
  53. package/src/services/adventure-autoplay.mjs +6 -1
  54. package/src/services/adventure-editor.mjs +43 -21
  55. package/src/services/adventure-viz.mjs +26 -9
  56. package/src/services/adventure.mjs +40 -10
  57. package/src/services/chat.mjs +270 -125
  58. package/src/services/extensions.mjs +51 -58
  59. package/src/services/extract-facts.mjs +906 -66
  60. package/src/services/init.mjs +4 -4
  61. package/src/services/ledger-viz.mjs +9 -4
  62. package/src/services/memory-panel-viz.mjs +4 -5
  63. package/src/services/mud-editor.mjs +40 -16
  64. package/src/services/mud-viz.mjs +8 -2
  65. package/src/services/mudiii-turn.mjs +5 -3
  66. package/src/services/mudiii-viz.mjs +8 -2
  67. package/src/services/news.mjs +306 -21
  68. package/src/services/research-viz.mjs +1 -1
  69. package/src/services/sprite-catalog-viz.mjs +10 -5
  70. package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
  71. package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
  72. package/src/surfaces/web/mud-browser-entry.mjs +7 -11
  73. package/src/surfaces/web/research-browser-entry.mjs +5 -2
  74. package/corpus/tier2/aws.jsonl +0 -39
  75. package/corpus/tier2/java.jsonl +0 -31
  76. package/corpus/tier2/python.jsonl +0 -30
@@ -27,30 +27,22 @@ import { assertValidRow } from "./row-backend.mjs";
27
27
  export const BAND_PARTITION_PREFIX = "corpus:";
28
28
  export const MANIFEST_SORT_KEY = "manifest";
29
29
 
30
- /** The three bands this plan ships a build pipeline and a loader for. */
30
+ /** The bands this repo ships a build pipeline and a loader for. */
31
31
  export const FIRST_CLASS_BANDS = Object.freeze([
32
- "wikidata-slice",
33
32
  "wordnet-complete",
34
- "conceptnet-full",
35
33
  ]);
36
34
 
37
- // Reserved so the partition name is spoken for; no pipeline for it ships
38
- // here. Its own design doc covers the band when it lands.
39
- export const RESERVED_BAND_NAMES = Object.freeze(["simplewiki-derived"]);
40
-
41
35
  /** The licence and attribution notice each first-class band's content
42
36
  * carries — a property of the band's identity, not of any one load, so the
43
37
  * loader reads it from here rather than taking it as a per-invocation flag.
44
38
  * `notice` is a repo-relative path to the human-readable attribution file;
45
39
  * null when the licence carries no attribution burden. */
46
40
  export const BAND_LICENSES = Object.freeze({
47
- "wikidata-slice": Object.freeze({ license: "CC0-1.0", notice: null }),
48
41
  "wordnet-complete": Object.freeze({ license: "CC-BY-4.0", notice: "corpus/wordnet/LICENSE-NOTICE" }),
49
- "conceptnet-full": Object.freeze({ license: "CC-BY-SA-4.0", notice: "corpus/conceptnet/conceptnet-full.NOTICE" }),
50
42
  });
51
43
 
52
44
  /** `{license, notice}` for `band`, or both null when the band carries no
53
- * entry here (a consumer's own band, outside the three first-class ones). */
45
+ * entry here (a consumer's own band, outside the first-class ones). */
54
46
  export function bandLicenseInfo(band) {
55
47
  return BAND_LICENSES[band] ?? { license: null, notice: null };
56
48
  }
@@ -153,3 +145,28 @@ export function bandFactRow({ subject, predicate, object, provenance = "", band,
153
145
  };
154
146
  return assertValidRow(row, { provenance });
155
147
  }
148
+
149
+ /** The inverse of `bandFactRow`: the `{subject, predicate, object,
150
+ * provenance}` triple a band's own wire row carries, read back off its
151
+ * `json` field. A caller that grounds a term straight from a band Query (no
152
+ * article, no extraction) hands this triple to `appendFacts` unchanged, so
153
+ * the fact lands under the SAME content-addressed id and the SAME
154
+ * `corpus:<band> ...` provenance the band shipped it with — trust reads it
155
+ * as a corpus source, not a live research one.
156
+ *
157
+ * Null when `json` fails to parse or the individual carries no
158
+ * rdf:subject/predicate/object triple — a malformed row degrades to
159
+ * "nothing to fold in" rather than a throw, since a caller reads a whole
160
+ * page of rows and one bad one should not cost the rest. */
161
+ export function factFromBandRow(row) {
162
+ let parsed;
163
+ try { parsed = JSON.parse(row?.json ?? ""); } catch { return null; }
164
+ const attributes = parsed?.individual?.attributes;
165
+ if (!Array.isArray(attributes)) return null;
166
+ const valueOf = (prop) => attributes.find((a) => a?.prop === prop)?.value;
167
+ const subject = valueOf("rdf:subject");
168
+ const predicate = valueOf("rdf:predicate");
169
+ const object = valueOf("rdf:object");
170
+ if (!subject || !predicate || !object) return null;
171
+ return { subject, predicate, object, provenance: valueOf("mgx:factProvenance") || "" };
172
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { loadMemory, UTTERANCE_CLASS, IN_REPLY_TO_PROP, readFactRows, findContradictions } from "./core.mjs";
7
7
  import { loadBlockIndex } from "./blocks.mjs";
8
+ import { compareFactsByContent } from "../../domain/memory/fact-order.mjs";
8
9
  import { buildTableauKb, findTableauViolations } from "../../domain/tableau.mjs";
9
10
  import { elUnsatisfiableClasses } from "../../domain/el-classify.mjs";
10
11
 
@@ -33,6 +34,23 @@ export function balancedSample(items, k) {
33
34
  }
34
35
 
35
36
  const attrOf = (ind, key) => (ind?.attributes || []).find((a) => a.key === key)?.value || "";
37
+
38
+ /** Codepoint order, never localeCompare — this text is read on whatever machine
39
+ * holds the store, and two locales have to land on the same lines. */
40
+ const byCodepoint = (a, b) => {
41
+ const ka = String(a ?? "");
42
+ const kb = String(b ?? "");
43
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
44
+ };
45
+
46
+ /** Order two individuals by their own id, in codepoint order. Every id here is
47
+ * derived from content — a Fact hashes its triple, an Utterance reads
48
+ * `utt:<session>#<ts>#<role>`, a Source keys on its provenance tag — so this
49
+ * is a pure function of the stored set, and the utterance case still comes out
50
+ * in the order the conversation happened. Without it the class samples below
51
+ * span the payload in the order the individuals were written, which is arrival
52
+ * order: two peers holding one store would sample different rows. */
53
+ const byIndividualId = (a, b) => byCodepoint(a?.id, b?.id);
36
54
  const truncate = (s, cap) => {
37
55
  const t = String(s ?? "").replace(/\s+/g, " ").trim();
38
56
  return t.length > cap ? `${t.slice(0, cap - 1)}…` : t;
@@ -55,7 +73,8 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
55
73
  if (!byClass.has(cls)) byClass.set(cls, []);
56
74
  byClass.get(cls).push(ind);
57
75
  }
58
- const classes = [...byClass.entries()].sort((a, b) => b[1].length - a[1].length);
76
+ for (const members of byClass.values()) members.sort(byIndividualId);
77
+ const classes = [...byClass.entries()].sort((a, b) => b[1].length - a[1].length || byCodepoint(a[0], b[0]));
59
78
  lines.push(`memory — ${individuals.length} individuals: ${classes.map(([c, of]) => `${of.length} ${c}`).join(", ")}.`);
60
79
  for (const [cls, of] of classes) {
61
80
  const k = sampleSize(of.length, { verbose });
@@ -68,7 +87,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
68
87
  .filter((r) => r.sourceIds.length || r.provenance)
69
88
  .sort((a, b) => b.trust - a.trust
70
89
  || b.sourceIds.length - a.sourceIds.length
71
- || `${a.subject} ${a.predicate} ${a.object}`.localeCompare(`${b.subject} ${b.predicate} ${b.object}`));
90
+ || compareFactsByContent(a, b));
72
91
  if (ranked.length) {
73
92
  lines.push("", "top facts by trust:");
74
93
  for (const r of ranked.slice(0, verbose ? 8 : 3)) {
@@ -137,7 +156,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
137
156
  const pairs = (replyGroup?.examples || [])
138
157
  .map((e) => ({ a: byId.get(e.subject), q: byId.get(e.object) }))
139
158
  .filter((p) => p.a && p.q && p.a.class === UTTERANCE_CLASS)
140
- .sort((x, y) => attrOf(y.a, "ts").localeCompare(attrOf(x.a, "ts")));
159
+ .sort((x, y) => byCodepoint(attrOf(y.a, "ts"), attrOf(x.a, "ts")) || byIndividualId(x.a, y.a));
141
160
  if (pairs.length) {
142
161
  lines.push("", `recent Q→A pairs (${pairs.length} recorded):`);
143
162
  for (const p of pairs.slice(0, verbose ? 8 : 3)) {
@@ -153,7 +172,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
153
172
  const tokens = entries.reduce((n, [, b]) => n + (b.tokens?.length || 0), 0);
154
173
  const top = entries
155
174
  .slice()
156
- .sort((a, b) => (b[1].rank ?? 0) - (a[1].rank ?? 0) || a[0].localeCompare(b[0]))
175
+ .sort((a, b) => (b[1].rank ?? 0) - (a[1].rank ?? 0) || byCodepoint(a[0], b[0]))
157
176
  .slice(0, verbose ? 8 : 3);
158
177
  lines.push("", `blocks — ${entries.length} folded session block${entries.length === 1 ? "" : "s"}, ${tokens} indexed tokens.`);
159
178
  lines.push(` top by rank: ${top.map(([id, b]) => `${String(id).slice(0, 8)} (${(b.rank ?? 0).toFixed(3)})`).join(", ")}`);
@@ -169,7 +188,7 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
169
188
  const freq = new Map();
170
189
  for (const f of facts) if (clean(f.object)) freq.set(f.object, (freq.get(f.object) || 0) + 1);
171
190
  const terms = [...freq.entries()]
172
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
191
+ .sort((a, b) => b[1] - a[1] || byCodepoint(a[0], b[0]))
173
192
  .map(([t]) => t)
174
193
  .slice(0, 3);
175
194
  const sample = facts.find((f) => clean(f.subject) && terms.includes(f.object));
@@ -89,8 +89,13 @@ function storedIndividualForm(ind) {
89
89
  /** The ord each row key carries. An existing key keeps the ord it already had
90
90
  * and a new one takes the next free number, matching how a store keeps a row's
91
91
  * sort position across an update. Without prior rows the ords are the payload's
92
- * own array order. */
93
- function ordAssigner(priorRows) {
92
+ * own array order.
93
+ *
94
+ * `baseOrds` is a read-only layer's key -> ord map, consulted after the prior
95
+ * rows so a key both layers hold keeps the row's own ord. It is read in place:
96
+ * a caller holding one for the life of its handle hands it over rather than
97
+ * projecting a row per key and parsing each back. */
98
+ function ordAssigner(priorRows, baseOrds) {
94
99
  const priorOrds = new Map();
95
100
  let next = 0;
96
101
  for (const row of priorRows || []) {
@@ -100,8 +105,9 @@ function ordAssigner(priorRows) {
100
105
  priorOrds.set(row.rowKey, ord);
101
106
  if (ord >= next) next = ord + 1;
102
107
  }
108
+ for (const ord of baseOrds?.values() || []) if (Number.isFinite(ord) && ord >= next) next = ord + 1;
103
109
  return (rowKey) => {
104
- const prior = priorOrds.get(rowKey);
110
+ const prior = priorOrds.get(rowKey) ?? baseOrds?.get(rowKey);
105
111
  if (prior !== undefined) return prior;
106
112
  const ord = next;
107
113
  next += 1;
@@ -139,14 +145,16 @@ const OVERSIZED_ROW_POSTURES = new Set(["throw", "drop", "keep"]);
139
145
  *
140
146
  * `priorRows` are the rows this payload was last projected as; pass them and
141
147
  * every unchanged row comes back byte-identical, so `diffRows` writes only
142
- * what actually moved. `onOversizedRow` is "throw" (default), "drop", or
143
- * "keep" (rows that never reach the wire see `admitRow`), and `log` takes
144
- * the drop notices. */
145
- export function payloadToRows(payload, { priorRows = null, onOversizedRow = "throw", log = warnToConsole } = {}) {
148
+ * what actually moved. `priorOrds` is the same information for a read-only
149
+ * layer that already holds it as a key -> ord map (a sqlite seed's own key
150
+ * columns), read under the prior rows. `onOversizedRow` is "throw" (default),
151
+ * "drop", or "keep" (rows that never reach the wire see `admitRow`), and
152
+ * `log` takes the drop notices. */
153
+ export function payloadToRows(payload, { priorRows = null, priorOrds = null, onOversizedRow = "throw", log = warnToConsole } = {}) {
146
154
  if (!OVERSIZED_ROW_POSTURES.has(onOversizedRow)) {
147
155
  throw new TypeError(`onOversizedRow must be "throw", "drop", or "keep", got ${JSON.stringify(onOversizedRow)}`);
148
156
  }
149
- const ordFor = ordAssigner(priorRows);
157
+ const ordFor = ordAssigner(priorRows, priorOrds);
150
158
  const posture = { onOversizedRow, log };
151
159
  const rows = [];
152
160
 
@@ -206,6 +214,168 @@ function sortFactIndividualsById(individuals) {
206
214
  for (let i = 0; i < slots.length; i += 1) individuals[slots[i]] = facts[i];
207
215
  }
208
216
 
217
+ /** The prose tokens, forward supersession list and backward-pointer presence one
218
+ * individual carries, read in a single pass over its attributes. First match
219
+ * per field, which is what the `find`-based readers elsewhere in this file
220
+ * take. */
221
+ function derivationInputsOf(ind) {
222
+ let proseTokens = "";
223
+ let supersedes = "";
224
+ let carriesSupersededBy = false;
225
+ let seenProseTokens = false;
226
+ let seenSupersedes = false;
227
+ for (const a of ind?.attributes || []) {
228
+ if (!seenProseTokens && a?.key === "prose_tokens") { proseTokens = a.value || ""; seenProseTokens = true; }
229
+ if (!seenSupersedes && a?.prop === SUPERSEDES_PROP) { supersedes = a.value || ""; seenSupersedes = true; }
230
+ if (a?.prop === SUPERSEDED_BY_PROP) carriesSupersededBy = true;
231
+ }
232
+ return { proseTokens, supersedes, carriesSupersededBy };
233
+ }
234
+
235
+ // What a payload carries forward so the next write reconciles its derived
236
+ // structures instead of building them again: what each individual last put into
237
+ // the prose index, what each record last named as superseded, and the reverse of
238
+ // that. Non-enumerable and symbol-keyed, so no copy of the payload inherits
239
+ // state that describes a different array of individuals, and a payload without
240
+ // it (a fresh assembly, a clone, a hand-built fixture) derives from scratch.
241
+ const CARRIED_DERIVATIONS = Symbol("tmct.carriedDerivations");
242
+
243
+ function carryForward(payload, carried) {
244
+ carried.index = payload.proseIndex;
245
+ Object.defineProperty(payload, CARRIED_DERIVATIONS, {
246
+ value: carried, writable: true, configurable: true, enumerable: false,
247
+ });
248
+ }
249
+
250
+ // The ids of the assembled individuals in ROW order, kept beside the assembled
251
+ // array. `sortFactIndividualsById` lifts the Facts out of the slots row order
252
+ // put them in, so the array itself no longer says which slot each Fact came
253
+ // from — and a removal needs exactly that, because dropping a Fact drops one of
254
+ // those slots, not the position the sort moved it to. Non-enumerable and
255
+ // symbol-keyed for the same reason as the derivations above; the `individuals`
256
+ // reference is the guard, so a copy or a rebuilt array is never reconciled
257
+ // against an order that describes a different one.
258
+ const ASSEMBLED_ROW_ORDER = Symbol("tmct.assembledRowOrder");
259
+
260
+ function carryRowOrder(payload, ids) {
261
+ Object.defineProperty(payload, ASSEMBLED_ROW_ORDER, {
262
+ value: { individuals: payload.individuals, ids },
263
+ writable: true, configurable: true, enumerable: false,
264
+ });
265
+ }
266
+
267
+ /** The row order this payload was assembled in, or null when it carries none
268
+ * or the one it carries describes a different individuals array. */
269
+ function carriedRowOrder(payload) {
270
+ const carried = payload?.[ASSEMBLED_ROW_ORDER];
271
+ if (!carried || carried.individuals !== payload.individuals) return null;
272
+ return carried;
273
+ }
274
+
275
+ /** True when `removedIds` can be dropped from this payload's individuals
276
+ * incrementally — every id is one the assembly actually holds, and the payload
277
+ * still carries the row order that says which slot each one occupies. Asked
278
+ * before anything is mutated, so a caller that gets `false` can rebuild from a
279
+ * payload nothing has touched. */
280
+ export function canDropAssembledIndividuals(payload, removedIds) {
281
+ const carried = carriedRowOrder(payload);
282
+ if (!carried) return false;
283
+ const held = new Set(carried.ids);
284
+ for (const id of removedIds) if (!held.has(String(id))) return false;
285
+ return true;
286
+ }
287
+
288
+ /** Drop `removedIds` from an assembled payload's individuals, in place, leaving
289
+ * the array a rebuild from the remaining rows would have produced. Reads the
290
+ * row order back, filters the dropped ids out of it, and refills the array in
291
+ * that order — so the fact slots that survive are the ones the surviving ROWS
292
+ * own, which is what `sortFactIndividualsById` then sorts into. Returns false
293
+ * when the payload has no usable row order, having changed nothing. */
294
+ export function dropAssembledIndividuals(payload, removedIds) {
295
+ const carried = carriedRowOrder(payload);
296
+ if (!carried) return false;
297
+ const dropped = new Set([...removedIds].map((id) => String(id)));
298
+ const byId = new Map();
299
+ for (const ind of payload.individuals || []) if (ind?.id) byId.set(String(ind.id), ind);
300
+ const keptIds = [];
301
+ const kept = [];
302
+ for (const id of carried.ids) {
303
+ if (dropped.has(id)) continue;
304
+ const ind = byId.get(id);
305
+ if (!ind) return false;
306
+ keptIds.push(id);
307
+ kept.push(ind);
308
+ }
309
+ payload.individuals = kept;
310
+ carried.individuals = kept;
311
+ carried.ids = keptIds;
312
+ return true;
313
+ }
314
+
315
+ /** Forget the row order this payload carries, for a caller that rewrote the ids
316
+ * the order names (the load-time legacy-fact-id heal). The next removal
317
+ * rebuilds from rows instead of patching. */
318
+ export function dropAssembledRowOrder(payload) {
319
+ if (payload && payload[ASSEMBLED_ROW_ORDER]) payload[ASSEMBLED_ROW_ORDER] = null;
320
+ }
321
+
322
+ /** Record one newly-assembled individual's id at the tail of the row order —
323
+ * where a row keyed for the first time lands, since `payloadToRows` gives it an
324
+ * ord past every ord already assembled. A payload carrying no row order stays
325
+ * that way, and the next removal rebuilds instead of patching. */
326
+ export function appendAssembledRowOrder(payload, id) {
327
+ const carried = carriedRowOrder(payload);
328
+ if (carried) carried.ids.push(String(id));
329
+ }
330
+
331
+ /** What this payload can reconcile against, or null when it must derive
332
+ * instead. `index` identity is the guard: state that describes some other
333
+ * prose index cannot be applied to this one. `needSupersessions` narrows to
334
+ * the callers that maintain the backward pointers too. */
335
+ function carriedDerivationsOf(payload, { needSupersessions } = {}) {
336
+ const carried = payload[CARRIED_DERIVATIONS];
337
+ if (!carried || carried.index !== payload.proseIndex || !payload.proseIndex) return null;
338
+ if (needSupersessions && !carried.successorsById) return null;
339
+ return carried;
340
+ }
341
+
342
+ /** Where `id` sits in a sorted posting list, and whether it is there at all. */
343
+ function postingSlot(list, id) {
344
+ let low = 0;
345
+ let high = list.length;
346
+ while (low < high) {
347
+ const mid = (low + high) >> 1;
348
+ if (list[mid] < id) low = mid + 1;
349
+ else high = mid;
350
+ }
351
+ return low;
352
+ }
353
+
354
+ /** Move one individual's contribution to the prose index from `before` to
355
+ * `after`, in place.
356
+ *
357
+ * `buildProseIndex` produces, per word, the multiset of ids that named it —
358
+ * one entry per (individual, occurrence of the word in its token string) —
359
+ * sorted, with a word nobody names absent altogether. Dropping one entry per
360
+ * old occurrence and inserting one per new occurrence lands on exactly that
361
+ * multiset, and inserting in sorted position keeps exactly that order, so a
362
+ * reconciled index and a rebuilt one are the same object graph. */
363
+ function moveProseTokens(index, id, before, after) {
364
+ if (before === after) return;
365
+ for (const word of before ? before.split(" ") : []) {
366
+ const list = index[word];
367
+ if (!list) continue;
368
+ const at = postingSlot(list, id);
369
+ if (list[at] !== id) continue;
370
+ list.splice(at, 1);
371
+ if (!list.length) delete index[word];
372
+ }
373
+ for (const word of after ? after.split(" ") : []) {
374
+ const list = index[word] || (index[word] = []);
375
+ list.splice(postingSlot(list, id), 0, id);
376
+ }
377
+ }
378
+
209
379
  /** Re-derive each record's backward supersession pointer from the union of the
210
380
  * forward ones. Two turns that superseded the same record concurrently both
211
381
  * land, because each wrote its own row and neither touched the record they
@@ -215,36 +385,143 @@ function sortFactIndividualsById(individuals) {
215
385
  * used to carry. Rows never carry one (`storedIndividualForm` strips it), so
216
386
  * that arm is dead when this runs over a fresh projection and live when it
217
387
  * runs again over individuals it already derived. */
218
- function applyDerivedSupersessions(individuals) {
388
+ function writeSupersededBy(ind, successors) {
389
+ const carried = (ind?.attributes || []).some((a) => a?.prop === SUPERSEDED_BY_PROP);
390
+ if (!successors && !carried) return;
391
+ const rest = (ind.attributes || []).filter((a) => a?.prop !== SUPERSEDED_BY_PROP);
392
+ ind.attributes = successors
393
+ ? [...rest, { prop: SUPERSEDED_BY_PROP, key: "supersededBy", value: [...successors].sort().join(" ") }]
394
+ : rest;
395
+ }
396
+
397
+ /** The forward supersession list one individual states, as the derivation reads
398
+ * it: a Fact's own `mgx:supersedes` value, and nothing at all from anything
399
+ * else, which is the same narrowing the from-scratch pass applies. */
400
+ const supersedesStatedBy = (ind, supersedes) => (ind?.class === FACT_CLASS ? supersedes : "");
401
+
402
+ /** Both derived structures over one payload, built from nothing and recorded so
403
+ * the next write can reconcile rather than repeat this. */
404
+ function deriveProseAndSupersessions(payload, individuals) {
219
405
  const successorsById = new Map();
406
+ const tokensById = new Map();
407
+ const supersedesById = new Map();
220
408
  for (const ind of individuals) {
221
- if (ind?.class !== FACT_CLASS) continue;
222
- for (const replaced of attrValue(ind, SUPERSEDES_PROP).split(" ").filter(Boolean)) {
409
+ const { proseTokens, supersedes } = derivationInputsOf(ind);
410
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
411
+ const stated = supersedesStatedBy(ind, supersedes);
412
+ if (!stated) continue;
413
+ supersedesById.set(ind.id, stated);
414
+ for (const replaced of stated.split(" ").filter(Boolean)) {
223
415
  const successors = successorsById.get(replaced) || new Set();
224
416
  successors.add(ind.id);
225
417
  successorsById.set(replaced, successors);
226
418
  }
227
419
  }
420
+ for (const ind of individuals) writeSupersededBy(ind, successorsById.get(ind?.id));
421
+ // Released before the replacement is built, not after: over a seed-sized
422
+ // store the prose index is the largest derived structure here, and holding
423
+ // the outgoing one while the incoming one grows doubles it for no reason.
424
+ payload.proseIndex = null;
425
+ payload.proseIndex = buildProseIndex(individuals);
426
+ carryForward(payload, { tokensById, supersedesById, successorsById });
427
+ }
428
+
429
+ /** Both derived structures brought up to date from the ones this payload
430
+ * already carries. One pass over the individuals reads what each contributes
431
+ * now; only what disagrees with what it contributed last time is applied.
432
+ *
433
+ * A write touches a handful of records out of a seed's worth, so this pays for
434
+ * the walk and the handful, where the from-scratch build pays for every token
435
+ * in the store and sorts every posting list it produced. */
436
+ function reconcileProseAndSupersessions(payload, individuals, carried) {
437
+ const { tokensById, supersedesById, successorsById } = carried;
438
+ const index = payload.proseIndex;
439
+ const resettled = new Set();
440
+ const carriesPointer = new Set();
441
+ let tokenBearers = 0;
442
+ let supersedingRecords = 0;
443
+
444
+ const forgetSupersedes = (id, replaced) => {
445
+ const successors = successorsById.get(replaced);
446
+ if (!successors?.delete(id)) return;
447
+ if (!successors.size) successorsById.delete(replaced);
448
+ resettled.add(replaced);
449
+ };
450
+ const recordSupersedes = (id, replaced) => {
451
+ const successors = successorsById.get(replaced) || new Set();
452
+ if (successors.has(id)) return;
453
+ successors.add(id);
454
+ successorsById.set(replaced, successors);
455
+ resettled.add(replaced);
456
+ };
457
+
458
+ for (const ind of individuals) {
459
+ const id = ind?.id;
460
+ const { proseTokens, supersedes, carriesSupersededBy } = derivationInputsOf(ind);
461
+ if (carriesSupersededBy) carriesPointer.add(id);
462
+
463
+ if (proseTokens) tokenBearers += 1;
464
+ const wasTokens = tokensById.get(id) || "";
465
+ if (proseTokens !== wasTokens) {
466
+ moveProseTokens(index, id, wasTokens, proseTokens);
467
+ if (proseTokens) tokensById.set(id, proseTokens);
468
+ else tokensById.delete(id);
469
+ }
470
+
471
+ const stated = supersedesStatedBy(ind, supersedes);
472
+ if (stated) supersedingRecords += 1;
473
+ const wasStated = supersedesById.get(id) || "";
474
+ if (stated !== wasStated) {
475
+ for (const replaced of wasStated.split(" ").filter(Boolean)) forgetSupersedes(id, replaced);
476
+ for (const replaced of stated.split(" ").filter(Boolean)) recordSupersedes(id, replaced);
477
+ if (stated) supersedesById.set(id, stated);
478
+ else supersedesById.delete(id);
479
+ }
480
+ }
481
+
482
+ // A caller that only added or rewrote individuals leaves both maps holding
483
+ // exactly what the walk above just saw, and the counts say so. They disagree
484
+ // only when an individual left the payload, which costs one more walk to
485
+ // find and is the rarer write by far.
486
+ if (tokensById.size !== tokenBearers || supersedesById.size !== supersedingRecords) {
487
+ const present = new Set(individuals.map((ind) => ind?.id));
488
+ for (const [id, tokens] of tokensById) {
489
+ if (present.has(id)) continue;
490
+ moveProseTokens(index, id, tokens, "");
491
+ tokensById.delete(id);
492
+ }
493
+ for (const [id, stated] of supersedesById) {
494
+ if (present.has(id)) continue;
495
+ for (const replaced of stated.split(" ").filter(Boolean)) forgetSupersedes(id, replaced);
496
+ supersedesById.delete(id);
497
+ }
498
+ }
499
+
500
+ // Exactly the records the from-scratch pass rewrites: the ones with
501
+ // successors and the ones already carrying a pointer. Both sets are bounded
502
+ // by how many supersessions the store holds, which is a handful beside its
503
+ // individuals, so this rewrites the same records for the same cost and leaves
504
+ // the rest of the store alone.
505
+ for (const id of carriesPointer) resettled.add(id);
506
+ for (const id of successorsById.keys()) resettled.add(id);
507
+ if (!resettled.size) return;
228
508
  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;
509
+ if (resettled.has(ind?.id)) writeSupersededBy(ind, successorsById.get(ind?.id));
236
510
  }
237
511
  }
238
512
 
239
513
  /** Recount `classes[]` from the assembled individuals, the same count-and-
240
514
  * sample shape the store keeps. */
241
515
  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) });
516
+ const order = [MEMORY_SESSION_CLASS, UTTERANCE_CLASS, FACT_CLASS, SOURCE_CLASS, RULE_CLASS];
517
+ const counted = new Map(order.map((name) => [name, { name, count: 0, sample: [] }]));
518
+ for (const ind of individuals) {
519
+ const row = counted.get(ind?.class);
520
+ if (!row) continue;
521
+ row.count += 1;
522
+ if (row.sample.length < 3) row.sample.push(ind.label);
246
523
  }
247
- return classes;
524
+ return order.map((name) => counted.get(name)).filter((row) => row.count);
248
525
  }
249
526
 
250
527
  /** The store's `generated_at`: the latest utterance timestamp it holds, which
@@ -281,8 +558,16 @@ export function rowsToPayload(rows, { meta = null } = {}) {
281
558
  individualEntries.sort(byOrdThenRowKey);
282
559
  groupEntries.sort(byOrdThenRowKey);
283
560
 
284
- payload.individuals = individualEntries.map((e) => e.individual).filter(Boolean);
561
+ const rowOrderIds = [];
562
+ const individuals = [];
563
+ for (const entry of individualEntries) {
564
+ if (!entry.individual) continue;
565
+ individuals.push(entry.individual);
566
+ rowOrderIds.push(String(entry.individual.id ?? ""));
567
+ }
568
+ payload.individuals = individuals;
285
569
  payload.objectProperties = groupEntries.map((e) => e.group).filter(Boolean);
570
+ carryRowOrder(payload, rowOrderIds);
286
571
  return renormalizeAssembledPayload(payload);
287
572
  }
288
573
 
@@ -296,17 +581,61 @@ export function rowsToPayload(rows, { meta = null } = {}) {
296
581
  export function renormalizeAssembledPayload(payload) {
297
582
  const individuals = payload.individuals || [];
298
583
  sortFactIndividualsById(individuals);
299
- applyDerivedSupersessions(individuals);
584
+ const carried = carriedDerivationsOf(payload, { needSupersessions: true });
585
+ if (carried) reconcileProseAndSupersessions(payload, individuals, carried);
586
+ else deriveProseAndSupersessions(payload, individuals);
300
587
  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
588
  payload.generated_at = latestUtteranceTimestamp(individuals);
307
589
  return payload;
308
590
  }
309
591
 
592
+ /** Only the prose index, reconciled the same way — for a caller that derives
593
+ * everything else itself and whose payload is not a row assembly
594
+ * (`mutateMemory` over a non-row backend). A payload with nothing to reconcile
595
+ * against gets the full build, and carries the state on so the next write
596
+ * reconciles. Mutates and returns `payload`. */
597
+ export function renormalizeProseIndex(payload) {
598
+ const individuals = payload.individuals || [];
599
+ const carried = carriedDerivationsOf(payload);
600
+ if (!carried) {
601
+ const tokensById = new Map();
602
+ for (const ind of individuals) {
603
+ const { proseTokens } = derivationInputsOf(ind);
604
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
605
+ }
606
+ payload.proseIndex = null;
607
+ payload.proseIndex = buildProseIndex(individuals);
608
+ carryForward(payload, { tokensById, supersedesById: null, successorsById: null });
609
+ return payload;
610
+ }
611
+ const { tokensById } = carried;
612
+ // This pass maintains the index and nothing else, so any supersession state
613
+ // beside it stops describing the individuals it claims to and goes now,
614
+ // rather than being reconciled against later.
615
+ carried.supersedesById = null;
616
+ carried.successorsById = null;
617
+ const index = payload.proseIndex;
618
+ let tokenBearers = 0;
619
+ for (const ind of individuals) {
620
+ const { proseTokens } = derivationInputsOf(ind);
621
+ if (proseTokens) tokenBearers += 1;
622
+ const wasTokens = tokensById.get(ind?.id) || "";
623
+ if (proseTokens === wasTokens) continue;
624
+ moveProseTokens(index, ind?.id, wasTokens, proseTokens);
625
+ if (proseTokens) tokensById.set(ind.id, proseTokens);
626
+ else tokensById.delete(ind?.id);
627
+ }
628
+ if (tokensById.size !== tokenBearers) {
629
+ const present = new Set(individuals.map((ind) => ind?.id));
630
+ for (const [id, tokens] of tokensById) {
631
+ if (present.has(id)) continue;
632
+ moveProseTokens(index, id, tokens, "");
633
+ tokensById.delete(id);
634
+ }
635
+ }
636
+ return payload;
637
+ }
638
+
310
639
  /** The rows to write and the row keys to delete to turn `before` into `after`.
311
640
  * A row whose stored bytes did not change is not in either list, so a store
312
641
  * pays only for what actually moved. */
@@ -18,12 +18,19 @@ export const EXTRACTION_FINDINGS = Object.freeze([
18
18
  "clause-fallback", // the row grounded from a clause fragment after the whole sentence declined
19
19
  "pronoun-carry", // the subject was substituted from the paragraph's pronoun carry
20
20
  "definitional-frame", // the row came from a definitional copula frame ("X is the name for Y")
21
+ "reported-speech", // the row was read from a reported-speech clause, its speaker attributed separately
21
22
  ]);
22
23
  const EXTRACTION_FINDING_SET = new Set(EXTRACTION_FINDINGS);
23
- const RULE_KINDS = new Set([
24
+
25
+ // The closed vocabulary of a taught Rule's SHAPE tag — three structural
26
+ // kinds plus the action-* family shared by one taught sentence's sibling
27
+ // Rules. Exported (like EXTRACTION_FINDINGS above) so an estate guard can
28
+ // cross-check it against its own prose mirrors in the ontology files.
29
+ export const RULE_KINDS = Object.freeze([
24
30
  "compose2", "filter", "recursive",
25
31
  "action-signature", "action-precond", "action-effect", "action-constraint",
26
32
  ]);
33
+ const RULE_KIND_SET = new Set(RULE_KINDS);
27
34
 
28
35
  // Mirrors the REQUIRED subset of core.mjs's own (unexported) RULE_SLOT_SPEC —
29
36
  // kept in sync by hand. Two kinds there also carry optional slots
@@ -140,8 +147,8 @@ function checkFact(ind, violations) {
140
147
  function checkRule(ind, violations) {
141
148
  if (!nonEmpty(attrValue(ind, "mgx:ruleName"))) violations.push("a Rule needs a non-empty mgx:ruleName");
142
149
  const kind = attrValue(ind, "mgx:ruleKind");
143
- if (!kind || !RULE_KINDS.has(kind)) {
144
- violations.push(`a Rule's mgx:ruleKind must be one of ${[...RULE_KINDS].join(" | ")} (got ${JSON.stringify(kind)})`);
150
+ if (!kind || !RULE_KIND_SET.has(kind)) {
151
+ violations.push(`a Rule's mgx:ruleKind must be one of ${RULE_KINDS.join(" | ")} (got ${JSON.stringify(kind)})`);
145
152
  return; // no declared kind to check slots against
146
153
  }
147
154
  for (const prop of RULE_SLOT_PROPS[kind]) {