@polycode-projects/the-mechanical-code-talker 6.0.19 → 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 +4 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +453 -103
- package/src/adapters/memory/corpus-bands.mjs +27 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +106 -9
- 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 +422 -56
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +21 -13
- package/src/domain/tableau.mjs +23 -14
- 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 +253 -113
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +670 -95
- 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 +257 -11
- 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
|
|
|
@@ -239,6 +247,87 @@ function carryForward(payload, carried) {
|
|
|
239
247
|
});
|
|
240
248
|
}
|
|
241
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
|
+
|
|
242
331
|
/** What this payload can reconcile against, or null when it must derive
|
|
243
332
|
* instead. `index` identity is the guard: state that describes some other
|
|
244
333
|
* prose index cannot be applied to this one. `needSupersessions` narrows to
|
|
@@ -469,8 +558,16 @@ export function rowsToPayload(rows, { meta = null } = {}) {
|
|
|
469
558
|
individualEntries.sort(byOrdThenRowKey);
|
|
470
559
|
groupEntries.sort(byOrdThenRowKey);
|
|
471
560
|
|
|
472
|
-
|
|
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;
|
|
473
569
|
payload.objectProperties = groupEntries.map((e) => e.group).filter(Boolean);
|
|
570
|
+
carryRowOrder(payload, rowOrderIds);
|
|
474
571
|
return renormalizeAssembledPayload(payload);
|
|
475
572
|
}
|
|
476
573
|
|
|
@@ -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]) {
|
package/src/domain/ask.mjs
CHANGED
|
@@ -43,6 +43,23 @@ import { pickPhrase } from "./answer-variants.mjs";
|
|
|
43
43
|
export { normalizeQuery, applyNegationFrames };
|
|
44
44
|
import { defaultNlp } from "./interpret/nlp-registry.mjs";
|
|
45
45
|
|
|
46
|
+
/** Codepoint order, never localeCompare. Every string sorted through this is a
|
|
47
|
+
* label, id or attribute value read off the graph. Several of these sorts get
|
|
48
|
+
* read at `[0]` (the newest commit a query substitutes in, the entry-point
|
|
49
|
+
* module a where-defined answer names) or cut to a top-N, so the comparator
|
|
50
|
+
* picks WHICH individual the answer is about. A locale-sensitive compare would
|
|
51
|
+
* let two readers ask one graph the same question and be told about different
|
|
52
|
+
* commits. */
|
|
53
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
54
|
+
|
|
55
|
+
/** Commits newest first, ties broken on the commit's own id. The date is
|
|
56
|
+
* ISO-8601, so a codepoint compare IS the date compare, and undated commits
|
|
57
|
+
* sort last. The id tiebreak is what makes `[0]` a pure function of the
|
|
58
|
+
* graph: two commits sharing a date would otherwise be separated by whichever
|
|
59
|
+
* edge happened to be walked first. */
|
|
60
|
+
const byNewestCommit = (dateOf) => (a, b) =>
|
|
61
|
+
byCodepoint(dateOf(b), dateOf(a)) || byCodepoint(String(a.id), String(b.id));
|
|
62
|
+
|
|
46
63
|
// Per-graph, per-kind memo; a local copy of codegraph.mjs's private
|
|
47
64
|
// edgesOfKind. Named differently from codegraph.mjs's own cache: the inlined
|
|
48
65
|
// viewer bundle concatenates a stripped codegraph.mjs + this file into one
|
|
@@ -2045,7 +2062,7 @@ function computeFind(graph, entityType, term) {
|
|
|
2045
2062
|
* the architecture map prints, so the two surfaces agree. */
|
|
2046
2063
|
function packageIndividuals(graph) {
|
|
2047
2064
|
return [...packageCounts(modulesOf(graph)).entries()]
|
|
2048
|
-
.sort((a, b) => b[1] - a[1] || a[0]
|
|
2065
|
+
.sort((a, b) => b[1] - a[1] || byCodepoint(a[0], b[0]))
|
|
2049
2066
|
.map(([dir]) => ({ id: `pkg:${dir}`, label: dir, class: "Package" }));
|
|
2050
2067
|
}
|
|
2051
2068
|
|
|
@@ -2364,7 +2381,7 @@ export function degreeMetric(graph, ind, metric) {
|
|
|
2364
2381
|
function evalRecentCommits(graph) {
|
|
2365
2382
|
const commits = graph.individuals.filter((i) => i.class === "Commit");
|
|
2366
2383
|
const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
|
|
2367
|
-
commits.sort((
|
|
2384
|
+
commits.sort(byNewestCommit(dateOf));
|
|
2368
2385
|
return { compositeKind: "recentCommits", matches: commits };
|
|
2369
2386
|
}
|
|
2370
2387
|
|
|
@@ -2407,7 +2424,7 @@ function evalCommitFilter(graph, ast) {
|
|
|
2407
2424
|
if (op === "after") return d > pivotDate;
|
|
2408
2425
|
return d === pivotDate; // "on"
|
|
2409
2426
|
})
|
|
2410
|
-
.sort((
|
|
2427
|
+
.sort(byNewestCommit(dateOf));
|
|
2411
2428
|
// A kind-headed window reads what the qualifying commits touched at that
|
|
2412
2429
|
// grain; the bare shape answers with the commits themselves.
|
|
2413
2430
|
const touched = entityType && entityType !== "Commit" && entityType !== "Change"
|
|
@@ -2450,7 +2467,7 @@ function evalTemporal(graph, ast, opts) {
|
|
|
2450
2467
|
// reverseOverSet(touches) collects touching commits across both grains.
|
|
2451
2468
|
const commits = reverseOverSet(graph, "touches", "Commit", ids);
|
|
2452
2469
|
const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
|
|
2453
|
-
commits.sort((
|
|
2470
|
+
commits.sort(byNewestCommit(dateOf));
|
|
2454
2471
|
return { compositeKind: "temporal", matches: commits, entityType: ast.entityType, innerCount: inner.length };
|
|
2455
2472
|
}
|
|
2456
2473
|
|
|
@@ -3899,7 +3916,7 @@ function rankEntryPointModules(graph, term) {
|
|
|
3899
3916
|
fixture: isTestFixturePath(ind.label) ? 1 : 0,
|
|
3900
3917
|
}))
|
|
3901
3918
|
.sort((a, b) => (b.named - a.named) || (a.depth - b.depth) || (a.fixture - b.fixture)
|
|
3902
|
-
|| String(a.ind.label)
|
|
3919
|
+
|| byCodepoint(String(a.ind.label), String(b.ind.label)))
|
|
3903
3920
|
.map((x) => x.ind);
|
|
3904
3921
|
}
|
|
3905
3922
|
|
|
@@ -3939,7 +3956,7 @@ function subclassClosure(graph, ind, kind) {
|
|
|
3939
3956
|
found.set(next.id, next);
|
|
3940
3957
|
queue.push(...childrenOf(next));
|
|
3941
3958
|
}
|
|
3942
|
-
return [...found.values()].sort((a, b) => String(a.label)
|
|
3959
|
+
return [...found.values()].sort((a, b) => byCodepoint(String(a.label), String(b.label)));
|
|
3943
3960
|
}
|
|
3944
3961
|
|
|
3945
3962
|
/** A graph's own vocabulary nodes carry their definition text under one of
|
|
@@ -4171,7 +4188,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
4171
4188
|
const c = graph.byId.get(e.subject);
|
|
4172
4189
|
if (c && c.class === "Commit") commits.push(c);
|
|
4173
4190
|
}
|
|
4174
|
-
commits.sort((
|
|
4191
|
+
commits.sort(byNewestCommit(dateOf));
|
|
4175
4192
|
}
|
|
4176
4193
|
// The dated commit this answer named is a discourse `event` referent, so a
|
|
4177
4194
|
// later "was that before X was touched" binds it (see evalCommitFilter).
|
|
@@ -4203,7 +4220,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
4203
4220
|
const c = graph.byId.get(e.subject);
|
|
4204
4221
|
if (c && c.class === "Commit") commits.push(c);
|
|
4205
4222
|
}
|
|
4206
|
-
commits.sort((
|
|
4223
|
+
commits.sort(byNewestCommit(dateOf));
|
|
4207
4224
|
// The dated commit behind the "who last touched X" answer is the same
|
|
4208
4225
|
// `event` referent the when-shape registers, so either phrasing feeds a
|
|
4209
4226
|
// later temporal comparison. Registered only when the commit carries a date.
|
|
@@ -5371,7 +5388,7 @@ function substituteLastCommitPhrase(graph, query) {
|
|
|
5371
5388
|
const commits = graph.individuals.filter((i) => i.class === "Commit");
|
|
5372
5389
|
if (!commits.length) return q;
|
|
5373
5390
|
const dateOf = (c) => String((c.attributes || []).find((a) => a.key === "date")?.value || "");
|
|
5374
|
-
const newest = [...commits].sort((
|
|
5391
|
+
const newest = [...commits].sort(byNewestCommit(dateOf))[0];
|
|
5375
5392
|
if (!newest) return q;
|
|
5376
5393
|
const out = q.replace(LAST_COMMIT_PHRASE_RE, `commit ${newest.label}`);
|
|
5377
5394
|
const bareTrimmed = out.trim().replace(/[?.!]+$/, "");
|
|
@@ -5559,7 +5576,7 @@ function evalWorldRelation(graph, ast) {
|
|
|
5559
5576
|
// never placed outright.
|
|
5560
5577
|
const pairs = [...stated.values(), ...[...taught.values()].filter((p) => !stated.has(p.subject.id))]
|
|
5561
5578
|
.sort((a, b) => (
|
|
5562
|
-
String(a.subject.id)
|
|
5579
|
+
byCodepoint(String(a.subject.id), String(b.subject.id)) || byCodepoint(String(a.object), String(b.object))
|
|
5563
5580
|
));
|
|
5564
5581
|
return {
|
|
5565
5582
|
compositeKind: "worldRelation",
|
package/src/domain/cli-verbs.mjs
CHANGED
|
@@ -58,12 +58,11 @@ export const CLI_VERBS = [
|
|
|
58
58
|
prose: ["initialize a repo for tmct (default: cwd): .tmct/,"],
|
|
59
59
|
flags: [
|
|
60
60
|
{ flag: "[--force]", prose: ["tmct.toml, .tmct/TOOLS.md (the cold-tool catalog),", "tier-1 corpus seed, provenance record"] },
|
|
61
|
-
{ flag: "[--corpus <id|path>]", prose: ["also seed a corpus — a
|
|
61
|
+
{ flag: "[--corpus <id|path>]", prose: ["also seed a corpus — a bundle name (code|conceptnet|child|", "namenet|general) or a jsonl file path — opt-in, offline, $0"] },
|
|
62
62
|
{ flag: "[--ontology <name|path>]", prose: ["activate+seed an ontology bundle (a recognized name or a path)"] },
|
|
63
63
|
{ flag: "[--lexicon <name|path>]", prose: ["activate a lexicon bundle (recognized name or a path;", "merged read-time, never seeded — see mergedLexiconExtra)"] },
|
|
64
64
|
{ flag: "[--graph <path>]", prose: ["set graph_file/graph_files in tmct.toml (repeatable)"] },
|
|
65
65
|
{ flag: "[--config <path>]", prose: ["write to an alternate tmct.toml location"] },
|
|
66
|
-
{ flag: "[--detect]", prose: ["suggest a tier-2 corpus from the repo's manifests", "(pyproject.toml → python, pom.xml → java); never seeds unasked"] },
|
|
67
66
|
{ flag: "[--with-persona <name>]", prose: ["write an explicit [extensions]/[bias] preset into tmct.toml", "(\"code\" — today's implicit default, made explicit)"] },
|
|
68
67
|
{ flag: "[--persona-size <medium|large>]", prose: ["grow the default \"human\" persona's fact count", "beyond Small (the default): \"medium\" activates", "human-medium.jsonl (~1,608 facts total), \"large\" also", "activates human-large.jsonl (~13,600 facts total,", "with genuine multi-hop hypernym chains) — additive", "size tiers of the SAME bundle, not separate personas"] },
|
|
69
68
|
{ flag: "[--memory-backend <default|memory|sqlite>]", prose: ["write tmct.toml's [memory] backend", "(same flag name as `tmct chat`) — a later `tmct chat`", "in this repo picks it up with no flag needed"] },
|
|
@@ -216,9 +215,9 @@ export const CLI_VERBS = [
|
|
|
216
215
|
mode: "corpus",
|
|
217
216
|
errorLabel: "corpus load",
|
|
218
217
|
usage: "tmct corpus load <band> [--table <name>] [--source <path>] [--dry-run]",
|
|
219
|
-
prose: ["load a shared, read-only corpus band (
|
|
218
|
+
prose: ["load a shared, read-only corpus band (wordnet-complete, or a"],
|
|
220
219
|
flags: [
|
|
221
|
-
{ flag: "[--table <name>]", prose: ["
|
|
220
|
+
{ flag: "[--table <name>]", prose: ["consumer's own) into a DynamoDB row-backend table from a jsonl of", "wire-row-shaped facts (default table from TMCT_DYNAMO_TABLE); a", "source whose digest already matches the band's manifest is a no-op"] },
|
|
222
221
|
{ flag: "[--source <path>]", prose: ["the band's jsonl (a scripts/corpus-bands/ build output, or any jsonl", "in the same wire-row shape)"] },
|
|
223
222
|
{ flag: "[--dry-run]", prose: ["report the row count and source digest without writing anything"] },
|
|
224
223
|
],
|
|
@@ -9,6 +9,11 @@ import { requireInjected } from "./injected.mjs";
|
|
|
9
9
|
|
|
10
10
|
const LABEL_TOKEN_COUNT = 5;
|
|
11
11
|
|
|
12
|
+
// Codepoint order, never localeCompare — hit ids and content tokens trace
|
|
13
|
+
// back to the memory store, and two readers must land on the same order
|
|
14
|
+
// regardless of locale.
|
|
15
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
16
|
+
|
|
12
17
|
/** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
|
|
13
18
|
* hit count). */
|
|
14
19
|
function unionFind(n) {
|
|
@@ -83,7 +88,7 @@ export function groupHits(hits, { overlapMin, store } = {}) {
|
|
|
83
88
|
for (const memberIdx of componentIdx.values()) {
|
|
84
89
|
const members = memberIdx
|
|
85
90
|
.map((i) => byId.get(ids[i]))
|
|
86
|
-
.sort((a, b) => a.id
|
|
91
|
+
.sort((a, b) => byCodepoint(a.id, b.id));
|
|
87
92
|
const memberIds = members.map((m) => m.id);
|
|
88
93
|
|
|
89
94
|
// label tokens: rank by member coverage, then IDF, then token text (deterministic).
|
|
@@ -92,7 +97,7 @@ export function groupHits(hits, { overlapMin, store } = {}) {
|
|
|
92
97
|
for (const t of new Set(tokensById[ids[i]])) coverage.set(t, (coverage.get(t) || 0) + 1);
|
|
93
98
|
}
|
|
94
99
|
const tokens = [...coverage.keys()]
|
|
95
|
-
.sort((a, b) => (coverage.get(b) - coverage.get(a)) || (idf(b) - idf(a)) || a
|
|
100
|
+
.sort((a, b) => (coverage.get(b) - coverage.get(a)) || (idf(b) - idf(a)) || byCodepoint(a, b))
|
|
96
101
|
.slice(0, LABEL_TOKEN_COUNT);
|
|
97
102
|
|
|
98
103
|
groups.push({
|
|
@@ -104,6 +109,6 @@ export function groupHits(hits, { overlapMin, store } = {}) {
|
|
|
104
109
|
});
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
groups.sort((a, b) => a.memberIds[0]
|
|
112
|
+
groups.sort((a, b) => byCodepoint(a.memberIds[0], b.memberIds[0]));
|
|
108
113
|
return groups;
|
|
109
114
|
}
|
|
@@ -16,6 +16,11 @@ import { requireInjected } from "./injected.mjs";
|
|
|
16
16
|
// supply their own copy in the `helpers` bag resolveRelationChase expects.
|
|
17
17
|
const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
18
18
|
|
|
19
|
+
// Codepoint order, never localeCompare — group ids trace back to memory-store
|
|
20
|
+
// block ids, and two readers must land on the same relation order regardless
|
|
21
|
+
// of locale.
|
|
22
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
23
|
+
|
|
19
24
|
// The taught ISA-family predicates (stored edges only, not their transitive closure).
|
|
20
25
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
21
26
|
|
|
@@ -281,7 +286,7 @@ export async function inferRelations(groups, memory, { store } = {}) {
|
|
|
281
286
|
relationNames: relationNameCandidates(rows),
|
|
282
287
|
};
|
|
283
288
|
|
|
284
|
-
const sorted = list.slice().sort((x, y) => x.id
|
|
289
|
+
const sorted = list.slice().sort((x, y) => byCodepoint(x.id, y.id));
|
|
285
290
|
const out = [];
|
|
286
291
|
|
|
287
292
|
for (let i = 0; i < sorted.length; i += 1) {
|
|
@@ -311,6 +316,6 @@ export async function inferRelations(groups, memory, { store } = {}) {
|
|
|
311
316
|
}
|
|
312
317
|
}
|
|
313
318
|
|
|
314
|
-
out.sort((x, y) => x.from
|
|
319
|
+
out.sort((x, y) => byCodepoint(x.from, y.from) || byCodepoint(x.to, y.to) || byCodepoint(x.relation, y.relation));
|
|
315
320
|
return out;
|
|
316
321
|
}
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_MAX_SENTENCES_PER_GROUP = 3;
|
|
10
10
|
|
|
11
|
+
// Codepoint order, never localeCompare — group ids trace back to memory-store
|
|
12
|
+
// block ids, and two readers must land on the same order regardless of locale.
|
|
13
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
14
|
+
|
|
11
15
|
/** Every group id referenced as either side of an asserted relation. */
|
|
12
16
|
function relatedGroupIdsOf(relations) {
|
|
13
17
|
const set = new Set();
|
|
@@ -56,7 +60,7 @@ export function pruneCompletion(state = {}, { maxSentencesPerGroup = DEFAULT_MAX
|
|
|
56
60
|
|
|
57
61
|
const relatedGroupIds = relatedGroupIdsOf(relations);
|
|
58
62
|
|
|
59
|
-
const sortedGroups = groups.slice().sort((a, b) => a.id
|
|
63
|
+
const sortedGroups = groups.slice().sort((a, b) => byCodepoint(a.id, b.id));
|
|
60
64
|
for (const g of sortedGroups) {
|
|
61
65
|
const ranked = Array.isArray(rankedByGroup[g.id]) ? rankedByGroup[g.id] : [];
|
|
62
66
|
const groupFeedsInference = relatedGroupIds.has(g.id);
|
|
@@ -12,6 +12,11 @@ import { requireInjected } from "./injected.mjs";
|
|
|
12
12
|
// (no abbreviation dictionary).
|
|
13
13
|
const SENTENCE_SPLIT_RE = /(?<=[.!?])\s+(?=[A-Z0-9])/;
|
|
14
14
|
|
|
15
|
+
// Codepoint order, never localeCompare — sourceBlockId traces back to the
|
|
16
|
+
// memory store's block ids, and two readers must land on the same rank order
|
|
17
|
+
// regardless of locale.
|
|
18
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Split raw block text into trimmed, non-empty sentences (order-preserving, no dedup).
|
|
17
22
|
*
|
|
@@ -89,7 +94,7 @@ export function rankSentences(group, { overlapMin, query = null, store } = {}) {
|
|
|
89
94
|
});
|
|
90
95
|
|
|
91
96
|
scored.sort((a, b) => b.score - a.score
|
|
92
|
-
|| a.sourceBlockId
|
|
93
|
-
|| a.sentence
|
|
97
|
+
|| byCodepoint(a.sourceBlockId, b.sourceBlockId)
|
|
98
|
+
|| byCodepoint(a.sentence, b.sentence));
|
|
94
99
|
return scored;
|
|
95
100
|
}
|
|
@@ -12,6 +12,10 @@ import DEFAULT_CONFIG from "./config.json" with { type: "json" };
|
|
|
12
12
|
|
|
13
13
|
const DESCRIPTION_FAMILIES = FAMILY_PRIORITY.filter((f) => f !== "isa" && f !== "other");
|
|
14
14
|
|
|
15
|
+
/** Codepoint order, never localeCompare — an ontology root is a class name
|
|
16
|
+
* read off stored facts, so two locales have to name the same roots. */
|
|
17
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
18
|
+
|
|
15
19
|
/** Group the selector's `selected` items by family, preserving each item's
|
|
16
20
|
* ranked order, and return `{ family -> rows[] }` over the fact rows. */
|
|
17
21
|
function rowsByFamily(selected) {
|
|
@@ -82,7 +86,7 @@ export function closerRootsFor(chains, usedRows, count) {
|
|
|
82
86
|
rootRows.get(root).push(row);
|
|
83
87
|
}
|
|
84
88
|
const roots = [...rootCount.keys()]
|
|
85
|
-
.sort((a, b) => (rootCount.get(b) - rootCount.get(a)) || a
|
|
89
|
+
.sort((a, b) => (rootCount.get(b) - rootCount.get(a)) || byCodepoint(a, b))
|
|
86
90
|
.slice(0, Math.max(0, count));
|
|
87
91
|
const rows = [];
|
|
88
92
|
const seen = new Set();
|
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
// the ranking stays auditable.
|
|
26
26
|
|
|
27
27
|
import { SOURCE_PRIOR } from "../memory/trust.mjs";
|
|
28
|
+
import { compareFactsByContent } from "../memory/fact-order.mjs";
|
|
28
29
|
import { clusterSenses } from "../sense-split.mjs";
|
|
30
|
+
|
|
31
|
+
/** Codepoint order, never localeCompare — a cluster label is a class name read
|
|
32
|
+
* off stored facts, so two locales have to land on the same dominant sense. */
|
|
33
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
29
34
|
import DEFAULT_CONFIG from "./config.json" with { type: "json" };
|
|
30
35
|
|
|
31
36
|
// A relation predicate -> the sentence-family key the digest groups it under.
|
|
@@ -148,7 +153,7 @@ export function selectFacts(term, rows, store = {}, opts = {}) {
|
|
|
148
153
|
}
|
|
149
154
|
let dominantLabel = null;
|
|
150
155
|
let bestWeight = -1;
|
|
151
|
-
for (const [label, weight] of [...clusterWeight].sort((a, b) => a[0]
|
|
156
|
+
for (const [label, weight] of [...clusterWeight].sort((a, b) => byCodepoint(a[0], b[0]))) {
|
|
152
157
|
if (weight > bestWeight) { bestWeight = weight; dominantLabel = label; }
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -186,16 +191,17 @@ export function selectFacts(term, rows, store = {}, opts = {}) {
|
|
|
186
191
|
else eligible.push(item);
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
// Breadth-first cut to budget: each family sorted by score (
|
|
190
|
-
//
|
|
191
|
-
// order so the digest covers relations before
|
|
194
|
+
// Breadth-first cut to budget: each family sorted by score (ties broken on
|
|
195
|
+
// the fact's own content, never on the order it arrived), then pulled
|
|
196
|
+
// round-robin in family priority order so the digest covers relations before
|
|
197
|
+
// it repeats one.
|
|
192
198
|
const byFamily = new Map();
|
|
193
199
|
for (const item of eligible) {
|
|
194
200
|
if (!byFamily.has(item.family)) byFamily.set(item.family, []);
|
|
195
201
|
byFamily.get(item.family).push(item);
|
|
196
202
|
}
|
|
197
203
|
for (const list of byFamily.values()) {
|
|
198
|
-
list.sort((a, b) => b.score - a.score || a.row
|
|
204
|
+
list.sort((a, b) => b.score - a.score || compareFactsByContent(a.row, b.row));
|
|
199
205
|
}
|
|
200
206
|
const orderedFamilies = FAMILY_PRIORITY.filter((f) => byFamily.has(f))
|
|
201
207
|
.concat([...byFamily.keys()].filter((f) => !FAMILY_PRIORITY.includes(f)).sort());
|
|
@@ -222,7 +228,7 @@ export function selectFacts(term, rows, store = {}, opts = {}) {
|
|
|
222
228
|
selected.sort((a, b) => {
|
|
223
229
|
const fa = FAMILY_PRIORITY.indexOf(a.family);
|
|
224
230
|
const fb = FAMILY_PRIORITY.indexOf(b.family);
|
|
225
|
-
return (fa - fb) || (b.score - a.score) || a.row
|
|
231
|
+
return (fa - fb) || (b.score - a.score) || compareFactsByContent(a.row, b.row);
|
|
226
232
|
});
|
|
227
233
|
|
|
228
234
|
return {
|