@polycode-projects/the-mechanical-code-talker 2.6.1 → 2.7.0
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/corpus/LICENSES.json +14 -0
- package/corpus/child/LICENSE-NOTICE +43 -0
- package/corpus/child/README.md +79 -0
- package/corpus/child/index.json.gz +0 -0
- package/corpus/child/manifest.json +313 -0
- package/corpus/child/shards/child-00.jsonl.gz +0 -0
- package/corpus/child/shards/child-01.jsonl.gz +0 -0
- package/corpus/child/shards/child-02.jsonl.gz +0 -0
- package/corpus/child/shards/child-03.jsonl.gz +0 -0
- package/corpus/child/shards/child-04.jsonl.gz +0 -0
- package/corpus/child/shards/child-05.jsonl.gz +0 -0
- package/corpus/child/shards/child-06.jsonl.gz +0 -0
- package/corpus/child/shards/child-07.jsonl.gz +0 -0
- package/corpus/child/shards/child-08.jsonl.gz +0 -0
- package/corpus/child/shards/child-09.jsonl.gz +0 -0
- package/corpus/child/shards/child-0a.jsonl.gz +0 -0
- package/corpus/child/shards/child-0b.jsonl.gz +0 -0
- package/corpus/child/shards/child-0c.jsonl.gz +0 -0
- package/corpus/child/shards/child-0d.jsonl.gz +0 -0
- package/corpus/child/shards/child-0e.jsonl.gz +0 -0
- package/corpus/child/shards/child-0f.jsonl.gz +0 -0
- package/corpus/child/shards/child-10.jsonl.gz +0 -0
- package/corpus/child/shards/child-11.jsonl.gz +0 -0
- package/corpus/child/shards/child-12.jsonl.gz +0 -0
- package/corpus/child/shards/child-13.jsonl.gz +0 -0
- package/corpus/child/shards/child-14.jsonl.gz +0 -0
- package/corpus/child/shards/child-15.jsonl.gz +0 -0
- package/corpus/child/shards/child-16.jsonl.gz +0 -0
- package/corpus/child/shards/child-17.jsonl.gz +0 -0
- package/corpus/child/shards/child-18.jsonl.gz +0 -0
- package/corpus/child/shards/child-19.jsonl.gz +0 -0
- package/corpus/child/shards/child-1a.jsonl.gz +0 -0
- package/corpus/child/shards/child-1b.jsonl.gz +0 -0
- package/corpus/child/shards/child-1c.jsonl.gz +0 -0
- package/corpus/child/shards/child-1d.jsonl.gz +0 -0
- package/corpus/child/shards/child-1e.jsonl.gz +0 -0
- package/corpus/child/shards/child-1f.jsonl.gz +0 -0
- package/corpus/conceptnet/child-seed.mjs +169 -0
- package/corpus/conceptnet/filter-dump.mjs +69 -48
- package/corpus/worlds/README.md +26 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +33 -0
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +64 -0
- package/package.json +4 -1
- package/src/adapters/corpus/child-pack.mjs +115 -0
- package/src/adapters/corpus/conceptnet-map.toml +7 -0
- package/src/adapters/corpus/worlds-pack.mjs +122 -0
- package/src/domain/child-pack.mjs +79 -0
- package/src/domain/grammar/ace.mjs +77 -0
- package/src/domain/grammar/lexicon-core.json +6 -0
- package/src/domain/memory/trust.mjs +8 -0
- package/src/domain/worlds-pack.mjs +71 -0
- package/src/services/adventure.mjs +669 -0
- package/src/services/chat.mjs +26 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +521 -39
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// corpus/worlds-pack.mjs — lazy, failure-tolerated loader for the shipped
|
|
2
|
+
// worlds pack (corpus/worlds/): a gzipped world index consulted first, then
|
|
3
|
+
// exactly one gzipped JSONL shard per world. Nothing here ever throws at a
|
|
4
|
+
// caller — an absent, truncated or corrupt pack reads as null, and a null is
|
|
5
|
+
// the ordinary honest decline ("no worlds pack here").
|
|
6
|
+
//
|
|
7
|
+
// The provider seam mirrors reference-pack.mjs's exactly:
|
|
8
|
+
// registerWorldsPackProvider swaps the whole lookup behind one async
|
|
9
|
+
// `{ list(), load(worldName) }` contract. It exists because a browser surface
|
|
10
|
+
// cannot read this filesystem layout — a web provider can fetch worlds
|
|
11
|
+
// instead, and chat code never knows which one it is talking to. No provider
|
|
12
|
+
// registered = the fs loader below.
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { gunzipSync } from "node:zlib";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { isWorldsIndexEntry, isWorldRow, isWorldFactRow, isWorldRuleRow, isWorldMetaRow } from "../../domain/worlds-pack.mjs";
|
|
19
|
+
|
|
20
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
21
|
+
|
|
22
|
+
/** The pack directory: TMCT_WORLDS_PACK_DIR when set, else the package's own
|
|
23
|
+
* corpus/worlds/. */
|
|
24
|
+
export function worldsPackDir(env = process.env) {
|
|
25
|
+
return env?.TMCT_WORLDS_PACK_DIR || join(PKG_ROOT, "corpus", "worlds");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indexCacheByDir = new Map(); // dir -> { worldName: {s} } | null
|
|
29
|
+
const worldCacheByKey = new Map(); // `${dir}\0${world}` -> payload | null
|
|
30
|
+
|
|
31
|
+
/** Drop every cached index/world — for tests that mutate a pack dir. */
|
|
32
|
+
export function clearWorldsPackCache() {
|
|
33
|
+
indexCacheByDir.clear();
|
|
34
|
+
worldCacheByKey.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readGunzipped(file) {
|
|
38
|
+
try {
|
|
39
|
+
return gunzipSync(readFileSync(file));
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The pack's world index, lazily read and cached per dir; null (cached)
|
|
46
|
+
* when the pack is absent or unreadable. Never throws. */
|
|
47
|
+
export function loadWorldsIndex(dir) {
|
|
48
|
+
if (indexCacheByDir.has(dir)) return indexCacheByDir.get(dir);
|
|
49
|
+
let index = null;
|
|
50
|
+
const body = readGunzipped(join(dir, "index.json.gz"));
|
|
51
|
+
if (body) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
54
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) index = parsed;
|
|
55
|
+
} catch { /* tolerated: a corrupt index is an absent pack */ }
|
|
56
|
+
}
|
|
57
|
+
indexCacheByDir.set(dir, index);
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One world by name: index hit -> the one shard the index names (cached) ->
|
|
62
|
+
* { facts, rules, meta }. Null on an index miss or an unreadable shard, so
|
|
63
|
+
* an unknown world never costs a shard read. Never throws. */
|
|
64
|
+
export function loadWorld(dir, worldName) {
|
|
65
|
+
const index = loadWorldsIndex(dir);
|
|
66
|
+
if (!index) return null;
|
|
67
|
+
const entry = index[String(worldName ?? "")];
|
|
68
|
+
if (!isWorldsIndexEntry(entry)) return null;
|
|
69
|
+
const key = `${dir}\0${worldName}`;
|
|
70
|
+
if (worldCacheByKey.has(key)) return worldCacheByKey.get(key);
|
|
71
|
+
let payload = null;
|
|
72
|
+
const body = readGunzipped(join(dir, "shards", `${entry.s}.jsonl.gz`));
|
|
73
|
+
if (body) {
|
|
74
|
+
const facts = [];
|
|
75
|
+
const rules = [];
|
|
76
|
+
let meta = null;
|
|
77
|
+
for (const line of body.toString("utf8").split("\n")) {
|
|
78
|
+
if (!line.trim()) continue;
|
|
79
|
+
try {
|
|
80
|
+
const row = JSON.parse(line);
|
|
81
|
+
if (!isWorldRow(row) || row.world !== worldName) continue;
|
|
82
|
+
if (isWorldFactRow(row)) facts.push(row);
|
|
83
|
+
else if (isWorldRuleRow(row)) rules.push(row);
|
|
84
|
+
else if (isWorldMetaRow(row) && !meta) meta = row;
|
|
85
|
+
} catch { /* tolerated: a bad line loses one row, not the world */ }
|
|
86
|
+
}
|
|
87
|
+
if (facts.length || rules.length || meta) payload = { name: worldName, facts, rules, meta };
|
|
88
|
+
}
|
|
89
|
+
worldCacheByKey.set(key, payload);
|
|
90
|
+
return payload;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fsProviderFor = (dirOf) => ({
|
|
94
|
+
list: async () => {
|
|
95
|
+
const index = loadWorldsIndex(dirOf());
|
|
96
|
+
return index ? Object.keys(index).sort() : null;
|
|
97
|
+
},
|
|
98
|
+
load: async (worldName) => loadWorld(dirOf(), worldName),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const fsProvider = fsProviderFor(() => worldsPackDir());
|
|
102
|
+
|
|
103
|
+
let registeredProvider = null;
|
|
104
|
+
|
|
105
|
+
/** Swap the pack lookup: provider = { list: async () => string[]|null,
|
|
106
|
+
* load: async (worldName) => payload|null }. Pass null to restore the
|
|
107
|
+
* default fs loader. */
|
|
108
|
+
export function registerWorldsPackProvider(provider) {
|
|
109
|
+
registeredProvider = provider
|
|
110
|
+
&& typeof provider.load === "function" && typeof provider.list === "function"
|
|
111
|
+
? provider : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The active provider — the registered one, else the lazy fs loader. An
|
|
115
|
+
* explicit `env` bag (a chat turn's own env, which may carry
|
|
116
|
+
* TMCT_WORLDS_PACK_DIR) makes the fs loader resolve the pack dir from that
|
|
117
|
+
* bag instead of process.env; with no argument the behavior is unchanged. */
|
|
118
|
+
export function getWorldsPackProvider(env) {
|
|
119
|
+
if (registeredProvider) return registeredProvider;
|
|
120
|
+
if (env === undefined) return fsProvider;
|
|
121
|
+
return fsProviderFor(() => worldsPackDir(env));
|
|
122
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// child-pack.mjs — the pure half of the shipped CHILD triples pack
|
|
2
|
+
// (corpus/child/): the shard naming contract, the index/row shape validators
|
|
3
|
+
// every writer and reader share, and the provenance tag a fact learned from the
|
|
4
|
+
// pack carries. The pack itself is gzipped JSONL shards plus a gzipped term
|
|
5
|
+
// index; loading them is I/O and lives in src/adapters/corpus/child-pack.mjs.
|
|
6
|
+
//
|
|
7
|
+
// The pack is a lazy learn-on-miss reference the clean-miss cascade consults:
|
|
8
|
+
// on a miss for term T the loader returns T's ConceptNet triples, and the chat
|
|
9
|
+
// hook appends them to memory (see childProvenanceTag). It mirrors the reference
|
|
10
|
+
// pack's on-disk shape (index -> one shard per hit) but carries TRIPLES, not
|
|
11
|
+
// article prose — so its row is a list of {subject, predicate, object} facts,
|
|
12
|
+
// already mapped through conceptnet-map.toml into tmct's predicate vocabulary.
|
|
13
|
+
|
|
14
|
+
import { fnv1aHex, normFactTerm } from "./hash.mjs";
|
|
15
|
+
|
|
16
|
+
export const CHILD_PACK_NAME = "conceptnet";
|
|
17
|
+
export const CHILD_SHARD_COUNT = 32;
|
|
18
|
+
|
|
19
|
+
/** The shard a term's triples row lives in: FNV-1a first byte mod 32, as the
|
|
20
|
+
* file basename "child-00" … "child-1f". Part of the pack's on-disk contract —
|
|
21
|
+
* the build script shards with THIS function, so the reader never scans. The
|
|
22
|
+
* term is normFactTerm-folded first, so the key the index stores and the key a
|
|
23
|
+
* clean-miss lookup computes are the same spelling. */
|
|
24
|
+
export function shardNameFor(term) {
|
|
25
|
+
const byte = parseInt(fnv1aHex(normFactTerm(term)).slice(0, 2), 16);
|
|
26
|
+
return `child-${(byte % CHILD_SHARD_COUNT).toString(16).padStart(2, "0")}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SHARD_NAME_RE = /^child-[0-1][0-9a-f]$/;
|
|
30
|
+
|
|
31
|
+
/** An index entry {s, t, n}: the shard holding the row, the row's canonical
|
|
32
|
+
* term key (a normFactTerm fixed point), and the fact count (a positive
|
|
33
|
+
* integer, so a zero-fact row can never be indexed). */
|
|
34
|
+
export function isChildIndexEntry(e) {
|
|
35
|
+
return !!e && typeof e === "object"
|
|
36
|
+
&& typeof e.s === "string" && SHARD_NAME_RE.test(e.s)
|
|
37
|
+
&& typeof e.t === "string" && e.t.length > 0
|
|
38
|
+
&& Number.isInteger(e.n) && e.n > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One triple in a row: {subject, predicate, object, weight?}. subject/object
|
|
42
|
+
* are human terms ("penguin", "bird"); predicate is a tmct vocabulary URI
|
|
43
|
+
* ("rdfs:subClassOf", "mgx:capableOf", "mgxneg:capableOf"). weight, when
|
|
44
|
+
* present, is the ConceptNet edge weight (a positive number). */
|
|
45
|
+
export function isChildFact(f) {
|
|
46
|
+
if (!f || typeof f !== "object") return false;
|
|
47
|
+
for (const field of ["subject", "predicate", "object"]) {
|
|
48
|
+
if (typeof f[field] !== "string" || !f[field]) return false;
|
|
49
|
+
}
|
|
50
|
+
if (f.weight !== undefined && !(typeof f.weight === "number" && Number.isFinite(f.weight) && f.weight > 0)) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A shard row: {term, facts: [ChildFact, …]} — the term's edges, at least one,
|
|
55
|
+
* every one a valid ChildFact that actually touches the term (as subject or
|
|
56
|
+
* object, once normalised). */
|
|
57
|
+
export function isChildFactsRow(row) {
|
|
58
|
+
if (!row || typeof row !== "object") return false;
|
|
59
|
+
if (typeof row.term !== "string" || !row.term) return false;
|
|
60
|
+
if (!Array.isArray(row.facts) || row.facts.length === 0) return false;
|
|
61
|
+
for (const f of row.facts) {
|
|
62
|
+
if (!isChildFact(f)) return false;
|
|
63
|
+
if (normFactTerm(f.subject) !== row.term && normFactTerm(f.object) !== row.term) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The provenance tag a fact stored from a child-pack lookup carries. The chat
|
|
69
|
+
* hook stamps every fact it appends from term T's row with THIS tag;
|
|
70
|
+
* memory/trust.mjs parses it back to a corpus-tier Source ({kind:"corpus",
|
|
71
|
+
* name:"conceptnet"}) — the child slice is curated ConceptNet, scored at the
|
|
72
|
+
* 0.7 corpus prior. The term segment records which miss pulled the fact in, so
|
|
73
|
+
* a fact's origin shard stays auditable on its factProvenance string even
|
|
74
|
+
* though the Source it corroborates is the shared ConceptNet corpus. */
|
|
75
|
+
export function childProvenanceTag(term) {
|
|
76
|
+
return `${CHILD_PROVENANCE_PREFIX}${CHILD_PACK_NAME}:${normFactTerm(term)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const CHILD_PROVENANCE_PREFIX = "child:";
|
|
@@ -457,6 +457,83 @@ export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
|
457
457
|
return parseRelation(lexicon, toks, lower);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// ---- the imperative command pattern -----------------------------------------
|
|
461
|
+
// A subjectless action command ("go north", "take the key", "unlock the
|
|
462
|
+
// cabinet with the key"). Unlike the nine assertion/question patterns above,
|
|
463
|
+
// this one produces no OWL triple — an imperative has no truth value to
|
|
464
|
+
// assert, it has an ACTION NAME to resolve against the taught action
|
|
465
|
+
// families — so parseImperative returns a structured command instead
|
|
466
|
+
// (precedent: parseCardinality's own non-triple `n`). It is a separate
|
|
467
|
+
// export, never folded into parseAce: every triple pattern requires an
|
|
468
|
+
// explicit subject noun phrase, and parseAce's callers expect triples.
|
|
469
|
+
//
|
|
470
|
+
// The verb set is CLOSED (an unlisted verb is a hard null, never a guess),
|
|
471
|
+
// and object phrases resolve through the same lexicon-noun gate as every
|
|
472
|
+
// other pattern: a structural fit over an undeclared word rides out as
|
|
473
|
+
// `residue` so the caller can name it; a declared word in an unusable shape
|
|
474
|
+
// is a hard null.
|
|
475
|
+
|
|
476
|
+
const IMPERATIVE_VERBS = new Set(["go", "take", "drop", "open", "unlock", "close", "give", "look"]);
|
|
477
|
+
const IMPERATIVE_DIRECTIONS = new Set(["north", "south", "east", "west", "up", "down"]);
|
|
478
|
+
|
|
479
|
+
/** Resolve one imperative object phrase to its bare lexicon term. */
|
|
480
|
+
function imperativeNP(lexicon, tokens) {
|
|
481
|
+
const np = resolveNP(lexicon, tokens);
|
|
482
|
+
if (np.term == null) return { term: null, unknown: np.unknown };
|
|
483
|
+
return { term: local(lexicon, np.term), unknown: [] };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Parse one imperative command against the closed verb set. Returns
|
|
488
|
+
* `{ pattern: "imperative", verb, residue, object?, indirectObject?,
|
|
489
|
+
* instrument?, direction? }`, a residue-carrying miss for a structural fit
|
|
490
|
+
* over undeclared words (`residue` non-empty, no slots), or null when the
|
|
491
|
+
* sentence is not an imperative of this fragment at all.
|
|
492
|
+
*/
|
|
493
|
+
export function parseImperative(sentence, lexicon = loadLexicon()) {
|
|
494
|
+
const toks = tokenize(sentence);
|
|
495
|
+
if (!toks.length) return null;
|
|
496
|
+
const verb = toks[0].toLowerCase();
|
|
497
|
+
if (!IMPERATIVE_VERBS.has(verb)) return null;
|
|
498
|
+
const rest = toks.slice(1);
|
|
499
|
+
const lower = rest.map((t) => t.toLowerCase());
|
|
500
|
+
const command = (fields) => ({ pattern: "imperative", verb, residue: [], ...fields });
|
|
501
|
+
const miss = (unknown) => (unknown.length ? { pattern: "imperative", verb, residue: unknown } : null);
|
|
502
|
+
|
|
503
|
+
if (verb === "look") {
|
|
504
|
+
if (!rest.length || (rest.length === 1 && lower[0] === "around")) return command({});
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (verb === "go") {
|
|
508
|
+
if (rest.length === 1 && IMPERATIVE_DIRECTIONS.has(lower[0])) return command({ direction: lower[0] });
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
if (verb === "give") {
|
|
512
|
+
const toIdx = lower.indexOf("to");
|
|
513
|
+
if (toIdx < 1 || toIdx === rest.length - 1) return null;
|
|
514
|
+
const object = imperativeNP(lexicon, rest.slice(0, toIdx));
|
|
515
|
+
const indirect = imperativeNP(lexicon, rest.slice(toIdx + 1));
|
|
516
|
+
if (object.term == null || indirect.term == null) return miss([...object.unknown, ...indirect.unknown]);
|
|
517
|
+
return command({ object: object.term, indirectObject: indirect.term });
|
|
518
|
+
}
|
|
519
|
+
if (verb === "unlock") {
|
|
520
|
+
const withIdx = lower.indexOf("with");
|
|
521
|
+
if (withIdx !== -1) {
|
|
522
|
+
if (withIdx < 1 || withIdx === rest.length - 1) return null;
|
|
523
|
+
const object = imperativeNP(lexicon, rest.slice(0, withIdx));
|
|
524
|
+
const instrument = imperativeNP(lexicon, rest.slice(withIdx + 1));
|
|
525
|
+
if (object.term == null || instrument.term == null) return miss([...object.unknown, ...instrument.unknown]);
|
|
526
|
+
return command({ object: object.term, instrument: instrument.term });
|
|
527
|
+
}
|
|
528
|
+
// fall through to the plain-object arm: "unlock the cabinet" is a valid
|
|
529
|
+
// command whose missing instrument is the CALLER's precondition to name.
|
|
530
|
+
}
|
|
531
|
+
if (!rest.length) return null;
|
|
532
|
+
const object = imperativeNP(lexicon, rest);
|
|
533
|
+
if (object.term == null) return miss(object.unknown);
|
|
534
|
+
return command({ object: object.term });
|
|
535
|
+
}
|
|
536
|
+
|
|
460
537
|
/** Pattern 9 — "N can VERB" → mgx:capableOf. The modal is not a relation
|
|
461
538
|
* verb: without this, parseRelation reads "can" through lookupVerb and
|
|
462
539
|
* asserts a generic object property ("dog cans swim") that no capability
|
|
@@ -274,6 +274,9 @@
|
|
|
274
274
|
"judge": {},
|
|
275
275
|
"priest": {},
|
|
276
276
|
"servant": {},
|
|
277
|
+
"butler": {},
|
|
278
|
+
"housekeeper": {},
|
|
279
|
+
"gardener": {},
|
|
277
280
|
"employee": {},
|
|
278
281
|
"boss": {},
|
|
279
282
|
"husband": {},
|
|
@@ -328,6 +331,9 @@
|
|
|
328
331
|
"dress": {},
|
|
329
332
|
"clothing": {},
|
|
330
333
|
"chair": {},
|
|
334
|
+
"desk": {},
|
|
335
|
+
"lamp": {},
|
|
336
|
+
"portrait": {},
|
|
331
337
|
"bed": {},
|
|
332
338
|
"door": {},
|
|
333
339
|
"window": {},
|
|
@@ -30,6 +30,11 @@ function parseChatTagRest(rest) {
|
|
|
30
30
|
* kind set (the kinds SOURCE_PRIOR scores):
|
|
31
31
|
* corpus:conceptnet /r/IsA -> { kind:"corpus", name:"conceptnet" }
|
|
32
32
|
* corpus-weak:conceptnet /r/RelatedTo -> { kind:"corpusWeak", name:"conceptnet" }
|
|
33
|
+
* child:conceptnet:<term> -> { kind:"corpus", name:"conceptnet" }
|
|
34
|
+
* (the lazy child triples pack is curated ConceptNet — same corpus tier,
|
|
35
|
+
* same 0.7 prior, same shared Source as the bulk conceptnet import; the
|
|
36
|
+
* <term> segment records which miss pulled the fact in and is not part of
|
|
37
|
+
* the Source identity)
|
|
33
38
|
* ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
|
|
34
39
|
* teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
|
|
35
40
|
* web:<url> | url:<url> -> { kind:"web", url:<url> }
|
|
@@ -54,6 +59,9 @@ export function provenanceTagToSource(tag) {
|
|
|
54
59
|
const head = t.split(/\s+/)[0]; // drop trailing " /r/IsA" etc.
|
|
55
60
|
if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
|
|
56
61
|
if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
|
|
62
|
+
// child:<pack>:<term> — the lazy child triples pack, scored at the corpus tier
|
|
63
|
+
// under the pack's shared Source; the per-term tail is dropped from the id.
|
|
64
|
+
if (head.startsWith("child:")) return { kind: "corpus", name: head.slice("child:".length).split(":")[0] || "unknown" };
|
|
57
65
|
if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
|
|
58
66
|
if (head.startsWith("teach:")) {
|
|
59
67
|
// the chat teach lane's natural frames — chat.mjs's teachProvenanceTag
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// worlds-pack.mjs — the pure half of the shipped worlds pack: the row-shape
|
|
2
|
+
// validators every writer and reader share, the provenance tag a loaded
|
|
3
|
+
// world's facts carry, and the closed rule-kind set a world may instate. The
|
|
4
|
+
// pack itself is one gzipped JSONL shard per world plus a gzipped world
|
|
5
|
+
// index; loading them is I/O and lives in src/adapters/corpus/worlds-pack.mjs.
|
|
6
|
+
//
|
|
7
|
+
// A world row is one of three kinds:
|
|
8
|
+
// fact — an ordinary graph triple the loader appends into the session's
|
|
9
|
+
// memory store (rooms, exits, placements, NPC cast);
|
|
10
|
+
// rule — a pre-built action-Rule row (the same four action kinds the live
|
|
11
|
+
// teach frames store) the loader instates via appendRule;
|
|
12
|
+
// meta — the world's one announcement row (the opening line).
|
|
13
|
+
|
|
14
|
+
const WORLD_NAME_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
15
|
+
|
|
16
|
+
/** The action-rule kinds a world shard may carry — the same closed set
|
|
17
|
+
* src/adapters/memory/core.mjs stores for live-taught actions. */
|
|
18
|
+
export const WORLD_RULE_KINDS = Object.freeze([
|
|
19
|
+
"action-signature", "action-precond", "action-effect", "action-constraint",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const RULE_KIND_SET = new Set(WORLD_RULE_KINDS);
|
|
23
|
+
|
|
24
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
|
|
25
|
+
|
|
26
|
+
/** A pack world name: lowercase, hyphen-joined ("ashcombe-hall"). */
|
|
27
|
+
export function isWorldName(name) {
|
|
28
|
+
return typeof name === "string" && WORLD_NAME_RE.test(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An index entry { s }: the shard (basename, no extension) holding the
|
|
32
|
+
* world's rows. */
|
|
33
|
+
export function isWorldsIndexEntry(e) {
|
|
34
|
+
return !!e && typeof e === "object" && isNonEmptyString(e.s);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A fact row: { world, kind:"fact", subject, predicate, object }. */
|
|
38
|
+
export function isWorldFactRow(row) {
|
|
39
|
+
return !!row && typeof row === "object" && row.kind === "fact"
|
|
40
|
+
&& isWorldName(row.world)
|
|
41
|
+
&& isNonEmptyString(row.subject) && isNonEmptyString(row.predicate) && isNonEmptyString(row.object);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A rule row: { world, kind:"rule", name, ruleKind, slots } — ruleKind one
|
|
45
|
+
* of WORLD_RULE_KINDS, slots a flat object of non-empty strings (the exact
|
|
46
|
+
* per-kind slot contract is appendRule's to enforce at instate time). */
|
|
47
|
+
export function isWorldRuleRow(row) {
|
|
48
|
+
if (!row || typeof row !== "object" || row.kind !== "rule") return false;
|
|
49
|
+
if (!isWorldName(row.world) || !isNonEmptyString(row.name)) return false;
|
|
50
|
+
if (!RULE_KIND_SET.has(row.ruleKind)) return false;
|
|
51
|
+
if (!row.slots || typeof row.slots !== "object" || Array.isArray(row.slots)) return false;
|
|
52
|
+
const values = Object.values(row.slots);
|
|
53
|
+
return values.length > 0 && values.every(isNonEmptyString);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A meta row: { world, kind:"meta", opening } — the world's opening line. */
|
|
57
|
+
export function isWorldMetaRow(row) {
|
|
58
|
+
return !!row && typeof row === "object" && row.kind === "meta"
|
|
59
|
+
&& isWorldName(row.world) && isNonEmptyString(row.opening);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Any valid world row. */
|
|
63
|
+
export function isWorldRow(row) {
|
|
64
|
+
return isWorldFactRow(row) || isWorldRuleRow(row) || isWorldMetaRow(row);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The provenance tag every fact/rule loaded from a world carries —
|
|
68
|
+
* "world:<name>", so a loaded world is auditable apart from taught facts. */
|
|
69
|
+
export function worldProvenanceTag(worldName) {
|
|
70
|
+
return `world:${worldName}`;
|
|
71
|
+
}
|