@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
|
@@ -2,48 +2,6 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"generated": "by corpus/tier2/generate.mjs",
|
|
4
4
|
"corpuses": [
|
|
5
|
-
{
|
|
6
|
-
"id": "aws",
|
|
7
|
-
"kind": "domain",
|
|
8
|
-
"description": "Amazon Web Services core services and primitives (S3, Lambda, DynamoDB, EC2, IAM, SQS) mapped to general cloud/CS concepts.",
|
|
9
|
-
"source": {
|
|
10
|
-
"kind": "curated",
|
|
11
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
12
|
-
},
|
|
13
|
-
"file": "aws.jsonl",
|
|
14
|
-
"facts": 39,
|
|
15
|
-
"bytes": 4802,
|
|
16
|
-
"sha256": "7ab3e656bc4bc717be3c50d97af030e8592d6597cde182b15390a1dfaca9755a",
|
|
17
|
-
"license": "MPL-2.0"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": "python",
|
|
21
|
-
"kind": "language",
|
|
22
|
-
"description": "Python language constructs and stdlib types mapped to the shared CS concept vocabulary (list->array, dict->hash table, …).",
|
|
23
|
-
"source": {
|
|
24
|
-
"kind": "curated",
|
|
25
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
26
|
-
},
|
|
27
|
-
"file": "python.jsonl",
|
|
28
|
-
"facts": 30,
|
|
29
|
-
"bytes": 3794,
|
|
30
|
-
"sha256": "009245d024357bc4285e967fba486e32349ccec8fbe2796ba13011cb2be4c19c",
|
|
31
|
-
"license": "MPL-2.0"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"id": "java",
|
|
35
|
-
"kind": "language",
|
|
36
|
-
"description": "Java language and JVM constructs mapped to the shared CS concept vocabulary (ArrayList->list, HashMap->hash table, …).",
|
|
37
|
-
"source": {
|
|
38
|
-
"kind": "curated",
|
|
39
|
-
"tool": "corpus/tier2/generate.mjs"
|
|
40
|
-
},
|
|
41
|
-
"file": "java.jsonl",
|
|
42
|
-
"facts": 31,
|
|
43
|
-
"bytes": 3920,
|
|
44
|
-
"sha256": "d089426833c393f6f1574fe75c4ac94a1483ee80f72f79bd7fd32aaaa24a6e44",
|
|
45
|
-
"license": "MPL-2.0"
|
|
46
|
-
},
|
|
47
5
|
{
|
|
48
6
|
"id": "general",
|
|
49
7
|
"kind": "domain",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.20",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -120,6 +120,8 @@
|
|
|
120
120
|
"probe:news-sources": "node scripts/probe-news-sources.mjs",
|
|
121
121
|
"bench:news": "node scripts/news-bench/run.mjs --seed=xl",
|
|
122
122
|
"bench:news:fast": "node scripts/news-bench/run.mjs --seed=fixture",
|
|
123
|
+
"bench:news:iterate": "node scripts/news-bench/iterate.mjs",
|
|
124
|
+
"bench:inputs": "node scripts/news-bench/ensure-bench-inputs.mjs",
|
|
123
125
|
"check:links": "node scripts/check-links.mjs",
|
|
124
126
|
"check:pii": "node scripts/pii-lint.mjs",
|
|
125
127
|
"check:pack": "node scripts/check-pack-manifest.mjs",
|
|
@@ -138,14 +140,14 @@
|
|
|
138
140
|
"chat:repo": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --repo",
|
|
139
141
|
"chat:plain": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --plain",
|
|
140
142
|
"chat:narrate": "node --disable-warning=ExperimentalWarning bin/tmct.mjs chat --narrate",
|
|
141
|
-
"init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
143
|
+
"init": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet",
|
|
142
144
|
"init:small": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init",
|
|
143
145
|
"init:sqlite": "npm run init:small",
|
|
144
146
|
"init:persona:human": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona human",
|
|
145
147
|
"init:persona:empty": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --with-persona empty",
|
|
146
148
|
"init:large": "npm run init",
|
|
147
|
-
"init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
148
|
-
"init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus
|
|
149
|
+
"init:xl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-xl && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus child",
|
|
150
|
+
"init:xxl": "node --disable-warning=ExperimentalWarning bin/tmct.mjs init --persona-size large && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus code && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus conceptnet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus wordnet-full && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus namenet && node --disable-warning=ExperimentalWarning bin/tmct.mjs import --corpus child",
|
|
149
151
|
"memory": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory",
|
|
150
152
|
"memory:verbose": "node --disable-warning=ExperimentalWarning bin/tmct.mjs memory --verbose",
|
|
151
153
|
"syllogise": "node --disable-warning=ExperimentalWarning bin/tmct.mjs syllogise",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// corpus/child-seed.mjs — the BULK reader for the shipped CHILD triples pack
|
|
2
|
+
// (corpus/child/), used when the pack seeds a store as an ordinary corpus band.
|
|
3
|
+
// src/adapters/corpus/child-pack.mjs is the other half: one term at a time, on a
|
|
4
|
+
// miss, for the clean-miss cascade. This one walks every shard once.
|
|
5
|
+
//
|
|
6
|
+
// The shards already carry tmct-vocabulary triples ({subject, predicate,
|
|
7
|
+
// object}), so there is no conceptnet-map.toml step here — the rows come out of
|
|
8
|
+
// the pack build already mapped.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { gunzipSync } from "node:zlib";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { CHILD_SHARD_COUNT, isChildFactsRow } from "../../domain/child-pack.mjs";
|
|
14
|
+
import { appendNewFacts, preferThenLimit } from "./conceptnet.mjs";
|
|
15
|
+
|
|
16
|
+
/** Every shard basename the pack can hold, in order — the naming contract
|
|
17
|
+
* shardNameFor writes, walked rather than listed, so this reader needs no
|
|
18
|
+
* directory listing and runs in a browser bundle. */
|
|
19
|
+
function shardBasenames() {
|
|
20
|
+
return Array.from(
|
|
21
|
+
{ length: CHILD_SHARD_COUNT },
|
|
22
|
+
(_, i) => `child-${i.toString(16).padStart(2, "0")}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Every distinct triple in `packDir`'s shards, in shard then row order, as
|
|
27
|
+
* appendFact-shaped facts. A term appears in one shard as a subject and in
|
|
28
|
+
* another as an object, so the same edge arrives twice — the first spelling
|
|
29
|
+
* wins and the duplicate is dropped here, before the store ever sees it.
|
|
30
|
+
* Absent or malformed shards contribute nothing rather than throwing, the same
|
|
31
|
+
* tolerance the per-term loader gives. */
|
|
32
|
+
export function loadChildPackFacts(packDir, provenancePrefix = "corpus:child") {
|
|
33
|
+
const facts = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const shard of shardBasenames()) {
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = gunzipSync(readFileSync(join(packDir, "shards", `${shard}.jsonl.gz`))).toString("utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const line of body.split("\n")) {
|
|
43
|
+
if (!line.trim()) continue;
|
|
44
|
+
let row;
|
|
45
|
+
try {
|
|
46
|
+
row = JSON.parse(line);
|
|
47
|
+
} catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!isChildFactsRow(row)) continue;
|
|
51
|
+
for (const f of row.facts) {
|
|
52
|
+
const key = `${f.subject} ${f.predicate} ${f.object}`;
|
|
53
|
+
if (seen.has(key)) continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
facts.push({
|
|
56
|
+
subject: f.subject,
|
|
57
|
+
predicate: f.predicate,
|
|
58
|
+
object: f.object,
|
|
59
|
+
provenance: `${provenancePrefix} ${row.term}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return facts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Seed `packDir`'s triples into `dir`'s memory through the same idempotent
|
|
68
|
+
* append every other corpus band uses. `limit`/`prefer` behave exactly as they
|
|
69
|
+
* do for a slice-shaped band. Returns { appended, skipped, total }. */
|
|
70
|
+
export async function seedChildPack(dir, { packDir, provenancePrefix, limit, prefer } = {}) {
|
|
71
|
+
const facts = preferThenLimit(loadChildPackFacts(packDir, provenancePrefix), prefer, limit);
|
|
72
|
+
const { appended, skipped } = await appendNewFacts(dir, facts);
|
|
73
|
+
return { appended, skipped, total: facts.length };
|
|
74
|
+
}
|
|
@@ -25,7 +25,7 @@ export const MAP_FILE = join(PKG_ROOT, "src", "adapters", "corpus", "conceptnet-
|
|
|
25
25
|
// The tier-1 curated Software-Engineering ontology (SEON): concepts.jsonl shares
|
|
26
26
|
// ConceptNet's slice shape and loads through the same loadSlice/loadMap/toFacts path.
|
|
27
27
|
// definitions.jsonl is a separate {term, definition, sense} list for lexicon lookups.
|
|
28
|
-
// Tier-2 corpuses
|
|
28
|
+
// Tier-2 corpuses share the slice shape too. The data lives under
|
|
29
29
|
// corpus/domains/code/ (the code domain pack's own directory); the bundle name
|
|
30
30
|
// "seon" and its "corpus:seon" provenance prefix are unchanged by the move —
|
|
31
31
|
// both are declared literally in src/services/extensions.mjs, not derived from
|
|
@@ -127,35 +127,30 @@ export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet")
|
|
|
127
127
|
return facts;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
131
|
-
* `limit`
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* `provenancePrefix` tags facts (default "corpus:conceptnet").
|
|
136
|
-
*
|
|
137
|
-
* `captureUnknownContext` (default false): also runs unknown-ingest.mjs's
|
|
138
|
-
* ingestUnknownFromAssertions so a term that only appears in an `ace="none"` row (never
|
|
139
|
-
* reified as a Fact) still lands in memory, tagged with the passage it was found in.
|
|
140
|
-
* `unknownContextLimit` bounds how many distinct terms one call captures (default 500).
|
|
141
|
-
* Loaded dynamically to avoid a load-time import cycle with unknown-ingest.mjs. */
|
|
142
|
-
export async function seedMemory(dir, {
|
|
143
|
-
limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix,
|
|
144
|
-
captureUnknownContext = false, unknownContextLimit,
|
|
145
|
-
} = {}) {
|
|
146
|
-
const [assertions, map] = await Promise.all([loadSlice(slicePath), loadMap(mapPath)]);
|
|
147
|
-
let facts = toFacts(assertions, map, provenancePrefix);
|
|
130
|
+
/** Stable-partition `facts` so the `prefer` predicates come first, then take the
|
|
131
|
+
* first `limit` of them. A capped band therefore buys its definitional backbone
|
|
132
|
+
* before it buys trivia. Either argument may be absent. */
|
|
133
|
+
export function preferThenLimit(facts, prefer, limit) {
|
|
134
|
+
let out = facts;
|
|
148
135
|
if (Array.isArray(prefer) && prefer.length) {
|
|
149
136
|
const rank = new Map(prefer.map((p, i) => [p, i]));
|
|
150
|
-
|
|
137
|
+
out = out.slice().sort((a, b) => (rank.get(a.predicate) ?? prefer.length) - (rank.get(b.predicate) ?? prefer.length));
|
|
151
138
|
}
|
|
152
|
-
|
|
139
|
+
return limit === undefined ? out : out.slice(0, limit);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Keyed with normFactTerm so it matches the store's own normalized read-back.
|
|
143
|
+
const factKey = (s, p, o) => `${normFactTerm(s)} ${p} ${normFactTerm(o)}`;
|
|
153
144
|
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
/** Write every fact `dir`'s store does not already hold, in one batched append,
|
|
146
|
+
* so re-seeding a band is idempotent and a term shared by two bands converges
|
|
147
|
+
* to one fact. `memory` is the store's already-loaded contents (callers that
|
|
148
|
+
* need it for their own work pass theirs rather than paying a second read).
|
|
149
|
+
* Returns { appended, skipped }. */
|
|
150
|
+
export async function appendNewFacts(dir, facts, memory) {
|
|
151
|
+
const store = memory ?? await loadMemory(dir);
|
|
156
152
|
const existing = new Set();
|
|
157
|
-
const
|
|
158
|
-
for (const ind of memory.individuals || []) {
|
|
153
|
+
for (const ind of store.individuals || []) {
|
|
159
154
|
if (ind?.class !== "Fact") continue;
|
|
160
155
|
const get = (key) => (ind.attributes || []).find((x) => x.key === key)?.value;
|
|
161
156
|
existing.add(factKey(get("subject"), get("predicate"), get("object")));
|
|
@@ -173,6 +168,30 @@ export async function seedMemory(dir, {
|
|
|
173
168
|
toWrite.push(fact);
|
|
174
169
|
}
|
|
175
170
|
const res = await appendFacts(dir, toWrite);
|
|
171
|
+
return { appended: res.appended, skipped: skipped + res.skipped };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Seed a repo's memory graph (<dir>/.tmct/memory/graph.json) from the committed slice.
|
|
175
|
+
* `limit` caps the facts written; `prefer` stable-partitions facts by predicate (so a
|
|
176
|
+
* capped seed favors the definitional band over whatever trivia the slice opens with).
|
|
177
|
+
* Idempotent: pre-reads the store to skip triples already there, then writes survivors
|
|
178
|
+
* in one batched appendFacts call. Returns { appended, skipped, total }.
|
|
179
|
+
* `provenancePrefix` tags facts (default "corpus:conceptnet").
|
|
180
|
+
*
|
|
181
|
+
* `captureUnknownContext` (default false): also runs unknown-ingest.mjs's
|
|
182
|
+
* ingestUnknownFromAssertions so a term that only appears in an `ace="none"` row (never
|
|
183
|
+
* reified as a Fact) still lands in memory, tagged with the passage it was found in.
|
|
184
|
+
* `unknownContextLimit` bounds how many distinct terms one call captures (default 500).
|
|
185
|
+
* Loaded dynamically to avoid a load-time import cycle with unknown-ingest.mjs. */
|
|
186
|
+
export async function seedMemory(dir, {
|
|
187
|
+
limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix,
|
|
188
|
+
captureUnknownContext = false, unknownContextLimit,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const [assertions, map] = await Promise.all([loadSlice(slicePath), loadMap(mapPath)]);
|
|
191
|
+
const facts = preferThenLimit(toFacts(assertions, map, provenancePrefix), prefer, limit);
|
|
192
|
+
|
|
193
|
+
const memory = await loadMemory(dir);
|
|
194
|
+
const { appended, skipped } = await appendNewFacts(dir, facts, memory);
|
|
176
195
|
|
|
177
196
|
let unknown;
|
|
178
197
|
if (captureUnknownContext) {
|
|
@@ -185,7 +204,7 @@ export async function seedMemory(dir, {
|
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
return {
|
|
188
|
-
appended
|
|
207
|
+
appended, skipped, total: facts.length,
|
|
189
208
|
...(unknown ? { unknown } : {}),
|
|
190
209
|
};
|
|
191
210
|
}
|
|
@@ -77,9 +77,13 @@ export function isResearchSource(source) {
|
|
|
77
77
|
* `research:<source>:<folded term>`. memory/trust.mjs reads the `research:`
|
|
78
78
|
* prefix back as a referenceLive Source, so a live-fetched fact scores below
|
|
79
79
|
* every curated pack, and the source segment keeps which adapter fetched it
|
|
80
|
-
* readable off the fact itself.
|
|
80
|
+
* readable off the fact itself. A multi-word term's internal spaces fold to
|
|
81
|
+
* underscores (ConceptNet's own /c/en/foo_bar convention, which normFactTerm
|
|
82
|
+
* already unwinds on the way in) so the tag stays one whitespace-free token —
|
|
83
|
+
* the same shape every other single-segment provenance tag carries, and safe
|
|
84
|
+
* under a caller that reads only the tag's first whitespace-split word. */
|
|
81
85
|
export function researchSourceTag(sourceName, term) {
|
|
82
|
-
return `research:${sourceName}:${normFactTerm(term)}`;
|
|
86
|
+
return `research:${sourceName}:${normFactTerm(term).replace(/ /g, "_")}`;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/** The facts a looked-up row licenses, each stamped with the source's own tag.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// corpus/wikidata-live.mjs — the live Wikidata research source.
|
|
2
|
-
//
|
|
1
|
+
// corpus/wikidata-live.mjs — the live Wikidata research source. Small GET
|
|
2
|
+
// round trips against www.wikidata.org's Action API, mapped onto tmct's
|
|
3
3
|
// seed-ontology relations at THIS boundary and nowhere else: everything
|
|
4
4
|
// downstream reads ordinary tmct facts and never learns the word "Wikidata".
|
|
5
5
|
//
|
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
// own rate policy and a SPARQL string built per term.
|
|
12
12
|
//
|
|
13
13
|
// The round trips:
|
|
14
|
-
// 1. wbsearchentities —
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// 1. wbsearchentities — every candidate item whose English label matches
|
|
15
|
+
// the term, best fold first.
|
|
16
|
+
// 2. wbgetentities — a candidate's label, description, revision, claims.
|
|
17
|
+
// 3. wbgetentities — the English labels of that candidate's mapped
|
|
18
|
+
// claims' object items, batched into ONE request. A
|
|
19
|
+
// claim's value is a Q-id, and a stored fact's object
|
|
20
|
+
// has to be a human term. Skipped when nothing
|
|
21
|
+
// mapped.
|
|
22
|
+
// Steps 2 and 3 repeat, candidate by candidate, only while the current one
|
|
23
|
+
// turns out to be a media or document work sharing the term's name rather
|
|
24
|
+
// than the term itself — the common case still costs three round trips.
|
|
20
25
|
//
|
|
21
26
|
// Courtesy is structural, mirroring wikipedia-live.mjs: one in-flight lookup
|
|
22
27
|
// at a time, a minimum interval between round trips, a 429/maxlag cool-off
|
|
@@ -72,6 +77,29 @@ export const WIKIDATA_PROPERTY_RELATIONS = Object.freeze({
|
|
|
72
77
|
// it would store from a prose lead sentence.
|
|
73
78
|
const ISA_PROPERTIES = ["P279", "P31"];
|
|
74
79
|
|
|
80
|
+
// A closed list of Wikidata item classes that name a media or document work,
|
|
81
|
+
// not the everyday concept a term search asked for. A search on "canadian
|
|
82
|
+
// companies" or "continents" can land on a paper or an album that merely
|
|
83
|
+
// SHARES the term's name — Wikidata's own title match, not a definition.
|
|
84
|
+
// Folded through normFactTerm the same way every isa term is, so the check
|
|
85
|
+
// compares like with like. Each class is a Wikidata English label read
|
|
86
|
+
// straight off the item, not a guess at one — refine this list from what
|
|
87
|
+
// Wikidata actually returns, keep it named and small.
|
|
88
|
+
const MEDIA_WORK_CLASSES = new Set([
|
|
89
|
+
"scholarly article",
|
|
90
|
+
"album",
|
|
91
|
+
"song",
|
|
92
|
+
"single",
|
|
93
|
+
"film",
|
|
94
|
+
"television series",
|
|
95
|
+
"television series episode",
|
|
96
|
+
"band",
|
|
97
|
+
"musical group",
|
|
98
|
+
"video game",
|
|
99
|
+
"book",
|
|
100
|
+
"novel",
|
|
101
|
+
]);
|
|
102
|
+
|
|
75
103
|
// How much of one item a single lookup reads: at most this many object values
|
|
76
104
|
// per property, and this many facts in total. A busy item like "human" carries
|
|
77
105
|
// hundreds of statements, and a research lookup wants the shape of the thing,
|
|
@@ -81,23 +109,27 @@ const MAX_FACTS_PER_ITEM = 12;
|
|
|
81
109
|
|
|
82
110
|
const ITEM_ID_RE = /^Q[1-9][0-9]*$/;
|
|
83
111
|
|
|
84
|
-
/**
|
|
112
|
+
/** Every candidate item whose English label folds onto the key, exact folds
|
|
113
|
+
* first then prefix folds, each group in the search result's own order — the
|
|
85
114
|
* topic-drift guard, matching wikipedia-live.mjs's: "quasar" may resolve to
|
|
86
115
|
* "quasar" or "quasars", never to the first suggestion about something else.
|
|
87
116
|
* An exact fold beats a prefix fold wherever it appears in the result list,
|
|
88
117
|
* so a search that ranks "Quasars (album)" above "quasar" still lands on the
|
|
89
|
-
* term the caller asked for.
|
|
90
|
-
|
|
118
|
+
* term the caller asked for first. Returning every candidate, not just the
|
|
119
|
+
* best one, lets the caller step to the next title match when the best one
|
|
120
|
+
* turns out to be a media work sharing the name. */
|
|
121
|
+
function candidateItemIds(key, body) {
|
|
91
122
|
const results = Array.isArray(body?.search) ? body.search : [];
|
|
92
|
-
|
|
123
|
+
const exact = [];
|
|
124
|
+
const prefix = [];
|
|
93
125
|
for (const hit of results) {
|
|
94
126
|
const id = String(hit?.id ?? "");
|
|
95
127
|
if (!ITEM_ID_RE.test(id)) continue;
|
|
96
128
|
const folded = normFactTerm(hit?.label ?? "");
|
|
97
|
-
if (folded === key)
|
|
98
|
-
if (
|
|
129
|
+
if (folded === key) exact.push(id);
|
|
130
|
+
else if (folded.startsWith(key)) prefix.push(id);
|
|
99
131
|
}
|
|
100
|
-
return
|
|
132
|
+
return [...exact, ...prefix];
|
|
101
133
|
}
|
|
102
134
|
|
|
103
135
|
/** Every mapped claim on an entity as {predicate, id} pairs, capped per
|
|
@@ -178,6 +210,11 @@ export function createWikidataLiveProvider({
|
|
|
178
210
|
return termById;
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
/** The looked-up item, or null when every candidate that matched the
|
|
214
|
+
* search either has no readable entity or turns out to be a media/document
|
|
215
|
+
* work sharing the term's name — a media-class isa is a wrong identity, not
|
|
216
|
+
* a definition, so the caller steps to the next title match instead of
|
|
217
|
+
* accepting it. Exhausting every candidate this way is the term missing. */
|
|
181
218
|
async function roundTrips(key) {
|
|
182
219
|
const search = await gate.fetchJson(actionUrl({
|
|
183
220
|
action: "wbsearchentities",
|
|
@@ -187,45 +224,49 @@ export function createWikidataLiveProvider({
|
|
|
187
224
|
limit: "5",
|
|
188
225
|
search: key,
|
|
189
226
|
}));
|
|
190
|
-
const
|
|
191
|
-
if (!id) return null;
|
|
227
|
+
const candidateIds = search ? candidateItemIds(key, search) : [];
|
|
192
228
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
for (const id of candidateIds) {
|
|
230
|
+
const read = await gate.fetchJson(actionUrl({
|
|
231
|
+
action: "wbgetentities",
|
|
232
|
+
languages: "en",
|
|
233
|
+
props: "labels|descriptions|claims|info",
|
|
234
|
+
ids: id,
|
|
235
|
+
}));
|
|
236
|
+
const entity = read?.entities?.[id];
|
|
237
|
+
if (!entity) continue;
|
|
201
238
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const seen = new Set();
|
|
207
|
-
for (const claim of claims) {
|
|
208
|
-
const object = termById.get(claim.id);
|
|
209
|
-
if (!object || object === key || seen.has(`${claim.predicate}\0${object}`)) continue;
|
|
210
|
-
seen.add(`${claim.predicate}\0${object}`);
|
|
211
|
-
facts.push({ subject: key, predicate: claim.predicate, object, provenance });
|
|
212
|
-
}
|
|
239
|
+
const claims = mappedClaims(entity);
|
|
240
|
+
const termById = await termsForIds(claims.map((c) => c.id));
|
|
241
|
+
const isa = isaFrom(claims, termById, key);
|
|
242
|
+
if (isa && MEDIA_WORK_CLASSES.has(isa)) continue;
|
|
213
243
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
244
|
+
const provenance = researchSourceTag(sourceName, key);
|
|
245
|
+
const facts = [];
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
for (const claim of claims) {
|
|
248
|
+
const object = termById.get(claim.id);
|
|
249
|
+
if (!object || object === key || seen.has(`${claim.predicate}\0${object}`)) continue;
|
|
250
|
+
seen.add(`${claim.predicate}\0${object}`);
|
|
251
|
+
facts.push({ subject: key, predicate: claim.predicate, object, provenance });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const description = String(entity.descriptions?.en?.value ?? "");
|
|
255
|
+
const row = {
|
|
256
|
+
term: key,
|
|
257
|
+
title: String(entity.labels?.en?.value ?? ""),
|
|
258
|
+
text: description,
|
|
259
|
+
summary: sentencesUpTo(description, SUMMARY_CHAR_CAP),
|
|
260
|
+
url: `${origin}/wiki/${id}`,
|
|
261
|
+
revid: Number(entity.lastrevid),
|
|
262
|
+
source: WIKIDATA_SOURCE_LABEL,
|
|
263
|
+
licence: WIKIDATA_LICENCE,
|
|
264
|
+
};
|
|
265
|
+
if (isa) row.isa = isa;
|
|
266
|
+
if (facts.length) row.facts = facts;
|
|
267
|
+
if (isResearchSourceRow(row)) return row;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
229
270
|
}
|
|
230
271
|
|
|
231
272
|
return {
|
|
@@ -27,6 +27,12 @@ const MAX_TOKENS_PER_BLOCK = 800; // beyond tokenizeProse's per-doc cap: union o
|
|
|
27
27
|
|
|
28
28
|
const blocksDir = (dir) => join(dir, BLOCKS_DIR_REL);
|
|
29
29
|
|
|
30
|
+
// Codepoint order, never localeCompare — the block ids this settles ties on
|
|
31
|
+
// decide WHICH blocks retrieveBlocks hands back, so a locale-dependent
|
|
32
|
+
// comparison would let two readers retrieve different text from one store.
|
|
33
|
+
// src/domain/memory/fact-order.mjs states the rule for the fact rows.
|
|
34
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
35
|
+
|
|
30
36
|
/** Only safe, filesystem-friendly block file names (ids are session uuids or
|
|
31
37
|
* corpus slugs; anything else is normalized, never trusted into a path). */
|
|
32
38
|
const safeName = (id) => String(id).replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 120) || "_";
|
|
@@ -216,7 +222,7 @@ export async function retrieveBlocks(dir, query, k = 3) {
|
|
|
216
222
|
id, score: (idfSum * (1 + rank) * trustFactor) / Math.sqrt(1 + degree), rank, trust, file: b.file,
|
|
217
223
|
});
|
|
218
224
|
}
|
|
219
|
-
scored.sort((a, b) => b.score - a.score || b.rank - a.rank || a.id
|
|
225
|
+
scored.sort((a, b) => b.score - a.score || b.rank - a.rank || byCodepoint(a.id, b.id));
|
|
220
226
|
const top = scored.slice(0, Math.max(1, k));
|
|
221
227
|
for (const hit of top) {
|
|
222
228
|
try { hit.text = await readFile(join(blocksDir(dir), hit.file), "utf8"); }
|