@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
package/src/domain/domain.mjs
CHANGED
|
@@ -37,10 +37,17 @@ const optionalTerm = (value) => {
|
|
|
37
37
|
return t === "" ? undefined : t;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/** Codepoint order, never localeCompare. Every string this file sorts came out
|
|
41
|
+
* of the fact/Rule store. The planner walks actions, signatures and state rows
|
|
42
|
+
* in exactly the order these comparators leave them, so a locale-sensitive
|
|
43
|
+
* compare would let two machines holding one taught domain return different
|
|
44
|
+
* plans from it. */
|
|
45
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
46
|
+
|
|
40
47
|
const rowSort = (a, b) =>
|
|
41
|
-
a.subject
|
|
42
|
-
a.predicate
|
|
43
|
-
a.object
|
|
48
|
+
byCodepoint(a.subject, b.subject) ||
|
|
49
|
+
byCodepoint(a.predicate, b.predicate) ||
|
|
50
|
+
byCodepoint(a.object, b.object);
|
|
44
51
|
|
|
45
52
|
const normRow = (row) => ({
|
|
46
53
|
subject: normTerm(row.subject),
|
|
@@ -108,13 +115,13 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
108
115
|
});
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
|
-
const actions = [...byName.values()].sort((a, b) => a.name
|
|
118
|
+
const actions = [...byName.values()].sort((a, b) => byCodepoint(a.name, b.name));
|
|
112
119
|
for (const action of actions) {
|
|
113
120
|
action.signatures.sort((a, b) =>
|
|
114
|
-
a.subjectClass
|
|
115
|
-
action.preconds.sort((a, b) => JSON.stringify(a)
|
|
116
|
-
action.effects.sort((a, b) => JSON.stringify(a)
|
|
117
|
-
action.constraints.sort((a, b) => JSON.stringify(a)
|
|
121
|
+
byCodepoint(a.subjectClass, b.subjectClass) || byCodepoint(a.targetClass, b.targetClass));
|
|
122
|
+
action.preconds.sort((a, b) => byCodepoint(JSON.stringify(a), JSON.stringify(b)));
|
|
123
|
+
action.effects.sort((a, b) => byCodepoint(JSON.stringify(a), JSON.stringify(b)));
|
|
124
|
+
action.constraints.sort((a, b) => byCodepoint(JSON.stringify(a), JSON.stringify(b)));
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
// Class membership from typing edges. A member is a subject with a typing
|
|
@@ -21,9 +21,18 @@ import {
|
|
|
21
21
|
SUBCLASS_PREDICATE, ON_PROPERTY_PREDICATE, SOME_VALUES_FROM_PREDICATE, TYPE_PREDICATE,
|
|
22
22
|
DEFAULT_MAX_ENVIRONMENTS, buildCardinalityRestrictions,
|
|
23
23
|
} from "./syllogise.mjs";
|
|
24
|
+
import { compareFactsByContent } from "./memory/fact-order.mjs";
|
|
24
25
|
|
|
25
26
|
const SEP = "␟"; // an in-key separator no fact term can contain — same convention as syllogise.mjs's own SEP
|
|
26
27
|
|
|
28
|
+
// Codepoint order, never localeCompare — a stored row's id is read on whatever
|
|
29
|
+
// machine holds the graph, and two locales have to land on the same order.
|
|
30
|
+
const byId = (a, b) => {
|
|
31
|
+
const ka = String(a.id);
|
|
32
|
+
const kb = String(b.id);
|
|
33
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
34
|
+
};
|
|
35
|
+
|
|
27
36
|
/** The two reserved concept names every EL derivation is built from. Neither
|
|
28
37
|
* can collide with a stored term: normFactTerm never produces them from a
|
|
29
38
|
* class noun, and normalizeElTBox drops any row that literally names one. */
|
|
@@ -78,7 +87,7 @@ const isCardinalityPredicate = (p) => CARDINALITY_PREDICATES.has(lower(p));
|
|
|
78
87
|
*/
|
|
79
88
|
export function normalizeElTBox(rows, { budget = 500 } = {}) {
|
|
80
89
|
const input = (Array.isArray(rows) ? rows : []).filter((r) => r && r.id && r.subject && r.predicate && r.object !== undefined && r.object !== null);
|
|
81
|
-
const sorted = [...input].sort(
|
|
90
|
+
const sorted = [...input].sort(byId);
|
|
82
91
|
const truncated = sorted.length > budget;
|
|
83
92
|
const used = truncated ? sorted.slice(0, budget) : sorted;
|
|
84
93
|
|
|
@@ -216,7 +225,7 @@ export function normalizeElTBox(rows, { budget = 500 } = {}) {
|
|
|
216
225
|
for (const role of [...transitiveRoleRow.keys()].sort()) {
|
|
217
226
|
roleAxioms.push({ kind: "transitive", role, from: [transitiveRoleRow.get(role).id] });
|
|
218
227
|
}
|
|
219
|
-
for (const r of [...subPropertyRows].sort(
|
|
228
|
+
for (const r of [...subPropertyRows].sort(compareFactsByContent)) {
|
|
220
229
|
roleAxioms.push({ kind: "sub", sub: r.subject, sup: r.object, from: [r.id] });
|
|
221
230
|
}
|
|
222
231
|
|
|
@@ -60,6 +60,7 @@ export const FACT_PREDICATE_PHRASES = Object.freeze({
|
|
|
60
60
|
"mgx:consumes": "eats",
|
|
61
61
|
"mgx:vision-radius": "sees within",
|
|
62
62
|
"mgx:guards": "guards",
|
|
63
|
+
"mgx:attributedTo": "is attributed to",
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
/** The closed participle set the relational teach frames read as "X is
|
|
@@ -70,6 +71,23 @@ export const FACT_PREDICATE_PHRASES = Object.freeze({
|
|
|
70
71
|
* them back into English, so both read the one vocabulary. */
|
|
71
72
|
export const TEACH_PARTICIPLE_SRC = "connected|related|associated|linked|based|derived|composed|made|used|known|located|found|involved|concerned";
|
|
72
73
|
|
|
74
|
+
/** The participles a news report's agentless passive states its subject's own
|
|
75
|
+
* condition with — "is banned from", "was deported to". One per verb in the
|
|
76
|
+
* extractor's closed newswire event band, so the same list that decides which
|
|
77
|
+
* events read also decides which passives read back as English. Kept apart
|
|
78
|
+
* from TEACH_PARTICIPLE_SRC because the teach lane parses that list into its
|
|
79
|
+
* own frames and nothing should widen those by writing here. */
|
|
80
|
+
export const NEWS_PASSIVE_PARTICIPLE_SRC = [
|
|
81
|
+
"hit", "struck", "killed", "injured", "wounded", "damaged", "destroyed", "devastated",
|
|
82
|
+
"banned", "halted", "blocked", "barred", "suspended", "imposed",
|
|
83
|
+
"arrested", "detained", "jailed", "charged", "convicted", "sentenced", "deported", "released", "freed",
|
|
84
|
+
"elected", "appointed", "ousted", "overthrown",
|
|
85
|
+
"signed", "adopted", "approved", "rejected", "vetoed",
|
|
86
|
+
"launched", "unveiled", "seized", "captured", "invaded", "attacked", "bombed", "targeted",
|
|
87
|
+
"discovered", "uncovered", "rescued", "evacuated",
|
|
88
|
+
"sparked", "triggered", "caused", "forced", "deployed", "restored", "expanded",
|
|
89
|
+
].join("|");
|
|
90
|
+
|
|
73
91
|
/** The MECHANICAL fallback for a predicate the table has no curated entry
|
|
74
92
|
* for — specifically the minted "mgx:<lemma>" predicates ("mgx:eat",
|
|
75
93
|
* "mgx:drive", …) — the mechanical INVERSE of the naive -s/-es/-ies fold the
|
|
@@ -132,14 +150,35 @@ const SINGULAR_NOUNS_ENDING_S = new Set([
|
|
|
132
150
|
"news", "physics", "species", "series", "means", "measles", "mathematics", "politics", "economics",
|
|
133
151
|
]);
|
|
134
152
|
|
|
153
|
+
/** How much word has to sit in front of one of those nouns before the whole
|
|
154
|
+
* reads as a COMPOUND built on it. An English compound takes its number from
|
|
155
|
+
* its rightmost element, so "hackernews", "subspecies", "miniseries" and
|
|
156
|
+
* "geopolitics" are all as singular as the noun they end in, and the table
|
|
157
|
+
* above covers the family rather than one site's name. Three characters is
|
|
158
|
+
* where the real first elements start ("sub", "geo", "mini", "hacker") and
|
|
159
|
+
* where the words that merely END in one of those nouns stop: "sinews",
|
|
160
|
+
* "renews" and "demeans" leave two characters in front and stay plural. */
|
|
161
|
+
const COMPOUND_FIRST_ELEMENT_MIN_CHARS = 3;
|
|
162
|
+
|
|
163
|
+
/** Does a head noun end in one of the singular "-s" nouns above, as itself or
|
|
164
|
+
* as the last element of a compound built on it? */
|
|
165
|
+
function endsInSingularNounEndingS(head) {
|
|
166
|
+
for (const noun of SINGULAR_NOUNS_ENDING_S) {
|
|
167
|
+
if (head === noun) return true;
|
|
168
|
+
if (head.endsWith(noun) && head.length - noun.length >= COMPOUND_FIRST_ELEMENT_MIN_CHARS) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
/**
|
|
136
174
|
* Is a stored fact's SUBJECT text grammatically plural, for the one thing
|
|
137
175
|
* this file needs it for: choosing a minted verb's surface form. Reads the
|
|
138
176
|
* HEAD noun only — the word right after any leading article, before a
|
|
139
177
|
* trailing "of ..." phrase, so "the group of scientists" agrees on "group"
|
|
140
178
|
* ("the group of scientists reports"), not "scientists". From there: the
|
|
141
|
-
* closed irregular table above, then the
|
|
142
|
-
*
|
|
179
|
+
* closed irregular table above, then the singular "-s" nouns and the compounds
|
|
180
|
+
* they head, then the regular "-s" suffix, same naive-morphology trade
|
|
181
|
+
* thirdPersonSingularSurface already takes above. A
|
|
143
182
|
* subject this can't read (empty, or a plural-invariant noun like "sheep")
|
|
144
183
|
* defaults to singular — English's own unmarked form, and also this file's
|
|
145
184
|
* pre-existing default for every caller that passes no subject at all.
|
|
@@ -148,7 +187,7 @@ export function isSubjectPlural(subject) {
|
|
|
148
187
|
const head = String(subject || "").trim().replace(/^(?:the|a|an)\s+/i, "").split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
149
188
|
if (!head) return false;
|
|
150
189
|
if (IRREGULAR_PLURAL_NOUNS.has(head)) return true;
|
|
151
|
-
if (
|
|
190
|
+
if (endsInSingularNounEndingS(head)) return false;
|
|
152
191
|
return /[a-z]s$/.test(head) && !/ss$/.test(head);
|
|
153
192
|
}
|
|
154
193
|
|
|
@@ -204,7 +243,7 @@ export function predicatePhrase(predicate, subject) {
|
|
|
204
243
|
// a participle + preposition renders as its copula surface: mgx:connected-with
|
|
205
244
|
// -> "is connected with" (the participle is already a participle, so no 3sg
|
|
206
245
|
// fold — "connecteds" isn't a word)
|
|
207
|
-
const part = new RegExp(`^mgx:(${TEACH_PARTICIPLE_SRC})-([a-z]+)$`, "i").exec(p);
|
|
246
|
+
const part = new RegExp(`^mgx:(${TEACH_PARTICIPLE_SRC}|${NEWS_PASSIVE_PARTICIPLE_SRC})-([a-z]+)$`, "i").exec(p);
|
|
208
247
|
if (part) return `is ${part[1].toLowerCase()} ${part[2].toLowerCase()}`;
|
|
209
248
|
// a shared-attribute predicate: mgx:same-goal-as -> "has the same goal as"
|
|
210
249
|
const same = /^mgx:same-([a-z]+)-as$/i.exec(p);
|
|
@@ -218,10 +257,49 @@ export function predicatePhrase(predicate, subject) {
|
|
|
218
257
|
const verb = isSubjectPlural(subject) ? minted[1] : thirdPersonSingularSurface(minted[1]);
|
|
219
258
|
return `${verb}${minted[2] ? ` ${minted[2]}` : ""}`;
|
|
220
259
|
}
|
|
260
|
+
// The lexicon's own minted verb predicates come pre-inflected: it builds
|
|
261
|
+
// "tmct:<3sg>[<Prep>]" from a declared verb, so "release" is stored as
|
|
262
|
+
// "tmct:releases" and "rely" + "on" as "tmct:reliesOn". Singular subjects
|
|
263
|
+
// read that surface as it stands. A plural one takes the bare form through
|
|
264
|
+
// baseVerbSurface, the documented inverse of the fold that made it, so
|
|
265
|
+
// "rescuers release" comes out of the same rule that gives "rescuers report".
|
|
266
|
+
// The camel-cased preposition is its own word in a sentence.
|
|
267
|
+
const declared = /^tmct:([a-z]+)([A-Z][a-z]+)?$/.exec(p);
|
|
268
|
+
if (declared) {
|
|
269
|
+
const verb = isSubjectPlural(subject) ? baseVerbSurface(declared[1]) : declared[1];
|
|
270
|
+
return `${verb}${declared[2] ? ` ${declared[2].toLowerCase()}` : ""}`;
|
|
271
|
+
}
|
|
221
272
|
const colon = p.indexOf(":");
|
|
222
273
|
return colon === -1 ? p : p.slice(colon + 1);
|
|
223
274
|
}
|
|
224
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The verb a stored predicate states an ACT with, as `{ lemma, particle }`,
|
|
278
|
+
* or null when the predicate states anything else. `mgx:free` reads
|
|
279
|
+
* `{ lemma: "free", particle: "" }`, the lexicon's pre-inflected
|
|
280
|
+
* `tmct:releases` reads `{ lemma: "release", particle: "" }`, and
|
|
281
|
+
* `mgx:strike-near` / `tmct:reliesOn` carry their particle beside the lemma.
|
|
282
|
+
*
|
|
283
|
+
* The branches below mirror predicatePhrase's own, in its order, so anything
|
|
284
|
+
* that reads as a curated phrase, a negation, a comparative, a passive
|
|
285
|
+
* participle or a shared attribute answers null here rather than a verb it
|
|
286
|
+
* never renders as. Two rows minted down different paths — one through the
|
|
287
|
+
* lexicon's declared verbs, one from a bare lemma — reduce to the same answer,
|
|
288
|
+
* which is what lets a caller ask whether they state their act in one word.
|
|
289
|
+
*/
|
|
290
|
+
export function predicateVerb(predicate) {
|
|
291
|
+
const p = String(predicate ?? "");
|
|
292
|
+
if (FACT_PREDICATE_PHRASES[p] || p.startsWith("mgxneg:")) return null;
|
|
293
|
+
if (/^mgx:[a-z]+(?:-[a-z]+)*-than$/i.test(p)) return null;
|
|
294
|
+
if (new RegExp(`^mgx:(?:${TEACH_PARTICIPLE_SRC}|${NEWS_PASSIVE_PARTICIPLE_SRC})-[a-z]+$`, "i").test(p)) return null;
|
|
295
|
+
if (/^mgx:same-[a-z]+-as$/i.test(p)) return null;
|
|
296
|
+
const minted = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
297
|
+
if (minted) return { lemma: minted[1].toLowerCase(), particle: minted[2]?.toLowerCase() ?? "" };
|
|
298
|
+
const declared = /^tmct:([a-z]+)([A-Z][a-z]+)?$/.exec(p);
|
|
299
|
+
if (declared) return { lemma: baseVerbSurface(declared[1]).toLowerCase(), particle: declared[2]?.toLowerCase() ?? "" };
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
225
303
|
/** "a heart has a valve" from one { subject, predicate, object } fact row —
|
|
226
304
|
* "scientists report a finding" for a plural row.subject, off the same
|
|
227
305
|
* agreement rule in predicatePhrase above. */
|
|
@@ -244,6 +322,7 @@ export const FINDING_CAVEATS = Object.freeze({
|
|
|
244
322
|
"clause-fallback": "(read from a clause fragment)",
|
|
245
323
|
"pronoun-carry": "(subject carried from the previous sentence)",
|
|
246
324
|
"identifier-token": "(identifier token)",
|
|
325
|
+
"reported-speech": "(read from reported speech)",
|
|
247
326
|
});
|
|
248
327
|
|
|
249
328
|
/**
|
|
@@ -276,10 +355,13 @@ export function findingCaveat(finding) {
|
|
|
276
355
|
export function phraseRendererSource() {
|
|
277
356
|
return [
|
|
278
357
|
`const TEACH_PARTICIPLE_SRC = ${JSON.stringify(TEACH_PARTICIPLE_SRC)};`,
|
|
358
|
+
`const NEWS_PASSIVE_PARTICIPLE_SRC = ${JSON.stringify(NEWS_PASSIVE_PARTICIPLE_SRC)};`,
|
|
279
359
|
`const thirdPersonSingularSurface = ${thirdPersonSingularSurface};`,
|
|
280
360
|
`const baseVerbSurface = ${baseVerbSurface};`,
|
|
281
361
|
`const IRREGULAR_PLURAL_NOUNS = new Set(${JSON.stringify([...IRREGULAR_PLURAL_NOUNS])});`,
|
|
282
362
|
`const SINGULAR_NOUNS_ENDING_S = new Set(${JSON.stringify([...SINGULAR_NOUNS_ENDING_S])});`,
|
|
363
|
+
`const COMPOUND_FIRST_ELEMENT_MIN_CHARS = ${COMPOUND_FIRST_ELEMENT_MIN_CHARS};`,
|
|
364
|
+
`const endsInSingularNounEndingS = ${endsInSingularNounEndingS};`,
|
|
283
365
|
`const isSubjectPlural = ${isSubjectPlural};`,
|
|
284
366
|
`const predicatePhrase = ${predicatePhrase};`,
|
|
285
367
|
`const factSentence = ${factSentence};`,
|
package/src/domain/hash.mjs
CHANGED
|
@@ -104,6 +104,14 @@ const TEXT_CAP = 2000; // an utterance's stored text (a whole answer fits; a p
|
|
|
104
104
|
* on which module did the writing. */
|
|
105
105
|
export const normText = (t) => String(t ?? "").replace(/\s+/g, " ").trim().slice(0, TEXT_CAP);
|
|
106
106
|
|
|
107
|
+
// A term that is exactly a fact group id ("fact:" + the 16 lowercase hex
|
|
108
|
+
// digits factIdFor mints — an 8-byte SHA-256 truncation, see factIdFor below)
|
|
109
|
+
// must survive normFactTerm whole. Without this guard the generic CURIE
|
|
110
|
+
// strip below removes "fact:" from it same as any other prefix, so a
|
|
111
|
+
// reference TO a fact and a taught word become the same term and collide
|
|
112
|
+
// on one id. No existing corpus/vocabulary term takes this exact shape.
|
|
113
|
+
const FACT_ID_TERM_RE = /^fact:[0-9a-f]{16}$/;
|
|
114
|
+
|
|
107
115
|
/** Normalize a fact TERM (subject/object) so every writer converges on one
|
|
108
116
|
* spelling: ConceptNet's /c/en/foo_bar, tmct:Foo_bar, and bare "Foo bar" all
|
|
109
117
|
* become "foo bar". Also strips a leading "the"/"a"/"an" (idempotent — safe
|
|
@@ -111,6 +119,7 @@ export const normText = (t) => String(t ?? "").replace(/\s+/g, " ").trim().slice
|
|
|
111
119
|
* is meaningful controlled vocabulary. */
|
|
112
120
|
export function normFactTerm(t) {
|
|
113
121
|
let s = normText(t);
|
|
122
|
+
if (FACT_ID_TERM_RE.test(s.toLowerCase())) return s.toLowerCase();
|
|
114
123
|
s = s.replace(/^\/c\/[a-z]{2,3}\//i, "");
|
|
115
124
|
s = s.replace(/^[a-z][\w.-]*:/i, "");
|
|
116
125
|
s = s.replace(/_/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// computeTrust. `biasByBundle` is the `[bias]` table from tmct.toml. CRITICAL:
|
|
3
3
|
// bias only REORDERS a hit list — it must never drop or hide one.
|
|
4
4
|
|
|
5
|
+
import { compareFactsByContent } from "./fact-order.mjs";
|
|
6
|
+
|
|
5
7
|
/** Matches a corpus-kind Source id ("src:corpus:<bundleName>"); anything else
|
|
6
8
|
* is not a corpus bundle and ranks at the neutral bias of 1. */
|
|
7
9
|
const CORPUS_SOURCE_RE = /^src:corpus:(.+)$/;
|
|
@@ -23,12 +25,14 @@ export function biasForRow(row, biasByBundle = {}) {
|
|
|
23
25
|
return Math.max(...ids.map((id) => biasForSourceId(id, biasByBundle)));
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
/** Rank fact rows by bias (desc), then trust (desc), then
|
|
27
|
-
*
|
|
28
|
+
/** Rank fact rows by bias (desc), then trust (desc), then content order —
|
|
29
|
+
* reorders only, never drops a row. The last step is content and not array
|
|
30
|
+
* index on purpose: an index tiebreak is arrival order, so two peers holding
|
|
31
|
+
* one fact set would rank it two ways. */
|
|
28
32
|
export function rankByBiasThenTrust(rows, biasByBundle = {}) {
|
|
29
33
|
const list = Array.isArray(rows) ? rows : [];
|
|
30
34
|
return list
|
|
31
|
-
.map((row
|
|
32
|
-
.sort((a, b) => (b.bias - a.bias) || ((b.row?.trust ?? 0) - (a.row?.trust ?? 0)) || (a.
|
|
35
|
+
.map((row) => ({ row, bias: biasForRow(row, biasByBundle) }))
|
|
36
|
+
.sort((a, b) => (b.bias - a.bias) || ((b.row?.trust ?? 0) - (a.row?.trust ?? 0)) || compareFactsByContent(a.row, b.row))
|
|
33
37
|
.map((x) => x.row);
|
|
34
38
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// Pure and import-free of core.mjs, exactly like trust.mjs beside it.
|
|
20
20
|
|
|
21
21
|
import { findIsaChain, buildSubClassSuccessors, SUBCLASS_PREDICATE, TYPE_PREDICATE } from "../syllogise.mjs";
|
|
22
|
+
import { compareFactsByContent } from "./fact-order.mjs";
|
|
22
23
|
|
|
23
24
|
/** The negative-polarity CURIE prefix. A separate prefix, never an
|
|
24
25
|
* "mgx:not-<lemma>" mint: fact-phrase.mjs's predicatePhrase reads "mgx:not-fly" as
|
|
@@ -74,7 +75,7 @@ export const NEG_CAPABLE_OF_PREDICATE = negatedPredicate(CAPABLE_OF_PREDICATE);
|
|
|
74
75
|
* order it lists them in. One constant each, in one place, so the verbosity of
|
|
75
76
|
* every case-4 answer is tuned by editing two lines. */
|
|
76
77
|
export const CAPABILITY_REPORT_CAP = 6;
|
|
77
|
-
const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) ||
|
|
78
|
+
const byTrustThenName = (a, b) => (b.trust || 0) - (a.trust || 0) || compareFactsByContent(a, b);
|
|
78
79
|
|
|
79
80
|
const asSet = (v) => (v instanceof Set ? v : new Set(Array.isArray(v) ? v : [v]));
|
|
80
81
|
|
|
@@ -155,7 +156,9 @@ export function resolveCapabilityPolarity(subject, object, facts, { maxHops = 3
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// hop count first, trust only within a rank — the whole point of the design
|
|
158
|
-
candidates.sort((a, b) => a.hops - b.hops
|
|
159
|
+
candidates.sort((a, b) => a.hops - b.hops
|
|
160
|
+
|| (b.fact.trust || 0) - (a.fact.trust || 0)
|
|
161
|
+
|| compareFactsByContent(a.fact, b.fact));
|
|
159
162
|
const hops = candidates[0].hops;
|
|
160
163
|
const winning = candidates.filter((c) => c.hops === hops);
|
|
161
164
|
const positive = winning.filter((c) => c.polarity === "positive").map((c) => c.fact);
|
|
@@ -198,19 +201,22 @@ export function capabilityBaseRate(subject, object, facts, { maxHops = 3 } = {})
|
|
|
198
201
|
const rows = Array.isArray(facts) ? facts : [];
|
|
199
202
|
const isaRows = rows.filter((f) => f.predicate === SUBCLASS_PREDICATE || f.predicate === TYPE_PREDICATE);
|
|
200
203
|
|
|
201
|
-
|
|
204
|
+
// The class the whole report is about, so it cannot come from whichever
|
|
205
|
+
// parent row happened to be stored first.
|
|
206
|
+
const parents = isaRows.filter((f) => subjects.has(f.subject)).sort(compareFactsByContent).map((f) => f.object);
|
|
202
207
|
if (!parents.length) return null;
|
|
203
208
|
const klass = parents[0];
|
|
204
209
|
|
|
205
210
|
const siblings = [...new Set(
|
|
206
|
-
isaRows.filter((f) => f.object === klass && !subjects.has(f.subject))
|
|
211
|
+
isaRows.filter((f) => f.object === klass && !subjects.has(f.subject))
|
|
212
|
+
.sort(compareFactsByContent).map((f) => f.subject),
|
|
207
213
|
)];
|
|
208
214
|
|
|
209
215
|
const capabilityOf = (name) => {
|
|
210
|
-
const hit = rows.
|
|
216
|
+
const hit = rows.filter(
|
|
211
217
|
(f) => f.subject === name && objects.has(f.object)
|
|
212
218
|
&& (f.predicate === CAPABLE_OF_PREDICATE || f.predicate === NEG_CAPABLE_OF_PREDICATE),
|
|
213
|
-
);
|
|
219
|
+
).sort(compareFactsByContent)[0];
|
|
214
220
|
if (!hit) return { name, polarity: "unknown", fact: null };
|
|
215
221
|
return { name, polarity: hit.predicate === NEG_CAPABLE_OF_PREDICATE ? "negative" : "positive", fact: hit };
|
|
216
222
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// memory/fact-order.mjs — the order a fact listing falls back to once ranking
|
|
2
|
+
// runs out of things to say. Every rank over fact rows (bias, trust, relevance)
|
|
3
|
+
// ends in ties, and a stable sort settles a tie by array index, which is
|
|
4
|
+
// arrival order: two peers holding one fact set then render the same facts on
|
|
5
|
+
// different lines.
|
|
6
|
+
//
|
|
7
|
+
// The tiebreak here is a pure function of the fact's own content — the same
|
|
8
|
+
// (subject, predicate, object) triple hash.mjs content-addresses a Fact by,
|
|
9
|
+
// plus the provenance that separates two sources asserting one triple. NUL
|
|
10
|
+
// delimits the parts for hash.mjs's reason: it never occurs inside a
|
|
11
|
+
// normalized term or predicate, so "a b|c" and "a|b c" cannot collide on one
|
|
12
|
+
// key. Codepoint order throughout, never localeCompare — two locales have to
|
|
13
|
+
// land on the same order.
|
|
14
|
+
//
|
|
15
|
+
// The store's own precedent is p2p-room.mjs's sortFactIndividualsById, which
|
|
16
|
+
// sorts Fact individuals by content-addressed id after every merge for exactly
|
|
17
|
+
// this reason. This is that discipline carried through to the read side.
|
|
18
|
+
|
|
19
|
+
/** A fact row's content-derived sort key. */
|
|
20
|
+
export const factOrderKey = (f) => [
|
|
21
|
+
f?.subject ?? "", f?.predicate ?? "", f?.object ?? "", f?.provenance ?? "",
|
|
22
|
+
].join("\0");
|
|
23
|
+
|
|
24
|
+
/** Order two fact rows by content. The last comparison in any fact ranking. */
|
|
25
|
+
export function compareFactsByContent(a, b) {
|
|
26
|
+
const ka = factOrderKey(a);
|
|
27
|
+
const kb = factOrderKey(b);
|
|
28
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
29
|
+
}
|
|
@@ -44,6 +44,9 @@ const MERGE_PREDICATE_STEMS = [
|
|
|
44
44
|
"mgx:createdBy",
|
|
45
45
|
"mgx:mannerOf", "mgx:relatedTo", "mgx:synonym", "mgx:antonym", "mgx:similarTo", "mgx:symbolOf",
|
|
46
46
|
"mgx:knows-about",
|
|
47
|
+
// One claim can be attributed to many speakers at once. Two outlets naming two
|
|
48
|
+
// different people corroborate the claim; they do not disagree about it.
|
|
49
|
+
"mgx:attributedTo",
|
|
47
50
|
];
|
|
48
51
|
|
|
49
52
|
export const MERGE_PREDICATES = new Set(
|