@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
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 {
|
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
|
}
|