@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.
- package/README.md +20 -23
- package/bin/tmct.mjs +16 -33
- package/corpus/LICENSES.json +0 -21
- package/corpus/README.md +10 -13
- package/corpus/reference/manifest.json +19 -19
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/tier2/generate.mjs +6 -142
- package/corpus/tier2/manifest.json +0 -42
- package/package.json +6 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/corpus/research-source.mjs +6 -2
- package/src/adapters/corpus/wikidata-live.mjs +92 -51
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +505 -107
- package/src/adapters/memory/corpus-bands.mjs +27 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +359 -30
- package/src/adapters/memory/shacl.mjs +10 -3
- package/src/domain/ask.mjs +27 -10
- package/src/domain/cli-verbs.mjs +3 -4
- package/src/domain/completions/group.mjs +8 -3
- package/src/domain/completions/infer.mjs +7 -2
- package/src/domain/completions/prune.mjs +5 -1
- package/src/domain/completions/rank.mjs +7 -2
- package/src/domain/digest/compose.mjs +5 -1
- package/src/domain/digest/select.mjs +12 -6
- package/src/domain/domain.mjs +15 -8
- package/src/domain/el-classify.mjs +11 -2
- package/src/domain/fact-phrase.mjs +86 -4
- package/src/domain/hash.mjs +9 -0
- package/src/domain/memory/bias.mjs +8 -4
- package/src/domain/memory/capability.mjs +12 -6
- package/src/domain/memory/fact-order.mjs +29 -0
- package/src/domain/memory/resolution.mjs +3 -0
- package/src/domain/news-feed.mjs +862 -92
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-gate.mjs +220 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +60 -21
- package/src/domain/tableau.mjs +23 -14
- package/src/domain/term-ledger.mjs +16 -1
- package/src/domain/worlds-pack.mjs +5 -1
- package/src/services/adventure-autoplay.mjs +6 -1
- package/src/services/adventure-editor.mjs +43 -21
- package/src/services/adventure-viz.mjs +26 -9
- package/src/services/adventure.mjs +40 -10
- package/src/services/chat.mjs +270 -125
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +906 -66
- package/src/services/init.mjs +4 -4
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +4 -5
- package/src/services/mud-editor.mjs +40 -16
- package/src/services/mud-viz.mjs +8 -2
- package/src/services/mudiii-turn.mjs +5 -3
- package/src/services/mudiii-viz.mjs +8 -2
- package/src/services/news.mjs +306 -21
- package/src/services/research-viz.mjs +1 -1
- package/src/services/sprite-catalog-viz.mjs +10 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
- package/src/surfaces/web/mud-browser-entry.mjs +7 -11
- package/src/surfaces/web/research-browser-entry.mjs +5 -2
- package/corpus/tier2/aws.jsonl +0 -39
- package/corpus/tier2/java.jsonl +0 -31
- 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
|
|
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
|
|
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
|
|
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
|
-
||
|
|
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")
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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. `
|
|
143
|
-
*
|
|
144
|
-
* the
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || !
|
|
144
|
-
violations.push(`a Rule's mgx:ruleKind must be one of ${
|
|
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]) {
|